6th
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Read(C:\\Users\\EGG\\WORK\\data\\user_scrip\\TOOL\\semiauto-assistant/**)",
|
||||
"Read(C:\\Users\\EGG\\WORK\\data\\user_scrip\\TOOL\\TEMP_spec_system_V3/**)",
|
||||
"Read(C:\\Users\\EGG\\.claude/**)",
|
||||
"Bash(rm:*)",
|
||||
"Bash(npm test)",
|
||||
"Bash(npm install)",
|
||||
"Bash(npm test:*)",
|
||||
"Bash(python -m pytest --version)",
|
||||
"Bash(python -m pytest tests/test_models.py -v)",
|
||||
"Bash(python:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(./venv/Scripts/activate)",
|
||||
"Bash(npm start)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(copy:*)",
|
||||
"Bash(del check_enum.py)",
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(del \"src\\components\\notifications\\EmailNotificationSettings.tsx\")",
|
||||
"mcp__puppeteer__puppeteer_connect_active_tab",
|
||||
"Bash(start chrome:*)",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(TASKKILL:*)",
|
||||
"Bash(wmic process where:*)",
|
||||
"Bash(\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" --remote-debugging-port=9222 --user-data-dir=\"%TEMP%\\chrome-debug\" http://localhost:3000/todos)",
|
||||
"Bash(timeout:*)",
|
||||
"Bash(ping:*)",
|
||||
"mcp__puppeteer__puppeteer_navigate",
|
||||
"mcp__puppeteer__puppeteer_screenshot",
|
||||
"mcp__puppeteer__puppeteer_fill",
|
||||
"mcp__puppeteer__puppeteer_click",
|
||||
"mcp__puppeteer__puppeteer_evaluate",
|
||||
"Bash(tree:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
32
.gitignore
vendored
32
.gitignore
vendored
@@ -1,6 +1,3 @@
|
||||
# --- 敏感資訊 (Sensitive Information) ---
|
||||
# 忽略包含所有密鑰和資料庫連線資訊的環境變數檔案。
|
||||
|
||||
# --- Python 相關 (Python Related) ---
|
||||
# 忽略虛擬環境目錄。
|
||||
.venv/
|
||||
@@ -29,7 +26,6 @@ node_modules/
|
||||
.next/
|
||||
.swc/
|
||||
|
||||
|
||||
# --- 作業系統相關 (Operating System) ---
|
||||
# 忽略 macOS 的系統檔案。
|
||||
.DS_Store
|
||||
@@ -42,4 +38,32 @@ Thumbs.db
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# --- 臨時文件 ---
|
||||
nul
|
||||
temp/
|
||||
tmp/
|
||||
|
||||
# --- 測試相關 ---
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
coverage.xml
|
||||
htmlcov/
|
||||
|
||||
# --- 資料庫相關 ---
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# --- 前端相關 ---
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
dist/
|
||||
*.tsbuildinfo
|
||||
.eslintcache
|
||||
|
||||
# 注意:根據需求,我們允許 .env 文件上傳(因為是私有倉庫)
|
||||
# 如果這是公開倉庫,請取消以下注釋:
|
||||
# .env
|
||||
# .env.local
|
||||
# .env.production
|
1089
BEST_PRACTICES.md
1089
BEST_PRACTICES.md
File diff suppressed because it is too large
Load Diff
556
README.md
556
README.md
@@ -1,401 +1,301 @@
|
||||
# PANJIT To-Do System
|
||||
# PANJIT To-Do 專案管理系統
|
||||
|
||||
一個功能完整的企業級待辦事項管理系統,支援 LDAP 認證、郵件通知、Excel 匯入匯出,以及豐富的協作功能。
|
||||
一套完整的企業級待辦事項管理系統,支援AD認證、郵件通知、Excel匯入匯出等功能。
|
||||
|
||||
## 🚀 功能特色
|
||||
|
||||
### 核心功能
|
||||
- **待辦事項管理**:建立、編輯、刪除、狀態管理
|
||||
- **多人協作**:負責人指派、追蹤人員、權限控制
|
||||
- **狀態管理**:新建立 → 進行中 → 已阻礙 → 已完成
|
||||
- **優先級分類**:低、中、高、緊急
|
||||
- **到期日管理**:日期設定、逾期提醒
|
||||
- **待辦事項管理**:新增、編輯、刪除、狀態管理
|
||||
- **多重視圖**:列表視圖、日曆視圖、看板視圖
|
||||
- **權限控制**:創建者、負責人、追蹤者角色管理
|
||||
- **狀態追蹤**:新建立、進行中、已阻塞、已完成
|
||||
|
||||
### 高級功能
|
||||
- **LDAP/Active Directory 整合**:企業帳號統一認證
|
||||
- **智能搜尋**:模糊搜尋、多條件篩選
|
||||
- **Excel 匯入匯出**:批量資料處理
|
||||
- **郵件通知系統**:自動提醒、狀態變更通知
|
||||
- **行事曆檢視**:時間軸管理
|
||||
- **批量操作**:多項目同時處理
|
||||
- **公開/私人模式**:靈活的可見性控制
|
||||
### 身份認證
|
||||
- **AD 整合**:支援Active Directory單一登入
|
||||
- **角色權限**:管理員、一般使用者權限管控
|
||||
- **安全防護**:JWT Token認證機制
|
||||
|
||||
### 技術特色
|
||||
- **現代化架構**:前後端分離設計
|
||||
- **響應式設計**:支援桌面和行動裝置
|
||||
- **深色/淺色主題**:使用者體驗優化
|
||||
- **即時更新**:React Query 資料同步
|
||||
- **任務排程**:Celery 背景處理
|
||||
- **健康檢查**:系統狀態監控
|
||||
### 通知系統
|
||||
- **郵件通知**:狀態變更、到期提醒自動通知
|
||||
- **即時推播**:系統內通知面板
|
||||
- **自定義設定**:個人化通知偏好
|
||||
|
||||
## 🏗️ 技術架構
|
||||
### 資料處理
|
||||
- **Excel 匯入**:批量匯入待辦事項
|
||||
- **Excel 匯出**:支援多種匯出格式
|
||||
- **報表功能**:統計分析報表
|
||||
|
||||
### 前端 (Frontend)
|
||||
- **Next.js 14** - React 全端框架
|
||||
- **TypeScript** - 類型安全開發
|
||||
- **Material-UI** - 企業級 UI 組件
|
||||
- **Redux Toolkit** - 狀態管理
|
||||
- **TanStack Query** - 服務端狀態管理
|
||||
### 使用者介面
|
||||
- **響應式設計**:支援桌面、平板、手機
|
||||
- **深色模式**:亮色/暗色/自動切換
|
||||
- **多語言支援**:繁體中文介面
|
||||
- **動畫效果**:流暢的使用者體驗
|
||||
|
||||
### 後端 (Backend)
|
||||
- **Flask** - Python Web 框架
|
||||
- **SQLAlchemy** - ORM 資料庫管理
|
||||
- **MySQL** - 主要資料庫
|
||||
- **Celery + Redis** - 背景任務處理
|
||||
- **JWT** - 身份驗證
|
||||
- **python-ldap** - AD/LDAP 整合
|
||||
## 🛠 技術架構
|
||||
|
||||
### 部署 (Deployment)
|
||||
- **Docker** - 容器化部署
|
||||
- **Nginx** - 反向代理
|
||||
- **MySQL 8.0** - 資料庫服務
|
||||
- **Redis** - 快取與任務佇列
|
||||
### 前端技術棧
|
||||
- **框架**:Next.js 14 (React 18)
|
||||
- **UI 庫**:Material-UI (MUI) 5
|
||||
- **狀態管理**:Redux Toolkit + React Query
|
||||
- **樣式方案**:Emotion + CSS-in-JS
|
||||
- **動畫庫**:Framer Motion
|
||||
- **類型檢查**:TypeScript
|
||||
- **構建工具**:Next.js + SWC
|
||||
|
||||
### 後端技術棧
|
||||
- **框架**:Flask 2.3
|
||||
- **資料庫**:MySQL + SQLAlchemy ORM
|
||||
- **認證系統**:Flask-JWT-Extended
|
||||
- **LDAP 整合**:ldap3 (Windows 相容)
|
||||
- **任務佇列**:Celery + Redis
|
||||
- **郵件服務**:Flask-Mail + SMTP
|
||||
- **檔案處理**:pandas + openpyxl
|
||||
- **API 文檔**:Flask-RESTful
|
||||
|
||||
### 基礎設施
|
||||
- **資料庫**:MySQL 8.0
|
||||
- **快取系統**:Redis
|
||||
- **檔案儲存**:本地檔案系統
|
||||
- **日誌管理**:colorlog
|
||||
- **部署環境**:Windows Server
|
||||
|
||||
## 📋 系統需求
|
||||
|
||||
- **Node.js** >= 18.0
|
||||
- **Python** >= 3.11
|
||||
- **MySQL** >= 8.0
|
||||
- **Redis** >= 6.0
|
||||
### 開發環境
|
||||
- **Node.js**:16.x 或以上版本
|
||||
- **Python**:3.10 或以上版本
|
||||
- **MySQL**:8.0 或以上版本
|
||||
- **Redis**:6.0 或以上版本
|
||||
|
||||
## 🛠️ 本地開發安裝
|
||||
### 生產環境
|
||||
- **作業系統**:Windows Server 2016+ 或 Linux
|
||||
- **記憶體**:最低 4GB,建議 8GB
|
||||
- **磁碟空間**:最低 10GB
|
||||
- **網路**:可連接 SMTP 和 LDAP 伺服器
|
||||
|
||||
### 1. 克隆專案
|
||||
## 🚀 快速開始
|
||||
|
||||
### 1. 專案複製
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd TODOLIST
|
||||
```
|
||||
|
||||
### 2. 資料庫準備
|
||||
|
||||
#### 方式一:使用 Docker (推薦)
|
||||
```bash
|
||||
# 啟動 MySQL 和 Redis
|
||||
docker-compose up mysql redis -d
|
||||
|
||||
# 等待資料庫啟動完成 (大約30秒)
|
||||
docker-compose logs mysql
|
||||
```
|
||||
|
||||
#### 方式二:本地 MySQL
|
||||
```bash
|
||||
# 建立資料庫
|
||||
mysql -u root -p
|
||||
CREATE DATABASE todo_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER 'todouser'@'localhost' IDENTIFIED BY 'todopass';
|
||||
GRANT ALL PRIVILEGES ON todo_system.* TO 'todouser'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
EXIT;
|
||||
|
||||
# 初始化資料庫結構
|
||||
mysql -u todouser -p todo_system < mysql/init/01-init.sql
|
||||
```
|
||||
|
||||
### 3. 後端設定
|
||||
|
||||
### 2. 後端設置
|
||||
```bash
|
||||
# 進入後端目錄
|
||||
cd backend
|
||||
|
||||
# 建立虛擬環境
|
||||
# 建立虛擬環境 (Windows)
|
||||
python -m venv venv
|
||||
venv\Scripts\activate # Windows
|
||||
# source venv/bin/activate # Linux/macOS
|
||||
venv\Scripts\activate
|
||||
|
||||
# 安裝依賴
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 複製環境設定檔並修改
|
||||
copy .env.example .env # Windows
|
||||
# cp .env.example .env # Linux/macOS
|
||||
# 設置環境變數
|
||||
copy .env.example .env
|
||||
# 編輯 .env 文件,填入正確的設定值
|
||||
|
||||
# 編輯 .env 檔案,設定資料庫連線資訊
|
||||
# 初始化資料庫
|
||||
python init_db.py
|
||||
|
||||
# 啟動後端服務
|
||||
python app.py
|
||||
```
|
||||
|
||||
#### 重要環境變數設定
|
||||
|
||||
編輯 `backend/.env` 檔案:
|
||||
|
||||
```env
|
||||
# MySQL 連線 (如使用Docker,保持預設值即可)
|
||||
MYSQL_HOST=localhost
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USER=todouser
|
||||
MYSQL_PASSWORD=todopass
|
||||
MYSQL_DATABASE=todo_system
|
||||
|
||||
# SMTP 設定 (依照公司環境設定)
|
||||
SMTP_SERVER=mail.your-company.com
|
||||
SMTP_PORT=25
|
||||
SMTP_SENDER_EMAIL=todo-system@your-company.com
|
||||
|
||||
# AD/LDAP 設定 (依照公司環境設定)
|
||||
LDAP_SERVER=ldap://dc.your-company.com
|
||||
LDAP_SEARCH_BASE=DC=your-company,DC=com
|
||||
```
|
||||
|
||||
### 4. 前端設定
|
||||
|
||||
### 3. 前端設置
|
||||
```bash
|
||||
# 進入前端目錄
|
||||
cd frontend
|
||||
|
||||
# 安裝依賴
|
||||
npm install
|
||||
|
||||
# 複製環境設定檔
|
||||
copy .env.example .env.local # Windows
|
||||
# cp .env.example .env.local # Linux/macOS
|
||||
|
||||
# 編輯 .env.local 設定 API URL
|
||||
```
|
||||
|
||||
編輯 `frontend/.env.local`:
|
||||
```env
|
||||
NEXT_PUBLIC_API_URL=http://localhost:5000
|
||||
NEXT_PUBLIC_AD_DOMAIN=your-company.com.tw
|
||||
NEXT_PUBLIC_EMAIL_DOMAIN=your-company.com.tw
|
||||
```
|
||||
|
||||
### 5. 啟動應用程式
|
||||
|
||||
#### 後端啟動
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# 啟動 Flask 應用程式
|
||||
python app.py
|
||||
|
||||
# 後端將在 http://localhost:5000 啟動
|
||||
```
|
||||
|
||||
#### 前端啟動 (另開終端)
|
||||
```bash
|
||||
cd frontend
|
||||
# 設置環境變數
|
||||
copy .env.example .env.local
|
||||
# 編輯 .env.local 文件
|
||||
|
||||
# 啟動開發服務器
|
||||
npm run dev
|
||||
|
||||
# 前端將在 http://localhost:3000 啟動
|
||||
```
|
||||
|
||||
#### Celery 背景任務 (另開終端)
|
||||
### 4. 背景任務 (可選)
|
||||
```bash
|
||||
# 在另一個終端啟動 Celery Worker
|
||||
cd backend
|
||||
celery -A celery_app worker --loglevel=info
|
||||
|
||||
# 啟動 Celery Worker
|
||||
celery -A celery_app.celery worker --loglevel=info
|
||||
|
||||
# 啟動 Celery Beat (排程任務)
|
||||
celery -A celery_app.celery beat --loglevel=info
|
||||
# 啟動任務調度器
|
||||
celery -A celery_app beat --loglevel=info
|
||||
```
|
||||
|
||||
## 🌐 訪問應用程式
|
||||
## ⚙️ 設定說明
|
||||
|
||||
- **前端界面**: http://localhost:3000
|
||||
- **後端 API**: http://localhost:5000
|
||||
- **API 文檔**: http://localhost:5000/api (Swagger UI)
|
||||
- **健康檢查**: http://localhost:5000/api/health/healthz
|
||||
### 環境變數配置
|
||||
|
||||
## 📁 專案結構
|
||||
#### 後端設定 (backend/.env)
|
||||
```env
|
||||
# 資料庫連線
|
||||
DATABASE_URL=mysql+pymysql://username:password@host:port/database
|
||||
MYSQL_HOST=mysql.example.com
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USER=your_user
|
||||
MYSQL_PASSWORD=your_password
|
||||
MYSQL_DATABASE=your_database
|
||||
|
||||
```
|
||||
TODOLIST/
|
||||
├── backend/ # Flask 後端
|
||||
│ ├── routes/ # API 路由
|
||||
│ ├── models.py # 資料模型
|
||||
│ ├── config.py # 設定檔
|
||||
│ ├── app.py # Flask 應用程式入口
|
||||
│ ├── tasks.py # Celery 背景任務
|
||||
│ └── utils/ # 工具函數
|
||||
├── frontend/ # Next.js 前端
|
||||
│ ├── src/
|
||||
│ │ ├── app/ # Next.js 應用程式路由
|
||||
│ │ ├── components/ # React 組件
|
||||
│ │ ├── store/ # Redux 狀態管理
|
||||
│ │ ├── lib/ # API 客戶端
|
||||
│ │ └── types/ # TypeScript 類型定義
|
||||
├── mysql/
|
||||
│ └── init/ # 資料庫初始化 SQL
|
||||
├── nginx/ # Nginx 設定
|
||||
├── docker-compose.yml # Docker 編排設定
|
||||
└── PRD.md # 產品需求文件
|
||||
# JWT 設定
|
||||
JWT_SECRET_KEY=your-super-secret-key
|
||||
JWT_ACCESS_TOKEN_EXPIRES=86400
|
||||
|
||||
# LDAP 設定
|
||||
LDAP_SERVER=your-ldap-server.com
|
||||
LDAP_PORT=389
|
||||
LDAP_BIND_USER_DN=CN=ServiceAccount,CN=Users,DC=example,DC=com
|
||||
LDAP_BIND_USER_PASSWORD=service_password
|
||||
LDAP_SEARCH_BASE=OU=Users,DC=example,DC=com
|
||||
|
||||
# SMTP 設定
|
||||
SMTP_SERVER=smtp.example.com
|
||||
SMTP_PORT=25
|
||||
SMTP_SENDER_EMAIL=noreply@example.com
|
||||
|
||||
# Redis 設定
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
```
|
||||
|
||||
## 🔧 開發指令
|
||||
#### 前端設定 (frontend/.env.local)
|
||||
```env
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:5000/api
|
||||
NEXT_PUBLIC_APP_NAME=PANJIT To-Do System
|
||||
```
|
||||
|
||||
### 後端開發
|
||||
## 📝 部署指南
|
||||
|
||||
### 生產環境部署
|
||||
|
||||
1. **資料庫準備**
|
||||
- 建立 MySQL 資料庫
|
||||
- 執行資料庫遷移腳本
|
||||
- 設定資料庫備份策略
|
||||
|
||||
2. **應用程式部署**
|
||||
- 設定 IIS 或 Apache 反向代理
|
||||
- 配置 SSL 證書
|
||||
- 設定環境變數
|
||||
|
||||
3. **背景服務設定**
|
||||
- 配置 Celery Windows Service
|
||||
- 設定 Redis 服務自動啟動
|
||||
- 配置日誌輪轉
|
||||
|
||||
## 🔐 權限矩陣
|
||||
|
||||
### 待辦事項權限控制
|
||||
|
||||
| 操作/角色 | 建立者 | 負責人 | 追蹤者 | 其他使用者 |
|
||||
|----------|--------|--------|--------|------------|
|
||||
| **查看(非公開)** | ✅ | ✅ | ❌ | ❌ |
|
||||
| **查看(公開)** | ✅ | ✅ | ✅ | ✅ |
|
||||
| **編輯** | ✅ | ❌ | ❌ | ❌ |
|
||||
| **刪除** | ✅ | ❌ | ❌ | ❌ |
|
||||
| **更改狀態** | ✅ | ❌ | ❌ | ❌ |
|
||||
| **指派負責人** | ✅ | ❌ | ❌ | ❌ |
|
||||
| **設為公開/私人** | ✅ | ❌ | ❌ | ❌ |
|
||||
| **追蹤(公開)** | ✅ | ✅ | ✅ | ✅ |
|
||||
| **追蹤(非公開)** | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
### 權限說明
|
||||
|
||||
#### 可見性規則
|
||||
- **公開待辦事項**:所有使用者皆可查看
|
||||
- **非公開待辦事項**:僅建立者和負責人可查看
|
||||
- 追蹤者只能存在於公開的待辦事項
|
||||
|
||||
#### 編輯權限
|
||||
- **完全控制**:僅建立者擁有編輯、刪除、狀態變更等所有權限
|
||||
- **唯讀權限**:負責人、追蹤者及其他使用者僅能查看,無法編輯
|
||||
|
||||
#### 追蹤功能
|
||||
- 只有公開的待辦事項才能被追蹤
|
||||
- 任何人都可以追蹤公開的待辦事項
|
||||
- 非公開的待辦事項不支援追蹤功能
|
||||
|
||||
## 🧪 測試
|
||||
|
||||
### 單元測試
|
||||
```bash
|
||||
# 後端測試
|
||||
cd backend
|
||||
pytest tests/ -v
|
||||
|
||||
# 啟動開發服務器 (自動重載)
|
||||
flask run --debug
|
||||
|
||||
# 資料庫遷移
|
||||
flask db upgrade
|
||||
|
||||
# 執行 Python 腳本
|
||||
python -m scripts.init_admin_user
|
||||
```
|
||||
|
||||
### 前端開發
|
||||
```bash
|
||||
# 前端測試
|
||||
cd frontend
|
||||
|
||||
# 開發模式
|
||||
npm run dev
|
||||
|
||||
# 類型檢查
|
||||
npm run type-check
|
||||
|
||||
# 程式碼檢查
|
||||
npm run lint
|
||||
|
||||
# 建置生產版本
|
||||
npm run build
|
||||
|
||||
# 啟動生產服務器
|
||||
npm start
|
||||
npm run test
|
||||
```
|
||||
|
||||
## 📊 功能說明
|
||||
|
||||
### 1. 使用者登入
|
||||
- 使用 AD/LDAP 帳號登入
|
||||
- 首次登入自動建立使用者偏好設定
|
||||
- JWT Token 驗證機制
|
||||
|
||||
### 2. 待辦管理
|
||||
- 建立/編輯/刪除待辦事項
|
||||
- 支援多負責人與多追蹤者
|
||||
- 狀態管理:NEW/DOING/BLOCKED/DONE
|
||||
- 優先級:LOW/MEDIUM/HIGH/URGENT
|
||||
- 到期日設定與星號標記
|
||||
|
||||
### 3. 通知系統
|
||||
- **Fire 一鍵提醒**:立即發送郵件,2分鐘冷卻,每日20封限制
|
||||
- **排程提醒**:到期前3天、當天、逾期1天自動提醒
|
||||
- **週摘要**:每週一早上9點發送個人待辦摘要
|
||||
|
||||
### 4. Excel 匯入
|
||||
- 下載正式模板檔案
|
||||
- 逐列錯誤驗證與報告
|
||||
- AD 帳號存在性檢查
|
||||
- 重複項目檢測
|
||||
|
||||
### 5. 權限控制
|
||||
- **建立者**:完整編輯權限
|
||||
- **負責人**:完整編輯權限
|
||||
- **追蹤者**:僅檢視權限,可接收通知
|
||||
|
||||
## 🛡️ 安全性
|
||||
|
||||
- JWT Token 身份驗證
|
||||
- CORS 跨域保護
|
||||
- SQL Injection 防護 (SQLAlchemy)
|
||||
- XSS 防護 (React)
|
||||
- LDAP 注入防護
|
||||
- API Rate Limiting (建議生產環境啟用)
|
||||
|
||||
## 📝 API 文檔
|
||||
|
||||
### 主要 API 端點
|
||||
|
||||
- `POST /api/auth/login` - 使用者登入
|
||||
- `GET /api/todos` - 取得待辦清單
|
||||
- `POST /api/todos` - 建立待辦事項
|
||||
- `PATCH /api/todos/{id}` - 更新待辦事項
|
||||
- `DELETE /api/todos/{id}` - 刪除待辦事項
|
||||
- `POST /api/todos/{id}/fire-email` - Fire 一鍵提醒
|
||||
- `GET /api/imports/template` - 下載 Excel 模板
|
||||
- `POST /api/imports` - 上傳 Excel 檔案
|
||||
|
||||
完整 API 文檔請查看:http://localhost:5000/api/docs
|
||||
|
||||
## 🚀 生產部署
|
||||
|
||||
### 使用 Docker Compose
|
||||
|
||||
### 類型檢查
|
||||
```bash
|
||||
# 建置並啟動所有服務
|
||||
docker-compose up -d
|
||||
|
||||
# 檢查服務狀態
|
||||
docker-compose ps
|
||||
|
||||
# 查看日誌
|
||||
docker-compose logs -f backend frontend
|
||||
# 前端類型檢查
|
||||
cd frontend
|
||||
npm run type-check
|
||||
```
|
||||
|
||||
### 環境變數設定
|
||||
### 代碼規範檢查
|
||||
```bash
|
||||
# 前端 ESLint 檢查
|
||||
cd frontend
|
||||
npm run lint
|
||||
```
|
||||
|
||||
生產環境請務必修改:
|
||||
- `SECRET_KEY` - Flask 密鑰
|
||||
- `JWT_SECRET_KEY` - JWT 密鑰
|
||||
- 資料庫密碼
|
||||
- SMTP 設定
|
||||
- LDAP 設定
|
||||
## 📚 API 文檔
|
||||
|
||||
## 🔍 故障排除
|
||||
主要 API 端點:
|
||||
|
||||
### 認證相關
|
||||
- `POST /api/auth/login` - 使用者登入
|
||||
- `POST /api/auth/logout` - 使用者登出
|
||||
- `GET /api/auth/me` - 取得當前使用者資訊
|
||||
|
||||
### 待辦事項
|
||||
- `GET /api/todos` - 取得待辦清單
|
||||
- `POST /api/todos` - 建立新待辦事項
|
||||
- `PUT /api/todos/{id}` - 更新待辦事項
|
||||
- `DELETE /api/todos/{id}` - 刪除待辦事項
|
||||
|
||||
### Excel 功能
|
||||
- `POST /api/excel/import` - Excel 匯入
|
||||
- `GET /api/excel/export` - Excel 匯出
|
||||
- `GET /api/excel/template` - 下載匯入模板
|
||||
|
||||
## 🤝 開發貢獻
|
||||
|
||||
### 代碼規範
|
||||
- 使用 TypeScript 進行前端開發
|
||||
- 遵循 ESLint 和 Prettier 設定
|
||||
- 後端使用 Python Type Hints
|
||||
- 提交前執行測試
|
||||
|
||||
### Git 工作流程
|
||||
1. 建立功能分支
|
||||
2. 開發並測試功能
|
||||
3. 提交 Pull Request
|
||||
4. 代碼審查
|
||||
5. 合併到主分支
|
||||
|
||||
## 🐛 問題排解
|
||||
|
||||
### 常見問題
|
||||
|
||||
1. **資料庫連線失敗**
|
||||
```bash
|
||||
# 檢查 MySQL 是否啟動
|
||||
docker-compose ps mysql
|
||||
|
||||
# 檢查連線設定
|
||||
mysql -h localhost -u todouser -p todo_system
|
||||
```
|
||||
**Q: 登入失敗,顯示 LDAP 連線錯誤**
|
||||
A: 檢查 LDAP 設定和網路連線,確認服務帳號權限
|
||||
|
||||
2. **LDAP 連線失敗**
|
||||
- 檢查 LDAP 服務器設定
|
||||
- 確認網路連線
|
||||
- 驗證搜尋基底 DN 設定
|
||||
**Q: 郵件通知無法發送**
|
||||
A: 驗證 SMTP 設定,檢查防火牆和郵件伺服器設定
|
||||
|
||||
3. **郵件發送失敗**
|
||||
- 檢查 SMTP 服務器設定
|
||||
- 確認防火牆設定
|
||||
- 驗證寄件者郵箱權限
|
||||
**Q: 前端無法連接後端 API**
|
||||
A: 確認 CORS 設定和 API 基礎 URL 配置
|
||||
|
||||
4. **前端無法連接後端**
|
||||
- 檢查 `NEXT_PUBLIC_API_URL` 設定
|
||||
- 確認 CORS 設定
|
||||
- 檢查後端服務是否正常啟動
|
||||
**Q: Excel 匯入失敗**
|
||||
A: 檢查文件格式和欄位映射,參考匯入模板
|
||||
|
||||
### 日誌檢查
|
||||
|
||||
```bash
|
||||
# 後端日誌
|
||||
tail -f backend/logs/app.log
|
||||
|
||||
# Docker 日誌
|
||||
docker-compose logs -f backend
|
||||
docker-compose logs -f frontend
|
||||
|
||||
# Celery 任務日誌
|
||||
docker-compose logs -f celery-worker
|
||||
```
|
||||
|
||||
## 👥 開發團隊
|
||||
|
||||
- **產品設計**: PANJIT IT Team
|
||||
- **後端開發**: Flask + SQLAlchemy
|
||||
- **前端開發**: Next.js + TypeScript
|
||||
- **系統架構**: Docker + Nginx
|
||||
|
||||
## 📄 授權
|
||||
|
||||
本專案為 PANJIT 內部使用,請勿外傳。
|
||||
|
||||
---
|
||||
|
||||
## 🔖 版本資訊
|
||||
|
||||
- **版本**: V1.0
|
||||
- **更新日期**: 2025-08-28
|
||||
- **Python**: 3.11+
|
||||
- **Node.js**: 18.0+
|
||||
- **資料庫**: MySQL 8.0
|
||||
|
||||
如有問題請聯繫 IT 部門或查看相關文檔。
|
@@ -1,141 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Create sample todo data for testing"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from dotenv import load_dotenv
|
||||
import pymysql
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
def create_sample_todos():
|
||||
"""Create sample todo items for testing"""
|
||||
print("=" * 60)
|
||||
print("Creating Sample Todo Data")
|
||||
print("=" * 60)
|
||||
|
||||
db_config = {
|
||||
'host': os.getenv('MYSQL_HOST', 'mysql.theaken.com'),
|
||||
'port': int(os.getenv('MYSQL_PORT', 33306)),
|
||||
'user': os.getenv('MYSQL_USER', 'A060'),
|
||||
'password': os.getenv('MYSQL_PASSWORD', 'WLeSCi0yhtc7'),
|
||||
'database': os.getenv('MYSQL_DATABASE', 'db_A060'),
|
||||
'charset': 'utf8mb4'
|
||||
}
|
||||
|
||||
try:
|
||||
connection = pymysql.connect(**db_config)
|
||||
cursor = connection.cursor()
|
||||
|
||||
# Sample todos data
|
||||
sample_todos = [
|
||||
{
|
||||
'title': '完成網站改版設計稿',
|
||||
'description': '設計新版網站的主要頁面布局,包含首頁、產品頁面和聯絡頁面',
|
||||
'status': 'DOING',
|
||||
'priority': 'HIGH',
|
||||
'due_date': (datetime.now() + timedelta(days=7)).date(),
|
||||
'creator_ad': '92367',
|
||||
'creator_display_name': 'ymirliu 陸一銘',
|
||||
'creator_email': 'ymirliu@panjit.com.tw',
|
||||
'starred': True
|
||||
},
|
||||
{
|
||||
'title': '資料庫效能優化',
|
||||
'description': '優化主要查詢語句,提升系統響應速度',
|
||||
'status': 'NEW',
|
||||
'priority': 'URGENT',
|
||||
'due_date': (datetime.now() + timedelta(days=3)).date(),
|
||||
'creator_ad': '92367',
|
||||
'creator_display_name': 'ymirliu 陸一銘',
|
||||
'creator_email': 'ymirliu@panjit.com.tw',
|
||||
'starred': False
|
||||
},
|
||||
{
|
||||
'title': 'API 文檔更新',
|
||||
'description': '更新所有 API 介面文檔,補充新增的端點說明',
|
||||
'status': 'DOING',
|
||||
'priority': 'MEDIUM',
|
||||
'due_date': (datetime.now() + timedelta(days=10)).date(),
|
||||
'creator_ad': 'test',
|
||||
'creator_display_name': '測試使用者',
|
||||
'creator_email': 'test@panjit.com.tw',
|
||||
'starred': False
|
||||
},
|
||||
{
|
||||
'title': '使用者測試回饋整理',
|
||||
'description': '整理上週使用者測試的所有回饋意見,並分類處理',
|
||||
'status': 'BLOCKED',
|
||||
'priority': 'LOW',
|
||||
'due_date': (datetime.now() + timedelta(days=15)).date(),
|
||||
'creator_ad': 'test',
|
||||
'creator_display_name': '測試使用者',
|
||||
'creator_email': 'test@panjit.com.tw',
|
||||
'starred': True
|
||||
},
|
||||
{
|
||||
'title': '系統安全性檢查',
|
||||
'description': '對系統進行全面的安全性檢查,確保沒有漏洞',
|
||||
'status': 'NEW',
|
||||
'priority': 'URGENT',
|
||||
'due_date': (datetime.now() + timedelta(days=2)).date(),
|
||||
'creator_ad': '92367',
|
||||
'creator_display_name': 'ymirliu 陸一銘',
|
||||
'creator_email': 'ymirliu@panjit.com.tw',
|
||||
'starred': False
|
||||
}
|
||||
]
|
||||
|
||||
created_count = 0
|
||||
|
||||
for todo in sample_todos:
|
||||
todo_id = str(uuid.uuid4())
|
||||
|
||||
sql = """
|
||||
INSERT INTO todo_item
|
||||
(id, title, description, status, priority, due_date, created_at, creator_ad, creator_display_name, creator_email, starred)
|
||||
VALUES
|
||||
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
|
||||
values = (
|
||||
todo_id,
|
||||
todo['title'],
|
||||
todo['description'],
|
||||
todo['status'],
|
||||
todo['priority'],
|
||||
todo['due_date'],
|
||||
datetime.now(),
|
||||
todo['creator_ad'],
|
||||
todo['creator_display_name'],
|
||||
todo['creator_email'],
|
||||
todo['starred']
|
||||
)
|
||||
|
||||
cursor.execute(sql, values)
|
||||
created_count += 1
|
||||
print(f"[OK] Created todo: {todo['title']} (ID: {todo_id[:8]}...)")
|
||||
|
||||
connection.commit()
|
||||
|
||||
print(f"\n[SUCCESS] Created {created_count} sample todos successfully!")
|
||||
|
||||
# Show summary
|
||||
cursor.execute("SELECT COUNT(*) FROM todo_item")
|
||||
total_count = cursor.fetchone()[0]
|
||||
print(f"[INFO] Total todos in database: {total_count}")
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to create sample data: {str(e)}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_sample_todos()
|
@@ -1,98 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Create test todos directly in database for testing public/private functionality"""
|
||||
|
||||
import pymysql
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
def create_test_todos():
|
||||
"""Create test todos directly in database"""
|
||||
try:
|
||||
# Connect to database
|
||||
conn = pymysql.connect(
|
||||
host=os.getenv('MYSQL_HOST', 'mysql.theaken.com'),
|
||||
port=int(os.getenv('MYSQL_PORT', 33306)),
|
||||
user=os.getenv('MYSQL_USER', 'A060'),
|
||||
password=os.getenv('MYSQL_PASSWORD', 'WLeSCi0yhtc7'),
|
||||
database=os.getenv('MYSQL_DATABASE', 'db_A060')
|
||||
)
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Test data
|
||||
todos = [
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'title': '公開測試任務 - ymirliu 建立',
|
||||
'description': '這是一個公開任務,其他人可以看到並追蹤',
|
||||
'status': 'NEW',
|
||||
'priority': 'MEDIUM',
|
||||
'created_at': datetime.utcnow(),
|
||||
'creator_ad': '92367',
|
||||
'creator_display_name': 'ymirliu 劉念蒨',
|
||||
'creator_email': 'ymirliu@panjit.com.tw',
|
||||
'starred': False,
|
||||
'is_public': True,
|
||||
'tags': json.dumps(['測試', '公開功能'])
|
||||
},
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'title': '私人測試任務 - ymirliu 建立',
|
||||
'description': '這是一個私人任務,只有建立者可見',
|
||||
'status': 'NEW',
|
||||
'priority': 'HIGH',
|
||||
'created_at': datetime.utcnow(),
|
||||
'creator_ad': '92367',
|
||||
'creator_display_name': 'ymirliu 劉念蒨',
|
||||
'creator_email': 'ymirliu@panjit.com.tw',
|
||||
'starred': True,
|
||||
'is_public': False,
|
||||
'tags': json.dumps(['測試', '私人功能'])
|
||||
}
|
||||
]
|
||||
|
||||
# Insert todos
|
||||
for todo in todos:
|
||||
sql = """INSERT INTO todo_item
|
||||
(id, title, description, status, priority, created_at, creator_ad,
|
||||
creator_display_name, creator_email, starred, is_public, tags)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"""
|
||||
|
||||
cursor.execute(sql, (
|
||||
todo['id'], todo['title'], todo['description'], todo['status'],
|
||||
todo['priority'], todo['created_at'], todo['creator_ad'],
|
||||
todo['creator_display_name'], todo['creator_email'],
|
||||
todo['starred'], todo['is_public'], todo['tags']
|
||||
))
|
||||
|
||||
print(f"✓ 建立 {'公開' if todo['is_public'] else '私人'} Todo: {todo['title']}")
|
||||
|
||||
# Commit changes
|
||||
conn.commit()
|
||||
print(f"\n✅ 成功建立 {len(todos)} 個測試 Todo")
|
||||
|
||||
# Verify the insertion
|
||||
cursor.execute("SELECT id, title, is_public FROM todo_item WHERE creator_ad = '92367'")
|
||||
results = cursor.fetchall()
|
||||
print(f"\n📊 資料庫中的測試數據:")
|
||||
for result in results:
|
||||
print(f" - {result[1]} ({'公開' if result[2] else '私人'})")
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 建立測試數據失敗: {str(e)}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_test_todos()
|
@@ -1,90 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Debug LDAP search to find the correct format"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from dotenv import load_dotenv
|
||||
from ldap3 import Server, Connection, SUBTREE, ALL_ATTRIBUTES
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
def debug_ldap():
|
||||
"""Debug LDAP search"""
|
||||
print("=" * 60)
|
||||
print("Debug LDAP Search")
|
||||
print("=" * 60)
|
||||
|
||||
# Get LDAP configuration
|
||||
ldap_server = os.getenv('LDAP_SERVER', 'ldap://panjit.com.tw')
|
||||
ldap_port = int(os.getenv('LDAP_PORT', 389))
|
||||
ldap_bind_user = os.getenv('LDAP_BIND_USER_DN', '')
|
||||
ldap_bind_password = os.getenv('LDAP_BIND_USER_PASSWORD', '')
|
||||
ldap_search_base = os.getenv('LDAP_SEARCH_BASE', 'DC=panjit,DC=com,DC=tw')
|
||||
|
||||
print(f"LDAP Server: {ldap_server}")
|
||||
print(f"LDAP Port: {ldap_port}")
|
||||
print(f"Search Base: {ldap_search_base}")
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
# Create server object
|
||||
server = Server(
|
||||
ldap_server,
|
||||
port=ldap_port,
|
||||
use_ssl=False,
|
||||
get_info=ALL_ATTRIBUTES
|
||||
)
|
||||
|
||||
# Create connection with bind user
|
||||
conn = Connection(
|
||||
server,
|
||||
user=ldap_bind_user,
|
||||
password=ldap_bind_password,
|
||||
auto_bind=True,
|
||||
raise_exceptions=True
|
||||
)
|
||||
|
||||
print("[OK] Successfully connected to LDAP server")
|
||||
|
||||
# Test different search filters
|
||||
test_searches = [
|
||||
"(&(objectClass=person)(sAMAccountName=ymirliu))",
|
||||
"(&(objectClass=person)(userPrincipalName=ymirliu@panjit.com.tw))",
|
||||
"(&(objectClass=person)(mail=ymirliu@panjit.com.tw))",
|
||||
"(&(objectClass=person)(cn=*ymirliu*))",
|
||||
"(&(objectClass=person)(displayName=*ymirliu*))",
|
||||
]
|
||||
|
||||
for i, search_filter in enumerate(test_searches, 1):
|
||||
print(f"\n[{i}] Testing filter: {search_filter}")
|
||||
|
||||
conn.search(
|
||||
ldap_search_base,
|
||||
search_filter,
|
||||
SUBTREE,
|
||||
attributes=['sAMAccountName', 'displayName', 'mail', 'userPrincipalName', 'cn']
|
||||
)
|
||||
|
||||
if conn.entries:
|
||||
print(f" Found {len(conn.entries)} entries:")
|
||||
for entry in conn.entries:
|
||||
print(f" sAMAccountName: {entry.sAMAccountName}")
|
||||
print(f" userPrincipalName: {entry.userPrincipalName}")
|
||||
print(f" displayName: {entry.displayName}")
|
||||
print(f" mail: {entry.mail}")
|
||||
print(f" cn: {entry.cn}")
|
||||
print()
|
||||
else:
|
||||
print(" No entries found")
|
||||
|
||||
conn.unbind()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] LDAP connection failed: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
debug_ldap()
|
@@ -85,19 +85,19 @@ class TodoItem(db.Model):
|
||||
|
||||
def can_edit(self, user_ad):
|
||||
"""Check if user can edit this todo"""
|
||||
if self.creator_ad == user_ad:
|
||||
return True
|
||||
return any(r.ad_account == user_ad for r in self.responsible_users)
|
||||
# Only creator can edit
|
||||
return self.creator_ad == user_ad
|
||||
|
||||
def can_view(self, user_ad):
|
||||
"""Check if user can view this todo"""
|
||||
# Public todos can be viewed by anyone
|
||||
if self.is_public:
|
||||
return True
|
||||
# Private todos can be viewed by creator, responsible users, and followers
|
||||
if self.can_edit(user_ad):
|
||||
# Private todos can be viewed by creator and responsible users only
|
||||
if self.creator_ad == user_ad:
|
||||
return True
|
||||
return any(f.ad_account == user_ad for f in self.followers)
|
||||
# Check if user is a responsible user
|
||||
return any(r.ad_account == user_ad for r in self.responsible_users)
|
||||
|
||||
def can_follow(self, user_ad):
|
||||
"""Check if user can follow this todo"""
|
||||
@@ -154,7 +154,8 @@ class TodoAuditLog(db.Model):
|
||||
actor_ad = db.Column(db.String(128), nullable=False)
|
||||
todo_id = db.Column(CHAR(36), db.ForeignKey('todo_item.id', ondelete='SET NULL'))
|
||||
action = db.Column(ENUM('CREATE', 'UPDATE', 'DELETE', 'COMPLETE', 'IMPORT',
|
||||
'MAIL_SENT', 'MAIL_FAIL', 'FIRE_EMAIL', 'DIGEST_EMAIL', 'BULK_REMINDER'), nullable=False)
|
||||
'MAIL_SENT', 'MAIL_FAIL', 'FIRE_EMAIL', 'DIGEST_EMAIL', 'BULK_REMINDER',
|
||||
'FOLLOW', 'UNFOLLOW'), nullable=False)
|
||||
detail = db.Column(JSON)
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
|
@@ -142,11 +142,9 @@ def upload_excel():
|
||||
if responsible_str and responsible_str != 'nan':
|
||||
responsible_users = [user.strip() for user in responsible_str.replace(',', ';').split(';') if user.strip()]
|
||||
|
||||
# 追蹤人
|
||||
followers_str = str(row.get('追蹤人', row.get('followers', ''))).strip()
|
||||
followers = []
|
||||
if followers_str and followers_str != 'nan':
|
||||
followers = [user.strip() for user in followers_str.replace(',', ';').split(';') if user.strip()]
|
||||
# 公開設定
|
||||
is_public_str = str(row.get('公開設定', row.get('is_public', ''))).strip().lower()
|
||||
is_public = is_public_str in ['是', 'yes', 'true', '1', 'y'] if is_public_str and is_public_str != 'nan' else False
|
||||
|
||||
todos_data.append({
|
||||
'row': idx + 2,
|
||||
@@ -156,7 +154,8 @@ def upload_excel():
|
||||
'priority': priority,
|
||||
'due_date': due_date.isoformat() if due_date else None,
|
||||
'responsible_users': responsible_users,
|
||||
'followers': followers
|
||||
'followers': [], # Excel模板中沒有followers欄位,初始化為空陣列
|
||||
'is_public': is_public
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
@@ -200,9 +199,8 @@ def import_todos():
|
||||
|
||||
for todo_data in todos_data:
|
||||
try:
|
||||
# 驗證負責人和追蹤人的 AD 帳號
|
||||
# 驗證負責人的 AD 帳號
|
||||
responsible_users = todo_data.get('responsible_users', [])
|
||||
followers = todo_data.get('followers', [])
|
||||
|
||||
if responsible_users:
|
||||
valid_responsible = validate_ad_accounts(responsible_users)
|
||||
@@ -214,21 +212,17 @@ def import_todos():
|
||||
})
|
||||
continue
|
||||
|
||||
if followers:
|
||||
valid_followers = validate_ad_accounts(followers)
|
||||
invalid_followers = set(followers) - set(valid_followers.keys())
|
||||
if invalid_followers:
|
||||
errors.append({
|
||||
'row': todo_data.get('row', '?'),
|
||||
'error': f'無效的追蹤人帳號: {", ".join(invalid_followers)}'
|
||||
})
|
||||
continue
|
||||
|
||||
# 建立待辦事項
|
||||
due_date = None
|
||||
if todo_data.get('due_date'):
|
||||
due_date = datetime.strptime(todo_data['due_date'], '%Y-%m-%d').date()
|
||||
|
||||
# 處理公開設定
|
||||
is_public = False # 預設為非公開
|
||||
if todo_data.get('is_public'):
|
||||
is_public_str = str(todo_data['is_public']).strip().lower()
|
||||
is_public = is_public_str in ['是', 'yes', 'true', '1', 'y']
|
||||
|
||||
todo = TodoItem(
|
||||
id=str(uuid.uuid4()),
|
||||
title=todo_data['title'],
|
||||
@@ -239,29 +233,24 @@ def import_todos():
|
||||
creator_ad=identity,
|
||||
creator_display_name=claims.get('display_name', identity),
|
||||
creator_email=claims.get('email', ''),
|
||||
starred=False
|
||||
starred=False,
|
||||
is_public=is_public
|
||||
)
|
||||
db.session.add(todo)
|
||||
|
||||
# 新增負責人
|
||||
if responsible_users:
|
||||
for account in responsible_users:
|
||||
# 使用驗證後的AD帳號,確保格式統一
|
||||
ad_account = valid_responsible[account]['ad_account']
|
||||
responsible = TodoItemResponsible(
|
||||
todo_id=todo.id,
|
||||
ad_account=account,
|
||||
ad_account=ad_account,
|
||||
added_by=identity
|
||||
)
|
||||
db.session.add(responsible)
|
||||
|
||||
# 新增追蹤人
|
||||
if followers:
|
||||
for account in followers:
|
||||
follower = TodoItemFollower(
|
||||
todo_id=todo.id,
|
||||
ad_account=account,
|
||||
added_by=identity
|
||||
)
|
||||
db.session.add(follower)
|
||||
# 因為匯入的待辦事項預設為非公開,所以不支援追蹤人功能
|
||||
|
||||
# 新增稽核記錄
|
||||
audit = TodoAuditLog(
|
||||
@@ -447,7 +436,7 @@ def download_template():
|
||||
'優先級': ['高', '中'],
|
||||
'到期日': ['2025-12-31', '2026-01-15'],
|
||||
'負責人': ['user1@panjit.com.tw', 'user2@panjit.com.tw'],
|
||||
'追蹤人': ['user3@panjit.com.tw;user4@panjit.com.tw', 'user5@panjit.com.tw']
|
||||
'公開設定': ['否', '是']
|
||||
}
|
||||
|
||||
# 說明資料
|
||||
@@ -459,7 +448,7 @@ def download_template():
|
||||
'優先級: 緊急/高/中/低',
|
||||
'到期日: YYYY-MM-DD 格式',
|
||||
'負責人: AD帳號,多人用分號分隔',
|
||||
'追蹤人: AD帳號,多人用分號分隔'
|
||||
'公開設定: 是/否,決定其他人是否能看到此任務'
|
||||
],
|
||||
'說明': [
|
||||
'請填入待辦事項的標題',
|
||||
@@ -468,7 +457,7 @@ def download_template():
|
||||
'可選填 URGENT/HIGH/MEDIUM/LOW',
|
||||
'例如: 2024-12-31',
|
||||
'例如: john@panjit.com.tw',
|
||||
'例如: mary@panjit.com.tw;tom@panjit.com.tw'
|
||||
'是=公開任務,否=只有建立者和負責人能看到'
|
||||
]
|
||||
}
|
||||
|
||||
|
@@ -886,14 +886,7 @@ def follow_todo(todo_id):
|
||||
)
|
||||
db.session.add(follower)
|
||||
|
||||
# Log audit
|
||||
audit = TodoAuditLog(
|
||||
actor_ad=identity,
|
||||
todo_id=todo_id,
|
||||
action='FOLLOW',
|
||||
detail={'follower': identity}
|
||||
)
|
||||
db.session.add(audit)
|
||||
# Note: Skip audit log for FOLLOW action until ENUM is updated
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"User {identity} followed todo {todo_id}")
|
||||
@@ -924,14 +917,7 @@ def unfollow_todo(todo_id):
|
||||
# Remove follower
|
||||
db.session.delete(follower)
|
||||
|
||||
# Log audit
|
||||
audit = TodoAuditLog(
|
||||
actor_ad=identity,
|
||||
todo_id=todo_id,
|
||||
action='UNFOLLOW',
|
||||
detail={'follower': identity}
|
||||
)
|
||||
db.session.add(audit)
|
||||
# Note: Skip audit log for UNFOLLOW action until ENUM is updated
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"User {identity} unfollowed todo {todo_id}")
|
||||
|
@@ -1,181 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test database connection and check data"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from dotenv import load_dotenv
|
||||
import pymysql
|
||||
from datetime import datetime
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
def test_db_connection():
|
||||
"""Test database connection and list tables"""
|
||||
print("=" * 60)
|
||||
print("Testing Database Connection")
|
||||
print("=" * 60)
|
||||
|
||||
# Get database configuration
|
||||
db_config = {
|
||||
'host': os.getenv('MYSQL_HOST', 'mysql.theaken.com'),
|
||||
'port': int(os.getenv('MYSQL_PORT', 33306)),
|
||||
'user': os.getenv('MYSQL_USER', 'A060'),
|
||||
'password': os.getenv('MYSQL_PASSWORD', 'WLeSCi0yhtc7'),
|
||||
'database': os.getenv('MYSQL_DATABASE', 'db_A060'),
|
||||
'charset': 'utf8mb4'
|
||||
}
|
||||
|
||||
print(f"Host: {db_config['host']}")
|
||||
print(f"Port: {db_config['port']}")
|
||||
print(f"Database: {db_config['database']}")
|
||||
print(f"User: {db_config['user']}")
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
# Connect to database
|
||||
connection = pymysql.connect(**db_config)
|
||||
cursor = connection.cursor()
|
||||
|
||||
print("[OK] Successfully connected to database")
|
||||
|
||||
# List all tables
|
||||
print("\n[1] Listing all todo tables:")
|
||||
cursor.execute("SHOW TABLES LIKE 'todo%'")
|
||||
tables = cursor.fetchall()
|
||||
|
||||
if tables:
|
||||
for table in tables:
|
||||
print(f" - {table[0]}")
|
||||
else:
|
||||
print(" No todo tables found")
|
||||
|
||||
# Check todo_item table
|
||||
print("\n[2] Checking todo_item table:")
|
||||
cursor.execute("SELECT COUNT(*) FROM todo_item")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f" Total records: {count}")
|
||||
|
||||
if count > 0:
|
||||
print("\n Sample data from todo_item:")
|
||||
cursor.execute("""
|
||||
SELECT id, title, status, priority, due_date, creator_ad
|
||||
FROM todo_item
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
items = cursor.fetchall()
|
||||
for item in items:
|
||||
print(f" - {item[0][:8]}... | {item[1][:30]}... | {item[2]} | {item[5]}")
|
||||
|
||||
# Check todo_user_pref table
|
||||
print("\n[3] Checking todo_user_pref table:")
|
||||
cursor.execute("SELECT COUNT(*) FROM todo_user_pref")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f" Total users: {count}")
|
||||
|
||||
if count > 0:
|
||||
print("\n Sample users:")
|
||||
cursor.execute("""
|
||||
SELECT ad_account, display_name, email
|
||||
FROM todo_user_pref
|
||||
LIMIT 5
|
||||
""")
|
||||
users = cursor.fetchall()
|
||||
for user in users:
|
||||
print(f" - {user[0]} | {user[1]} | {user[2]}")
|
||||
|
||||
# Check todo_item_responsible table
|
||||
print("\n[4] Checking todo_item_responsible table:")
|
||||
cursor.execute("SELECT COUNT(*) FROM todo_item_responsible")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f" Total assignments: {count}")
|
||||
|
||||
# Check todo_item_follower table
|
||||
print("\n[5] Checking todo_item_follower table:")
|
||||
cursor.execute("SELECT COUNT(*) FROM todo_item_follower")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f" Total followers: {count}")
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("[OK] Database connection test successful!")
|
||||
print("=" * 60)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n[ERROR] Database connection failed: {str(e)}")
|
||||
print(f"Error type: {type(e).__name__}")
|
||||
return False
|
||||
|
||||
def create_sample_todo():
|
||||
"""Create a sample todo item for testing"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Creating Sample Todo Item")
|
||||
print("=" * 60)
|
||||
|
||||
db_config = {
|
||||
'host': os.getenv('MYSQL_HOST', 'mysql.theaken.com'),
|
||||
'port': int(os.getenv('MYSQL_PORT', 33306)),
|
||||
'user': os.getenv('MYSQL_USER', 'A060'),
|
||||
'password': os.getenv('MYSQL_PASSWORD', 'WLeSCi0yhtc7'),
|
||||
'database': os.getenv('MYSQL_DATABASE', 'db_A060'),
|
||||
'charset': 'utf8mb4'
|
||||
}
|
||||
|
||||
try:
|
||||
connection = pymysql.connect(**db_config)
|
||||
cursor = connection.cursor()
|
||||
|
||||
# Generate a UUID
|
||||
import uuid
|
||||
todo_id = str(uuid.uuid4())
|
||||
|
||||
# Insert sample todo
|
||||
sql = """
|
||||
INSERT INTO todo_item
|
||||
(id, title, description, status, priority, due_date, created_at, creator_ad, creator_display_name, creator_email, starred)
|
||||
VALUES
|
||||
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
|
||||
values = (
|
||||
todo_id,
|
||||
'Test Todo Item - ' + datetime.now().strftime('%Y-%m-%d %H:%M'),
|
||||
'This is a test todo item created from Python script',
|
||||
'NEW',
|
||||
'MEDIUM',
|
||||
'2025-09-15',
|
||||
datetime.now(),
|
||||
'test_user',
|
||||
'Test User',
|
||||
'test@panjit.com.tw',
|
||||
False
|
||||
)
|
||||
|
||||
cursor.execute(sql, values)
|
||||
connection.commit()
|
||||
|
||||
print(f"[OK] Created todo item with ID: {todo_id}")
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to create todo: {str(e)}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test database connection
|
||||
if test_db_connection():
|
||||
# Ask if user wants to create sample data
|
||||
response = input("\nDo you want to create a sample todo item? (y/n): ")
|
||||
if response.lower() == 'y':
|
||||
create_sample_todo()
|
||||
else:
|
||||
print("\n[WARNING] Please check your database configuration in .env file")
|
||||
sys.exit(1)
|
@@ -1,165 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test LDAP connection and authentication"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from ldap3 import Server, Connection, SUBTREE, ALL_ATTRIBUTES
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
def test_ldap_connection():
|
||||
"""Test LDAP connection"""
|
||||
print("=" * 50)
|
||||
print("Testing LDAP Connection")
|
||||
print("=" * 50)
|
||||
|
||||
# Get LDAP configuration
|
||||
ldap_server = os.getenv('LDAP_SERVER', 'ldap://panjit.com.tw')
|
||||
ldap_port = int(os.getenv('LDAP_PORT', 389))
|
||||
ldap_bind_user = os.getenv('LDAP_BIND_USER_DN', '')
|
||||
ldap_bind_password = os.getenv('LDAP_BIND_USER_PASSWORD', '')
|
||||
ldap_search_base = os.getenv('LDAP_SEARCH_BASE', 'DC=panjit,DC=com,DC=tw')
|
||||
|
||||
print(f"LDAP Server: {ldap_server}")
|
||||
print(f"LDAP Port: {ldap_port}")
|
||||
print(f"Bind User: {ldap_bind_user}")
|
||||
print(f"Search Base: {ldap_search_base}")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
# Create server object
|
||||
server = Server(
|
||||
ldap_server,
|
||||
port=ldap_port,
|
||||
use_ssl=False,
|
||||
get_info=ALL_ATTRIBUTES
|
||||
)
|
||||
|
||||
print("Creating LDAP connection...")
|
||||
|
||||
# Create connection with bind user
|
||||
conn = Connection(
|
||||
server,
|
||||
user=ldap_bind_user,
|
||||
password=ldap_bind_password,
|
||||
auto_bind=True,
|
||||
raise_exceptions=True
|
||||
)
|
||||
|
||||
print("[OK] Successfully connected to LDAP server")
|
||||
print(f"[OK] Server info: {conn.server}")
|
||||
|
||||
# Test search
|
||||
print("\nTesting LDAP search...")
|
||||
search_filter = "(objectClass=person)"
|
||||
conn.search(
|
||||
ldap_search_base,
|
||||
search_filter,
|
||||
SUBTREE,
|
||||
attributes=['sAMAccountName', 'displayName', 'mail'],
|
||||
size_limit=5
|
||||
)
|
||||
|
||||
print(f"[OK] Found {len(conn.entries)} entries")
|
||||
|
||||
if conn.entries:
|
||||
print("\nSample users:")
|
||||
for i, entry in enumerate(conn.entries[:3], 1):
|
||||
print(f" {i}. {entry.sAMAccountName} - {entry.displayName}")
|
||||
|
||||
conn.unbind()
|
||||
print("\n[OK] LDAP connection test successful!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n[ERROR] LDAP connection failed: {str(e)}")
|
||||
print(f"Error type: {type(e).__name__}")
|
||||
return False
|
||||
|
||||
def test_user_authentication(username, password):
|
||||
"""Test user authentication"""
|
||||
print("\n" + "=" * 50)
|
||||
print(f"Testing authentication for user: {username}")
|
||||
print("=" * 50)
|
||||
|
||||
# Get LDAP configuration
|
||||
ldap_server = os.getenv('LDAP_SERVER', 'ldap://panjit.com.tw')
|
||||
ldap_port = int(os.getenv('LDAP_PORT', 389))
|
||||
ldap_bind_user = os.getenv('LDAP_BIND_USER_DN', '')
|
||||
ldap_bind_password = os.getenv('LDAP_BIND_USER_PASSWORD', '')
|
||||
ldap_search_base = os.getenv('LDAP_SEARCH_BASE', 'DC=panjit,DC=com,DC=tw')
|
||||
ldap_user_attr = os.getenv('LDAP_USER_LOGIN_ATTR', 'userPrincipalName')
|
||||
|
||||
try:
|
||||
# Create server object
|
||||
server = Server(
|
||||
ldap_server,
|
||||
port=ldap_port,
|
||||
use_ssl=False,
|
||||
get_info=ALL_ATTRIBUTES
|
||||
)
|
||||
|
||||
# First, bind with service account to search for user
|
||||
conn = Connection(
|
||||
server,
|
||||
user=ldap_bind_user,
|
||||
password=ldap_bind_password,
|
||||
auto_bind=True,
|
||||
raise_exceptions=True
|
||||
)
|
||||
|
||||
# Search for user
|
||||
search_filter = f"(&(objectClass=person)({ldap_user_attr}={username}))"
|
||||
print(f"Searching with filter: {search_filter}")
|
||||
|
||||
conn.search(
|
||||
ldap_search_base,
|
||||
search_filter,
|
||||
SUBTREE,
|
||||
attributes=['sAMAccountName', 'displayName', 'mail', 'userPrincipalName', 'distinguishedName']
|
||||
)
|
||||
|
||||
if not conn.entries:
|
||||
print(f"[ERROR] User not found: {username}")
|
||||
return False
|
||||
|
||||
user_entry = conn.entries[0]
|
||||
user_dn = user_entry.distinguishedName.value
|
||||
|
||||
print(f"[OK] User found:")
|
||||
print(f" DN: {user_dn}")
|
||||
print(f" sAMAccountName: {user_entry.sAMAccountName}")
|
||||
print(f" displayName: {user_entry.displayName}")
|
||||
print(f" mail: {user_entry.mail}")
|
||||
|
||||
# Try to bind with user credentials
|
||||
print(f"\nAttempting to authenticate user...")
|
||||
user_conn = Connection(
|
||||
server,
|
||||
user=user_dn,
|
||||
password=password,
|
||||
auto_bind=True,
|
||||
raise_exceptions=True
|
||||
)
|
||||
|
||||
print("[OK] Authentication successful!")
|
||||
user_conn.unbind()
|
||||
conn.unbind()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Authentication failed: {str(e)}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test basic connection
|
||||
if test_ldap_connection():
|
||||
# If you want to test user authentication, uncomment and modify:
|
||||
# test_user_authentication("your_username@panjit.com.tw", "your_password")
|
||||
pass
|
||||
else:
|
||||
print("\n[WARNING] Please check your LDAP configuration in .env file")
|
||||
sys.exit(1)
|
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test LDAP authentication with provided credentials"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from dotenv import load_dotenv
|
||||
from app import create_app
|
||||
from utils.ldap_utils import authenticate_user
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
def test_ldap_auth():
|
||||
"""Test LDAP authentication"""
|
||||
print("=" * 60)
|
||||
print("Testing LDAP Authentication")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"LDAP Server: {os.getenv('LDAP_SERVER')}")
|
||||
print(f"Search Base: {os.getenv('LDAP_SEARCH_BASE')}")
|
||||
print(f"Login Attr: {os.getenv('LDAP_USER_LOGIN_ATTR')}")
|
||||
print("-" * 60)
|
||||
|
||||
username = 'ymirliu@panjit.com.tw'
|
||||
password = '3EDC4rfv5tgb'
|
||||
|
||||
print(f"Testing authentication for: {username}")
|
||||
|
||||
# Create Flask app and context
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
try:
|
||||
result = authenticate_user(username, password)
|
||||
if result:
|
||||
print("[SUCCESS] Authentication successful!")
|
||||
print(f"User info: {result}")
|
||||
else:
|
||||
print("[FAILED] Authentication failed")
|
||||
|
||||
return result is not None
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception during authentication: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_ldap_auth()
|
@@ -1,103 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: todo_mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-todo_system}
|
||||
MYSQL_USER: ${MYSQL_USER:-todouser}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-todopass}
|
||||
TZ: Asia/Taipei
|
||||
ports:
|
||||
- "${MYSQL_PORT:-3306}:3306"
|
||||
volumes:
|
||||
- ./mysql/data:/var/lib/mysql
|
||||
- ./mysql/init:/docker-entrypoint-initdb.d
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
networks:
|
||||
- todo_network
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: todo_redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
volumes:
|
||||
- ./redis/data:/data
|
||||
networks:
|
||||
- todo_network
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: todo_backend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- mysql
|
||||
- redis
|
||||
environment:
|
||||
- FLASK_ENV=${FLASK_ENV:-development}
|
||||
- MYSQL_HOST=mysql
|
||||
- MYSQL_PORT=3306
|
||||
- MYSQL_DATABASE=${MYSQL_DATABASE:-todo_system}
|
||||
- MYSQL_USER=${MYSQL_USER:-todouser}
|
||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD:-todopass}
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
ports:
|
||||
- "${BACKEND_PORT:-5000}:5000"
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- ./uploads:/app/uploads
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- todo_network
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
container_name: todo_frontend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-development}
|
||||
- NEXT_PUBLIC_API_URL=${API_URL:-http://localhost:5000}
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-3000}:3000"
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
- /app/.next
|
||||
networks:
|
||||
- todo_network
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: todo_nginx
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
- frontend
|
||||
ports:
|
||||
- "${NGINX_PORT:-80}:80"
|
||||
- "${NGINX_SSL_PORT:-443}:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
networks:
|
||||
- todo_network
|
||||
|
||||
networks:
|
||||
todo_network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
redis_data:
|
@@ -10,93 +10,31 @@ import {
|
||||
FormControlLabel,
|
||||
Button,
|
||||
Divider,
|
||||
Avatar,
|
||||
TextField,
|
||||
IconButton,
|
||||
Chip,
|
||||
Grid,
|
||||
Paper,
|
||||
Alert,
|
||||
Snackbar,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Slider,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Person,
|
||||
Palette,
|
||||
Notifications,
|
||||
Security,
|
||||
Language,
|
||||
Save,
|
||||
Edit,
|
||||
PhotoCamera,
|
||||
DarkMode,
|
||||
LightMode,
|
||||
SettingsBrightness,
|
||||
VolumeUp,
|
||||
Email,
|
||||
Sms,
|
||||
Phone,
|
||||
Schedule,
|
||||
Visibility,
|
||||
Lock,
|
||||
Key,
|
||||
Shield,
|
||||
Refresh,
|
||||
} from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTheme } from '@/providers/ThemeProvider';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import DashboardLayout from '@/components/layout/DashboardLayout';
|
||||
|
||||
const SettingsPage = () => {
|
||||
const { themeMode, setThemeMode, actualTheme } = useTheme();
|
||||
const searchParams = useSearchParams();
|
||||
const { actualTheme } = useTheme();
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(() => {
|
||||
const tabParam = searchParams.get('tab');
|
||||
return tabParam || 'profile';
|
||||
});
|
||||
|
||||
// 用戶設定
|
||||
const [userSettings, setUserSettings] = useState(() => {
|
||||
try {
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr);
|
||||
return {
|
||||
name: user.display_name || user.ad_account || '',
|
||||
email: user.email || '',
|
||||
department: '資訊部',
|
||||
position: '員工',
|
||||
phone: '',
|
||||
bio: '',
|
||||
avatar: (user.display_name || user.ad_account || 'U').charAt(0).toUpperCase(),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse user from localStorage:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
name: '',
|
||||
email: '',
|
||||
department: '資訊部',
|
||||
position: '員工',
|
||||
phone: '',
|
||||
bio: '',
|
||||
avatar: 'U',
|
||||
};
|
||||
});
|
||||
|
||||
// 通知設定
|
||||
// 郵件通知設定
|
||||
const [notificationSettings, setNotificationSettings] = useState({
|
||||
emailNotifications: true,
|
||||
pushNotifications: true,
|
||||
smsNotifications: false,
|
||||
todoReminders: true,
|
||||
deadlineAlerts: true,
|
||||
weeklyReports: true,
|
||||
@@ -104,639 +42,201 @@ const SettingsPage = () => {
|
||||
soundVolume: 70,
|
||||
});
|
||||
|
||||
// 隱私設定
|
||||
const [privacySettings, setPrivacySettings] = useState({
|
||||
profileVisibility: 'team',
|
||||
todoVisibility: 'responsible',
|
||||
showOnlineStatus: true,
|
||||
allowDirectMessages: true,
|
||||
dataSharing: false,
|
||||
});
|
||||
|
||||
// 工作設定
|
||||
const [workSettings, setWorkSettings] = useState({
|
||||
timeZone: 'Asia/Taipei',
|
||||
dateFormat: 'YYYY-MM-DD',
|
||||
timeFormat: '24h',
|
||||
workingHours: {
|
||||
start: '09:00',
|
||||
end: '18:00',
|
||||
},
|
||||
autoRefresh: 30,
|
||||
defaultView: 'list',
|
||||
});
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
console.log('Settings saved:', {
|
||||
user: userSettings,
|
||||
notifications: notificationSettings,
|
||||
privacy: privacySettings,
|
||||
work: workSettings,
|
||||
});
|
||||
console.log('郵件通知設定已儲存:', notificationSettings);
|
||||
setShowSuccess(true);
|
||||
};
|
||||
|
||||
const themeOptions = [
|
||||
{ value: 'light', label: '亮色模式', icon: <LightMode /> },
|
||||
{ value: 'dark', label: '深色模式', icon: <DarkMode /> },
|
||||
{ value: 'system', label: '跟隨系統', icon: <SettingsBrightness /> },
|
||||
];
|
||||
|
||||
const tabs = [
|
||||
{ id: 'profile', label: '個人資料', icon: <Person /> },
|
||||
{ id: 'appearance', label: '外觀主題', icon: <Palette /> },
|
||||
{ id: 'notifications', label: '通知設定', icon: <Notifications /> },
|
||||
{ id: 'privacy', label: '隱私安全', icon: <Security /> },
|
||||
{ id: 'work', label: '工作偏好', icon: <Schedule /> },
|
||||
];
|
||||
|
||||
const renderProfileSettings = () => (
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card sx={{
|
||||
backgroundColor: actualTheme === 'dark' ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<Person sx={{ color: 'primary.main', fontSize: 24 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
個人資料設定
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* 頭像區域 */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 4 }}>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
fontSize: '2rem',
|
||||
fontWeight: 700,
|
||||
background: 'linear-gradient(45deg, #3b82f6 30%, #8b5cf6 90%)',
|
||||
}}
|
||||
>
|
||||
{userSettings.avatar}
|
||||
</Avatar>
|
||||
<IconButton
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: -5,
|
||||
right: -5,
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'white',
|
||||
width: 32,
|
||||
height: 32,
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PhotoCamera sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||||
{userSettings.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{userSettings.position} · {userSettings.department}
|
||||
</Typography>
|
||||
<Chip
|
||||
label="已驗證"
|
||||
color="success"
|
||||
size="small"
|
||||
icon={<Shield sx={{ fontSize: 14 }} />}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="姓名"
|
||||
value={userSettings.name}
|
||||
onChange={(e) => setUserSettings(prev => ({ ...prev, name: e.target.value }))}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="電子信箱"
|
||||
value={userSettings.email}
|
||||
onChange={(e) => setUserSettings(prev => ({ ...prev, email: e.target.value }))}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="部門"
|
||||
value={userSettings.department}
|
||||
onChange={(e) => setUserSettings(prev => ({ ...prev, department: e.target.value }))}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="職位"
|
||||
value={userSettings.position}
|
||||
onChange={(e) => setUserSettings(prev => ({ ...prev, position: e.target.value }))}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="電話號碼"
|
||||
value={userSettings.phone}
|
||||
onChange={(e) => setUserSettings(prev => ({ ...prev, phone: e.target.value }))}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
label="個人簡介"
|
||||
placeholder="簡單介紹一下自己..."
|
||||
value={userSettings.bio}
|
||||
onChange={(e) => setUserSettings(prev => ({ ...prev, bio: e.target.value }))}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
const renderAppearanceSettings = () => (
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card sx={{
|
||||
backgroundColor: actualTheme === 'dark' ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<Palette sx={{ color: 'primary.main', fontSize: 24 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
外觀主題設定
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
選擇您喜歡的介面主題,讓工作更舒適
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{themeOptions.map((option) => (
|
||||
<Grid item xs={12} sm={4} key={option.value}>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Paper
|
||||
onClick={() => setThemeMode(option.value as 'light' | 'dark' | 'auto')}
|
||||
sx={{
|
||||
p: 3,
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center',
|
||||
backgroundColor: themeMode === option.value
|
||||
? (actualTheme === 'dark' ? 'rgba(59, 130, 246, 0.2)' : 'rgba(59, 130, 246, 0.1)')
|
||||
: (actualTheme === 'dark' ? '#374151' : '#f9fafb'),
|
||||
border: themeMode === option.value
|
||||
? `2px solid ${actualTheme === 'dark' ? '#60a5fa' : '#3b82f6'}`
|
||||
: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
borderRadius: 3,
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(59, 130, 246, 0.1)'
|
||||
: 'rgba(59, 130, 246, 0.05)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
mb: 2,
|
||||
color: themeMode === option.value ? 'primary.main' : 'text.secondary',
|
||||
'& svg': {
|
||||
fontSize: 40,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{option.icon}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
color: themeMode === option.value ? 'primary.main' : 'text.primary',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Typography>
|
||||
{themeMode === option.value && (
|
||||
<Chip
|
||||
label="已選擇"
|
||||
color="primary"
|
||||
size="small"
|
||||
sx={{ fontWeight: 600 }}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* 預覽區域 */}
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
|
||||
主題預覽
|
||||
</Typography>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 3,
|
||||
backgroundColor: actualTheme === 'dark' ? '#374151' : '#f8fafc',
|
||||
border: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
待辦事項預覽
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<Chip label="進行中" color="primary" size="small" />
|
||||
<Chip label="高優先級" color="error" size="small" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
2024-01-15 到期
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
這是在 {themeMode === 'light' ? '亮色' : themeMode === 'dark' ? '深色' : '系統'} 模式下的預覽效果
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
const renderNotificationSettings = () => (
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card sx={{
|
||||
backgroundColor: actualTheme === 'dark' ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<Notifications sx={{ color: 'primary.main', fontSize: 24 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
通知設定
|
||||
<Card sx={{
|
||||
backgroundColor: actualTheme === 'dark' ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<Notifications sx={{ color: 'primary.main', fontSize: 24 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
郵件通知設定
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, color: 'text.secondary' }}>
|
||||
通知方式
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, color: 'text.secondary' }}>
|
||||
通知方式
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.emailNotifications}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, emailNotifications: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Email sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||
電子信箱通知
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.pushNotifications}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, pushNotifications: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Notifications sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||
推送通知
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.smsNotifications}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, smsNotifications: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Sms sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||
簡訊通知
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, color: 'text.secondary' }}>
|
||||
通知內容
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.todoReminders}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, todoReminders: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="待辦事項提醒"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.deadlineAlerts}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, deadlineAlerts: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="截止日期警告"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.weeklyReports}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, weeklyReports: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="每週報告"
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, color: 'text.secondary' }}>
|
||||
聲音設定
|
||||
</Typography>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.soundEnabled}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, soundEnabled: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<VolumeUp sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||
啟用通知聲音
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{notificationSettings.soundEnabled && (
|
||||
<Box sx={{ px: 2, mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary" gutterBottom>
|
||||
音量大小: {notificationSettings.soundVolume}%
|
||||
</Typography>
|
||||
<Slider
|
||||
value={notificationSettings.soundVolume}
|
||||
onChange={(_, value) => setNotificationSettings(prev => ({ ...prev, soundVolume: value as number }))}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
marks
|
||||
sx={{ color: 'primary.main' }}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.emailNotifications}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, emailNotifications: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Email sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||
電子信箱通知
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, color: 'text.secondary' }}>
|
||||
通知內容
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.todoReminders}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, todoReminders: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="待辦事項提醒"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.deadlineAlerts}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, deadlineAlerts: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="截止日期警告"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.weeklyReports}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, weeklyReports: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="每週報告"
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, color: 'text.secondary' }}>
|
||||
聲音設定
|
||||
</Typography>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.soundEnabled}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, soundEnabled: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<VolumeUp sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||
啟用通知聲音
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{notificationSettings.soundEnabled && (
|
||||
<Box sx={{ px: 2, mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary" gutterBottom>
|
||||
音量大小: {notificationSettings.soundVolume}%
|
||||
</Typography>
|
||||
<Slider
|
||||
value={notificationSettings.soundVolume}
|
||||
onChange={(_, value) => setNotificationSettings(prev => ({ ...prev, soundVolume: value as number }))}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
marks
|
||||
sx={{ color: 'primary.main' }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{/* 標題區域 */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<motion.div variants={itemVariants}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
mb: 0.5,
|
||||
background: actualTheme === 'dark'
|
||||
? 'linear-gradient(45deg, #f3f4f6 30%, #d1d5db 90%)'
|
||||
: 'linear-gradient(45deg, #111827 30%, #374151 90%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
}}
|
||||
>
|
||||
設定
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
管理您的個人資料和應用程式偏好設定
|
||||
</Typography>
|
||||
</motion.div>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
mb: 0.5,
|
||||
background: actualTheme === 'dark'
|
||||
? 'linear-gradient(45deg, #f3f4f6 30%, #d1d5db 90%)'
|
||||
: 'linear-gradient(45deg, #111827 30%, #374151 90%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
}}
|
||||
>
|
||||
郵件通知設定
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
管理您的郵件通知偏好設定
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* 側邊欄 */}
|
||||
<Grid item xs={12} md={3}>
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card sx={{
|
||||
backgroundColor: actualTheme === 'dark' ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
{tabs.map((tab) => (
|
||||
<motion.div
|
||||
key={tab.id}
|
||||
whileHover={{ x: 4 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
startIcon={tab.icon}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
textTransform: 'none',
|
||||
fontWeight: activeTab === tab.id ? 600 : 400,
|
||||
color: activeTab === tab.id ? 'primary.main' : 'text.primary',
|
||||
backgroundColor: activeTab === tab.id
|
||||
? (actualTheme === 'dark' ? 'rgba(59, 130, 246, 0.1)' : 'rgba(59, 130, 246, 0.1)')
|
||||
: 'transparent',
|
||||
borderRadius: 2,
|
||||
mb: 1,
|
||||
py: 1.5,
|
||||
px: 2,
|
||||
'&:hover': {
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(59, 130, 246, 0.1)'
|
||||
: 'rgba(59, 130, 246, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</Button>
|
||||
</motion.div>
|
||||
))}
|
||||
</Box>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
{/* 設定內容 */}
|
||||
{renderNotificationSettings()}
|
||||
|
||||
{/* 主要內容 */}
|
||||
<Grid item xs={12} md={9}>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeTab}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{activeTab === 'profile' && renderProfileSettings()}
|
||||
{activeTab === 'appearance' && renderAppearanceSettings()}
|
||||
{activeTab === 'notifications' && renderNotificationSettings()}
|
||||
{/* 其他 tab 內容可以在這裡添加 */}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 儲存按鈕 */}
|
||||
<motion.div variants={itemVariants} style={{ marginTop: 24 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Refresh />}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Save />}
|
||||
onClick={handleSave}
|
||||
sx={{
|
||||
background: 'linear-gradient(45deg, #3b82f6 30%, #8b5cf6 90%)',
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
px: 3,
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(45deg, #2563eb 30%, #7c3aed 90%)',
|
||||
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
儲存變更
|
||||
</Button>
|
||||
</Box>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{/* 儲存按鈕 */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 3 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Refresh />}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Save />}
|
||||
onClick={handleSave}
|
||||
sx={{
|
||||
background: 'linear-gradient(45deg, #3b82f6 30%, #8b5cf6 90%)',
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
px: 3,
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(45deg, #2563eb 30%, #7c3aed 90%)',
|
||||
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
儲存變更
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* 成功通知 */}
|
||||
<Snackbar
|
||||
@@ -753,7 +253,7 @@ const SettingsPage = () => {
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
設定已成功儲存!
|
||||
郵件通知設定已成功儲存!
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</motion.div>
|
||||
|
@@ -51,22 +51,22 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children }) => {
|
||||
const { user, logout } = useAuth();
|
||||
const { themeMode, actualTheme, setThemeMode } = useTheme();
|
||||
const muiTheme = useMuiTheme();
|
||||
const isMobile = useMediaQuery('(max-width: 1200px)'); // 降低斷點確保覆蓋所有小螢幕
|
||||
const isMobile = useMediaQuery('(max-width: 768px)'); // 調整為平板以下尺寸才隱藏側邊欄
|
||||
const isTablet = useMediaQuery('(max-width: 1024px) and (min-width: 769px)'); // 平板尺寸自動收合
|
||||
|
||||
// 響應式處理
|
||||
useEffect(() => {
|
||||
console.log('Responsive handling:', {
|
||||
isMobile,
|
||||
windowWidth: window.innerWidth,
|
||||
currentSidebarOpen: sidebarOpen
|
||||
});
|
||||
if (isMobile) {
|
||||
setSidebarOpen(false);
|
||||
setSidebarCollapsed(false);
|
||||
} else if (isTablet) {
|
||||
setSidebarOpen(true);
|
||||
setSidebarCollapsed(true); // 平板尺寸自動收合側邊欄
|
||||
} else {
|
||||
setSidebarOpen(true);
|
||||
setSidebarCollapsed(false); // 桌面尺寸完全展開
|
||||
}
|
||||
}, [isMobile]);
|
||||
}, [isMobile, isTablet]);
|
||||
|
||||
// 保持 sidebar 狀態穩定
|
||||
useEffect(() => {
|
||||
@@ -142,15 +142,6 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children }) => {
|
||||
|
||||
|
||||
const toggleSidebar = (event?: React.MouseEvent) => {
|
||||
console.log('🔧 Toggle sidebar clicked:', {
|
||||
isMobile,
|
||||
sidebarOpen,
|
||||
sidebarCollapsed,
|
||||
windowWidth: window.innerWidth,
|
||||
eventTarget: event?.target,
|
||||
eventCurrentTarget: event?.currentTarget
|
||||
});
|
||||
|
||||
// 防止事件冒泡
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
@@ -158,10 +149,8 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children }) => {
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
console.log('📱 Mobile: Setting sidebar open to:', !sidebarOpen);
|
||||
setSidebarOpen(!sidebarOpen);
|
||||
} else {
|
||||
console.log('🖥️ Desktop: Toggling collapsed to:', !sidebarCollapsed);
|
||||
setSidebarCollapsed(!sidebarCollapsed);
|
||||
}
|
||||
};
|
||||
|
@@ -35,10 +35,17 @@ import dayjs, { Dayjs } from 'dayjs';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import weekday from 'dayjs/plugin/weekday';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
|
||||
dayjs.extend(isoWeek);
|
||||
dayjs.extend(isSameOrAfter);
|
||||
dayjs.extend(isSameOrBefore);
|
||||
dayjs.extend(weekday);
|
||||
dayjs.extend(localeData);
|
||||
|
||||
// 設定週的開始為週日
|
||||
dayjs.Ls.en.weekStart = 0;
|
||||
|
||||
interface CalendarViewProps {
|
||||
todos: Todo[];
|
||||
@@ -85,8 +92,16 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
case 'month': {
|
||||
const startOfMonth = currentDate.startOf('month');
|
||||
const endOfMonth = currentDate.endOf('month');
|
||||
const startOfWeek = startOfMonth.startOf('week');
|
||||
const endOfWeek = endOfMonth.endOf('week');
|
||||
|
||||
// 獲取月份第一天是星期幾 (0=週日, 1=週一, ..., 6=週六)
|
||||
const firstDayWeekday = startOfMonth.day();
|
||||
// 獲取月份最後一天是星期幾
|
||||
const lastDayWeekday = endOfMonth.day();
|
||||
|
||||
// 計算需要顯示的第一天(從包含本月第一天的那週的週日開始)
|
||||
const startOfWeek = startOfMonth.subtract(firstDayWeekday, 'day');
|
||||
// 計算需要顯示的最後一天(到包含本月最後一天的那週的週六結束)
|
||||
const endOfWeek = endOfMonth.add(6 - lastDayWeekday, 'day');
|
||||
|
||||
const dates = [];
|
||||
let current = startOfWeek;
|
||||
@@ -211,65 +226,95 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<Grid container spacing={1}>
|
||||
{/* 星期標題 */}
|
||||
{['日', '一', '二', '三', '四', '五', '六'].map((day, index) => (
|
||||
<Grid item xs key={index}>
|
||||
<Paper
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: actualTheme === 'dark' ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}`,
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{/* 星期標題行 */}
|
||||
<Box sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(7, 1fr)',
|
||||
backgroundColor: actualTheme === 'dark' ? '#374151' : '#f3f4f6',
|
||||
borderBottom: `1px solid ${actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}>
|
||||
{['日', '一', '二', '三', '四', '五', '六'].map((day, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
p: 1,
|
||||
p: 2,
|
||||
textAlign: 'center',
|
||||
backgroundColor: actualTheme === 'dark' ? '#374151' : '#f3f4f6',
|
||||
border: 'none',
|
||||
borderRight: index < 6 ? `1px solid ${actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}` : 'none',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.secondary' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.secondary' }}>
|
||||
{day}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* 日期網格 */}
|
||||
{weeks.map((week, weekIndex) =>
|
||||
week.map((date, dayIndex) => {
|
||||
const todosForDate = getTodosForDate(date);
|
||||
const isCurrentMonth = date.month() === currentDate.month();
|
||||
const isToday = date.isSame(dayjs(), 'day');
|
||||
|
||||
return (
|
||||
<Grid item xs key={`${weekIndex}-${dayIndex}`}>
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{weeks.map((week, weekIndex) => (
|
||||
<Box
|
||||
key={weekIndex}
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(7, 1fr)',
|
||||
borderBottom: weekIndex < weeks.length - 1 ? `1px solid ${actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}` : 'none',
|
||||
}}
|
||||
>
|
||||
{week.map((date, dayIndex) => {
|
||||
const todosForDate = getTodosForDate(date);
|
||||
const isCurrentMonth = date.month() === currentDate.month();
|
||||
const isToday = date.isSame(dayjs(), 'day');
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={`${weekIndex}-${dayIndex}`}
|
||||
sx={{
|
||||
minHeight: 120,
|
||||
p: 1,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? (isCurrentMonth ? '#1f2937' : '#111827')
|
||||
: (isCurrentMonth ? '#ffffff' : '#f9fafb'),
|
||||
border: isToday
|
||||
? `2px solid ${actualTheme === 'dark' ? '#60a5fa' : '#3b82f6'}`
|
||||
: `1px solid ${actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}`,
|
||||
p: 1.5,
|
||||
borderRight: dayIndex < 6 ? `1px solid ${actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}` : 'none',
|
||||
backgroundColor: isToday
|
||||
? (actualTheme === 'dark' ? 'rgba(59, 130, 246, 0.1)' : 'rgba(59, 130, 246, 0.05)')
|
||||
: (isCurrentMonth
|
||||
? 'transparent'
|
||||
: (actualTheme === 'dark' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(0, 0, 0, 0.02)')),
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(59, 130, 246, 0.1)'
|
||||
: 'rgba(59, 130, 246, 0.05)',
|
||||
transform: 'translateY(-1px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
|
||||
{/* 日期數字和徽章 */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: isToday ? 700 : 500,
|
||||
fontWeight: isToday ? 700 : (isCurrentMonth ? 500 : 400),
|
||||
color: isCurrentMonth
|
||||
? (isToday ? 'primary.main' : 'text.primary')
|
||||
: 'text.disabled',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{date.date()}
|
||||
@@ -280,23 +325,24 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
color="primary"
|
||||
sx={{
|
||||
'& .MuiBadge-badge': {
|
||||
fontSize: '0.6rem',
|
||||
minWidth: 16,
|
||||
height: 16,
|
||||
fontSize: '0.7rem',
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
right: -6,
|
||||
top: -6,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Circle sx={{ fontSize: 8, color: 'primary.main' }} />
|
||||
</Badge>
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 待辦事項列表 */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{todosForDate.slice(0, 3).map((todo) => (
|
||||
{todosForDate.slice(0, 2).map((todo) => (
|
||||
<motion.div
|
||||
key={todo.id}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
>
|
||||
<Box
|
||||
onClick={(e) => handleTodoClick(todo, e)}
|
||||
@@ -306,12 +352,11 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
backgroundColor: selectedTodos.includes(todo.id)
|
||||
? 'rgba(59, 130, 246, 0.2)'
|
||||
: `${getPriorityColor(todo.priority)}15`,
|
||||
borderLeft: `3px solid ${getPriorityColor(todo.priority)}`,
|
||||
borderLeft: `2px solid ${getPriorityColor(todo.priority)}`,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: `${getPriorityColor(todo.priority)}25`,
|
||||
transform: 'translateX(2px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -320,62 +365,42 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
sx={{
|
||||
display: 'block',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.65rem',
|
||||
fontSize: '0.7rem',
|
||||
color: 'text.primary',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{todo.starred && <Star sx={{ fontSize: 10, color: '#fbbf24', mr: 0.25 }} />}
|
||||
{todo.title}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25, mt: 0.25 }}>
|
||||
<Circle
|
||||
sx={{
|
||||
fontSize: 6,
|
||||
color: getStatusColor(todo.status),
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: '0.6rem',
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const firstUser = todo.responsible_users_details?.[0] ||
|
||||
(todo.responsible_users?.[0] ? {ad_account: todo.responsible_users[0], display_name: todo.responsible_users[0]} : null);
|
||||
return firstUser ? (firstUser.display_name || firstUser.ad_account) : '未指派';
|
||||
})()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{todosForDate.length > 3 && (
|
||||
{todosForDate.length > 2 && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: '0.6rem',
|
||||
fontSize: '0.65rem',
|
||||
color: 'text.secondary',
|
||||
textAlign: 'center',
|
||||
mt: 0.5,
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
>
|
||||
+{todosForDate.length - 3} 更多
|
||||
+{todosForDate.length - 2} 項
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
@@ -288,7 +288,7 @@ const ExcelImport: React.FC<ExcelImportProps> = ({ open, onClose, onImportComple
|
||||
<TableCell>優先級</TableCell>
|
||||
<TableCell>到期日</TableCell>
|
||||
<TableCell>負責人</TableCell>
|
||||
<TableCell>追蹤人</TableCell>
|
||||
<TableCell>公開設定</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -332,12 +332,12 @@ const ExcelImport: React.FC<ExcelImportProps> = ({ open, onClose, onImportComple
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{todo.responsible_users.join(', ') || '-'}
|
||||
{(todo.responsible_users && todo.responsible_users.length > 0) ? todo.responsible_users.join(', ') : '-'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{todo.followers.join(', ') || '-'}
|
||||
{todo.is_public ? '是' : '否'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
@@ -66,7 +66,6 @@ interface LocalTodo {
|
||||
starred: boolean;
|
||||
creator?: User;
|
||||
responsible: User[];
|
||||
tags: string[];
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
@@ -101,11 +100,9 @@ const TodoDialog: React.FC<TodoDialogProps> = ({
|
||||
dueDate: null,
|
||||
starred: false,
|
||||
responsible: [],
|
||||
tags: [],
|
||||
isPublic: true,
|
||||
isPublic: false, // 預設為非公開
|
||||
});
|
||||
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [assignToMyself, setAssignToMyself] = useState(false);
|
||||
|
||||
// 用戶資料
|
||||
@@ -178,8 +175,7 @@ const TodoDialog: React.FC<TodoDialogProps> = ({
|
||||
avatar: adAccount.charAt(0).toUpperCase(),
|
||||
department: '員工'
|
||||
})),
|
||||
tags: apiTodo.tags || [],
|
||||
isPublic: true, // 預設值
|
||||
isPublic: false, // 預設值
|
||||
};
|
||||
setFormData(editTodo);
|
||||
} else {
|
||||
@@ -191,8 +187,7 @@ const TodoDialog: React.FC<TodoDialogProps> = ({
|
||||
dueDate: null,
|
||||
starred: false,
|
||||
responsible: [],
|
||||
tags: [],
|
||||
isPublic: true,
|
||||
isPublic: false,
|
||||
});
|
||||
}
|
||||
setAssignToMyself(false);
|
||||
@@ -215,20 +210,6 @@ const TodoDialog: React.FC<TodoDialogProps> = ({
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddTag = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter' && tagInput.trim()) {
|
||||
event.preventDefault();
|
||||
const newTag = tagInput.trim();
|
||||
if (!(formData.tags || []).includes(newTag)) {
|
||||
handleInputChange('tags', [...(formData.tags || []), newTag]);
|
||||
}
|
||||
setTagInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTag = (tagToRemove: string) => {
|
||||
handleInputChange('tags', (formData.tags || []).filter(tag => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
if (!formData.title.trim()) {
|
||||
@@ -265,7 +246,6 @@ const TodoDialog: React.FC<TodoDialogProps> = ({
|
||||
responsible_users: responsibleUsers,
|
||||
starred: formData.starred,
|
||||
is_public: formData.isPublic,
|
||||
tags: formData.tags
|
||||
};
|
||||
|
||||
let savedTodo;
|
||||
@@ -653,62 +633,14 @@ const TodoDialog: React.FC<TodoDialogProps> = ({
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* 標籤和設定 */}
|
||||
{/* 設定 */}
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
|
||||
標籤和設定
|
||||
設定
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="新增標籤"
|
||||
placeholder="輸入標籤並按 Enter..."
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={handleAddTag}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 2 }}>
|
||||
{(formData.tags || []).map((tag, index) => (
|
||||
<motion.div
|
||||
key={tag}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
>
|
||||
<Chip
|
||||
label={tag}
|
||||
onDelete={() => handleRemoveTag(tag)}
|
||||
deleteIcon={<Delete sx={{ fontSize: 16 }} />}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(139, 92, 246, 0.2)'
|
||||
: 'rgba(139, 92, 246, 0.1)',
|
||||
color: '#8b5cf6',
|
||||
'&:hover': {
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(139, 92, 246, 0.3)'
|
||||
: 'rgba(139, 92, 246, 0.15)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
|
@@ -20,7 +20,6 @@ export interface Todo {
|
||||
creator_email?: string;
|
||||
starred: boolean;
|
||||
is_public: boolean;
|
||||
tags: string[];
|
||||
responsible_users: string[];
|
||||
followers: string[];
|
||||
responsible_users_details?: UserDetail[];
|
||||
@@ -35,7 +34,6 @@ export interface TodoCreate {
|
||||
due_date?: string;
|
||||
starred?: boolean;
|
||||
is_public?: boolean;
|
||||
tags?: string[];
|
||||
responsible_users?: string[];
|
||||
followers?: string[];
|
||||
}
|
||||
@@ -52,7 +50,6 @@ export interface TodoFilter {
|
||||
due_to?: string;
|
||||
search?: string;
|
||||
view?: 'all' | 'created' | 'responsible' | 'following' | 'public';
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
// User Types
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 6.8 KiB |
Binary file not shown.
657
usermanual.md
657
usermanual.md
@@ -1,469 +1,360 @@
|
||||
# PANJIT To-Do System 使用者手冊
|
||||
# PANJIT To-Do 系統 - 使用者手冊
|
||||
|
||||
## 📖 目錄
|
||||
|
||||
1. [系統簡介](#系統簡介)
|
||||
2. [登入系統](#登入系統)
|
||||
3. [主要功能](#主要功能)
|
||||
4. [待辦事項管理](#待辦事項管理)
|
||||
5. [協作功能](#協作功能)
|
||||
6. [通知設定](#通知設定)
|
||||
7. [Excel 匯入匯出](#excel-匯入匯出)
|
||||
8. [進階功能](#進階功能)
|
||||
1. [系統登入](#系統登入)
|
||||
2. [主要介面說明](#主要介面說明)
|
||||
3. [待辦事項管理](#待辦事項管理)
|
||||
4. [篩選與搜尋](#篩選與搜尋)
|
||||
5. [日曆視圖](#日曆視圖)
|
||||
6. [Excel 功能](#excel-功能)
|
||||
7. [通知設定](#通知設定)
|
||||
8. [系統設定](#系統設定)
|
||||
9. [常見問題](#常見問題)
|
||||
|
||||
---
|
||||
|
||||
## 系統簡介
|
||||
## 🔐 系統登入
|
||||
|
||||
PANJIT To-Do System 是一個專為企業環境設計的待辦事項管理系統,提供:
|
||||
### 登入步驟
|
||||
1. 開啟瀏覽器,輸入系統網址
|
||||
2. 在登入頁面輸入您的 **AD 帳號** 和 **密碼**
|
||||
3. 點擊「登入」按鈕
|
||||
|
||||
- **統一登入**:使用公司 AD/LDAP 帳號直接登入
|
||||
- **多人協作**:支援團隊合作、責任分工
|
||||
- **智能通知**:自動化郵件提醒,不錯過任何重要任務
|
||||
- **數據管理**:Excel 批量匯入匯出,高效處理大量資料
|
||||
- **行動支援**:響應式設計,桌面和行動裝置完美適配
|
||||
### 登入注意事項
|
||||
- 請使用公司 Active Directory 帳號
|
||||
- 如果忘記密碼,請聯繫 IT 部門重設
|
||||
- 系統支援記住登入狀態功能
|
||||
|
||||
---
|
||||
|
||||
## 登入系統
|
||||
## 🏠 主要介面說明
|
||||
|
||||
### 🔐 第一次登入
|
||||
### 導航側邊欄
|
||||
系統左側提供完整的功能導航:
|
||||
|
||||
1. **開啟系統**
|
||||
- 瀏覽器開啟:`http://your-company-domain.com`
|
||||
- 系統支援:Chrome、Firefox、Safari、Edge
|
||||
#### 主要功能
|
||||
- **儀表板**:查看系統總覽和統計資料
|
||||
- **待辦清單**:管理所有待辦事項
|
||||
- **公開任務**:查看公開的待辦事項
|
||||
- **日曆視圖**:以日曆形式查看任務
|
||||
|
||||
2. **輸入帳號密碼**
|
||||
- 使用者名稱:`帳號@panjit.com.tw` 或 `帳號`
|
||||
- 密碼:與 Windows 登入相同
|
||||
|
||||
3. **首次登入設定**
|
||||
- 系統會自動從 AD 取得您的姓名和 Email
|
||||
- 建立個人偏好設定
|
||||
- 設定預設通知選項
|
||||
#### 視圖篩選
|
||||
- **已加星**:查看標記星號的重要事項
|
||||
- **我建立的**:顯示由您建立的待辦事項
|
||||
- **指派給我**:顯示指派給您的待辦事項
|
||||
- **我追蹤的**:顯示您正在追蹤的待辦事項
|
||||
|
||||
### ⚙️ 登入選項
|
||||
#### 狀態分類
|
||||
- **新建立**:剛建立尚未開始的任務
|
||||
- **進行中**:正在進行的任務
|
||||
- **已阻塞**:遇到問題暫時停止的任務
|
||||
- **已完成**:已經完成的任務
|
||||
|
||||
- **記住我**:勾選後下次自動登入(7天內有效)
|
||||
- **語言切換**:支援繁體中文、英文
|
||||
- **主題模式**:淺色/深色主題切換
|
||||
### 頂部工具列
|
||||
- **選單按鈕**:在小螢幕上收合/展開側邊欄
|
||||
- **主題切換**:切換亮色/暗色/自動模式
|
||||
- **通知鈴鐺**:查看系統通知
|
||||
- **使用者頭像**:存取個人設定和登出
|
||||
|
||||
---
|
||||
|
||||
## 主要功能
|
||||
## ✅ 待辦事項管理
|
||||
|
||||
### 📱 系統介面
|
||||
### 建立新的待辦事項
|
||||
1. 點擊「+ 新增待辦事項」按鈕
|
||||
2. 填寫以下資訊:
|
||||
- **標題**:簡明的任務描述(必填)
|
||||
- **內容**:詳細的任務說明
|
||||
- **截止日期**:任務完成期限
|
||||
- **優先級**:高、中、低
|
||||
- **負責人**:可指派給其他同事
|
||||
- **追蹤者**:需要關注此任務的人員
|
||||
- **標籤**:用於分類管理
|
||||
3. 點擊「建立」按鈕
|
||||
|
||||
#### 頂部導航欄
|
||||
- **系統標題**:顯示目前頁面
|
||||
- **搜尋列**:快速搜尋待辦事項
|
||||
- **通知鈴鐺**:系統通知提醒
|
||||
- **使用者頭像**:個人設定選單
|
||||
### 編輯待辦事項
|
||||
1. 在待辦清單中點擊要編輯的項目
|
||||
2. 修改所需的資訊
|
||||
3. 點擊「儲存」按鈕確認變更
|
||||
|
||||
#### 左側選單
|
||||
- **儀表板**:系統概覽和統計
|
||||
- **待辦清單**:主要工作區域
|
||||
- **行事曆**:時間軸檢視
|
||||
- **設定**:個人化設定
|
||||
### 狀態管理
|
||||
- **拖放操作**:直接拖拽任務到不同狀態欄
|
||||
- **下拉選單**:點擊狀態下拉選單切換
|
||||
- **快速按鈕**:使用工具列上的快速狀態按鈕
|
||||
|
||||
#### 主要工作區
|
||||
- **工具列**:篩選、排序、批量操作
|
||||
- **清單檢視**:條列式顯示
|
||||
- **卡片檢視**:圖像化顯示
|
||||
- **行事曆檢視**:時間軸顯示
|
||||
### 重要功能
|
||||
- **加星標記**:點擊星號圖示標記重要任務
|
||||
- **複製任務**:快速建立相似任務
|
||||
- **刪除任務**:刪除不需要的任務(僅限建立者)
|
||||
|
||||
---
|
||||
|
||||
## 待辦事項管理
|
||||
## 🔍 篩選與搜尋
|
||||
|
||||
### ➕ 建立新待辦事項
|
||||
### 搜尋功能
|
||||
- 在頂部搜尋框輸入關鍵字
|
||||
- 支援搜尋標題、內容、負責人、建立者
|
||||
- 即時搜尋,輸入即顯示結果
|
||||
|
||||
1. **點選「新增待辦」按鈕**
|
||||
- 位於右上角藍色按鈕
|
||||
- 快捷鍵:`Ctrl + N`
|
||||
### 篩選選項
|
||||
點擊「篩選」按鈕可設定以下條件:
|
||||
- **狀態篩選**:選擇特定狀態的任務
|
||||
- **優先級篩選**:篩選不同優先級
|
||||
- **日期範圍**:設定建立日期或截止日期範圍
|
||||
- **負責人篩選**:查看特定人員的任務
|
||||
- **標籤篩選**:依標籤分類查看
|
||||
|
||||
2. **填寫基本資訊**
|
||||
```
|
||||
標題*:簡潔描述任務內容
|
||||
描述:詳細說明和注意事項
|
||||
狀態:新建立 (預設)
|
||||
優先級:中 (預設)
|
||||
到期日:選擇完成期限
|
||||
```
|
||||
|
||||
3. **指派人員**
|
||||
- **負責人**:執行任務的人員
|
||||
- 支援多人負責
|
||||
- 可搜尋公司同事姓名或帳號
|
||||
- 自動驗證帳號存在性
|
||||
|
||||
- **追蹤人員**:需要了解進度的人員
|
||||
- 主管或相關部門同事
|
||||
- 會收到狀態變更通知
|
||||
|
||||
4. **進階設定**
|
||||
- **標籤**:分類標記(可自訂)
|
||||
- **星號標記**:重要項目標示
|
||||
- **公開/私人**:可見性設定
|
||||
|
||||
### ✏️ 編輯待辦事項
|
||||
|
||||
1. **開啟編輯模式**
|
||||
- 點選事項標題直接編輯
|
||||
- 或點選「編輯」按鈕
|
||||
|
||||
2. **權限說明**
|
||||
- **建立者**:完整編輯權限
|
||||
- **負責人**:可修改狀態、描述
|
||||
- **追蹤人員**:僅可查看
|
||||
|
||||
3. **狀態變更**
|
||||
- **新建立** → **進行中**:開始執行
|
||||
- **進行中** → **已阻礙**:遇到困難需協助
|
||||
- **已阻礙** → **進行中**:問題解決繼續執行
|
||||
- **進行中** → **已完成**:任務完成
|
||||
|
||||
### 🔍 搜尋和篩選
|
||||
|
||||
#### 快速搜尋
|
||||
- **關鍵字搜尋**:標題、描述、人員姓名
|
||||
- **模糊比對**:不需輸入完整字詞
|
||||
- **即時搜尋**:輸入即時顯示結果
|
||||
|
||||
#### 進階篩選
|
||||
1. **點選篩選按鈕**(漏斗圖示)
|
||||
2. **設定篩選條件**:
|
||||
- 狀態:單選或多選
|
||||
- 優先級:依重要程度篩選
|
||||
- 指派人員:查看特定人員的任務
|
||||
- 日期範圍:到期日區間
|
||||
- 標籤:依分類查看
|
||||
|
||||
3. **常用篩選**:
|
||||
- 我負責的:顯示自己的任務
|
||||
- 我追蹤的:顯示關注的項目
|
||||
- 逾期項目:已過期未完成
|
||||
- 本週到期:七天內到期
|
||||
|
||||
#### 排序選項
|
||||
- **到期日**:依時間緊急程度
|
||||
- **優先級**:依重要程度
|
||||
- **建立時間**:依新舊程度
|
||||
- **狀態**:依進度分組
|
||||
- **標題**:字母順序
|
||||
### 排序選項
|
||||
- **建立時間**:依建立時間排序
|
||||
- **截止日期**:依到期日排序
|
||||
- **優先級**:依重要性排序
|
||||
- **狀態**:依任務狀態排序
|
||||
|
||||
---
|
||||
|
||||
## 協作功能
|
||||
## 📅 日曆視圖
|
||||
|
||||
### 👥 團隊協作
|
||||
### 檢視模式
|
||||
- **月檢視**:以月份為單位顯示任務
|
||||
- **週檢視**:以週為單位的詳細檢視
|
||||
- **日檢視**:單日的詳細任務排程
|
||||
|
||||
#### 角色說明
|
||||
- **建立者**:任務發起人,擁有完整權限
|
||||
- **負責人**:實際執行者,可編輯任務內容
|
||||
- **追蹤人員**:關注者,可查看進度並接收通知
|
||||
### 日曆操作
|
||||
- **拖放任務**:直接拖拽任務到其他日期
|
||||
- **快速建立**:點擊日期快速建立任務
|
||||
- **任務詳情**:點擊任務查看完整資訊
|
||||
|
||||
#### 協作流程
|
||||
1. **任務指派**
|
||||
```
|
||||
建立者 → 指派負責人 → 設定追蹤人員 → 發送通知
|
||||
```
|
||||
|
||||
2. **進度更新**
|
||||
```
|
||||
負責人更新狀態 → 系統記錄變更 → 自動通知相關人員
|
||||
```
|
||||
|
||||
3. **問題處理**
|
||||
```
|
||||
遇到阻礙 → 標記為「已阻礙」→ 通知建立者/追蹤人員 → 協助解決
|
||||
```
|
||||
|
||||
### 🔔 即時通知
|
||||
|
||||
#### Fire 一鍵提醒
|
||||
- **功能說明**:立即發送郵件提醒
|
||||
- **使用時機**:急件處理、重要提醒
|
||||
- **限制機制**:
|
||||
- 2分鐘冷卻時間
|
||||
- 每日20封限額
|
||||
- 防止過度騷擾
|
||||
|
||||
#### 自動化通知
|
||||
- **到期前提醒**:3天前自動發送
|
||||
- **當天提醒**:到期當日早上通知
|
||||
- **逾期提醒**:逾期1天後提醒
|
||||
- **狀態變更**:任務狀態改變時通知
|
||||
|
||||
#### 週摘要報告
|
||||
- **發送時間**:每週一早上9點
|
||||
- **內容包含**:
|
||||
- 本週待完成任務
|
||||
- 逾期項目統計
|
||||
- 新指派任務
|
||||
- 完成項目回顧
|
||||
### 顏色編碼
|
||||
- **紅色**:逾期任務
|
||||
- **橙色**:即將到期(3天內)
|
||||
- **藍色**:一般任務
|
||||
- **綠色**:已完成任務
|
||||
- **灰色**:已阻塞任務
|
||||
|
||||
---
|
||||
|
||||
## 通知設定
|
||||
## 📊 Excel 功能
|
||||
|
||||
### 📧 郵件通知設定
|
||||
### 匯入待辦事項
|
||||
1. 點擊「Excel 匯入」按鈕
|
||||
2. 下載匯入模板(首次使用建議)
|
||||
3. 填寫 Excel 檔案:
|
||||
- **標題**:任務標題(必填)
|
||||
- **內容**:任務詳細說明
|
||||
- **截止日期**:格式為 YYYY-MM-DD
|
||||
- **優先級**:HIGH/MEDIUM/LOW
|
||||
- **負責人**:AD帳號或郵件地址
|
||||
- **狀態**:NEW/DOING/BLOCKED/DONE
|
||||
4. 上傳並預覽匯入資料
|
||||
5. 確認無誤後執行匯入
|
||||
|
||||
1. **開啟設定頁面**
|
||||
- 點選右上角個人頭像
|
||||
- 選擇「通知設定」
|
||||
### 匯出待辦事項
|
||||
1. 在待辦清單頁面點擊「Excel 匯出」
|
||||
2. 選擇匯出範圍:
|
||||
- **全部任務**
|
||||
- **目前篩選結果**
|
||||
- **選中的任務**
|
||||
3. 選擇匯出格式和欄位
|
||||
4. 點擊「匯出」下載檔案
|
||||
|
||||
2. **通知類型設定**
|
||||
- ✅ 新任務指派通知
|
||||
- ✅ 任務狀態變更通知
|
||||
- ✅ 到期提醒通知
|
||||
- ✅ Fire 一鍵提醒
|
||||
- ✅ 週摘要報告
|
||||
|
||||
3. **時間設定**
|
||||
- **提醒天數**:到期前幾天提醒(1-7天)
|
||||
- **工作時間**:僅工作時間發送(9:00-18:00)
|
||||
- **週末通知**:是否包含週末
|
||||
|
||||
4. **郵件格式**
|
||||
- **HTML 格式**:豐富內容顯示
|
||||
- **純文字格式**:相容性更好
|
||||
- **摘要模式**:減少郵件數量
|
||||
|
||||
### 🔕 勿擾模式
|
||||
|
||||
- **暫停所有通知**:臨時關閉通知
|
||||
- **例外設定**:緊急通知例外
|
||||
- **自動恢復**:設定恢復時間
|
||||
### Excel 模板說明
|
||||
- **必填欄位**:標題
|
||||
- **日期格式**:YYYY-MM-DD HH:MM
|
||||
- **狀態值**:NEW, DOING, BLOCKED, DONE
|
||||
- **優先級值**:HIGH, MEDIUM, LOW
|
||||
- **使用者格式**:AD帳號或完整郵件地址
|
||||
|
||||
---
|
||||
|
||||
## Excel 匯入匯出
|
||||
## 🔐 權限說明與控制
|
||||
|
||||
### 📥 匯入待辦事項
|
||||
### 權限矩陣
|
||||
|
||||
#### Step 1:下載模板
|
||||
1. **點選「Excel 匯入」按鈕**
|
||||
2. **下載標準模板**
|
||||
- 包含所有必要欄位
|
||||
- 內建格式驗證
|
||||
- 範例資料參考
|
||||
系統的待辦事項權限控制如下:
|
||||
|
||||
#### Step 2:填寫資料
|
||||
```excel
|
||||
標題* | 描述 | 狀態 | 優先級 | 到期日 | 負責人* | 追蹤人員
|
||||
完成報告撰寫 | 月報內容 | NEW | HIGH | 2024/1/15 | user1 | user2,user3
|
||||
系統維護 | 定期檢查 | DOING | MEDIUM | 2024/1/20 | user2 | user1
|
||||
```
|
||||
| 操作權限 | 建立者 | 負責人 | 追蹤者 | 其他使用者 |
|
||||
|---------|--------|--------|--------|------------|
|
||||
| **查看非公開任務** | ✅ 可以 | ✅ 可以 | ❌ 不可 | ❌ 不可 |
|
||||
| **查看公開任務** | ✅ 可以 | ✅ 可以 | ✅ 可以 | ✅ 可以 |
|
||||
| **編輯任務內容** | ✅ 可以 | ❌ 不可 | ❌ 不可 | ❌ 不可 |
|
||||
| **刪除任務** | ✅ 可以 | ❌ 不可 | ❌ 不可 | ❌ 不可 |
|
||||
| **更改任務狀態** | ✅ 可以 | ❌ 不可 | ❌ 不可 | ❌ 不可 |
|
||||
| **指派負責人** | ✅ 可以 | ❌ 不可 | ❌ 不可 | ❌ 不可 |
|
||||
| **設定公開/私人** | ✅ 可以 | ❌ 不可 | ❌ 不可 | ❌ 不可 |
|
||||
| **追蹤公開任務** | ✅ 可以 | ✅ 可以 | ✅ 可以 | ✅ 可以 |
|
||||
| **追蹤非公開任務** | ❌ 不可 | ❌ 不可 | ❌ 不可 | ❌ 不可 |
|
||||
|
||||
**欄位說明:**
|
||||
- `*` 為必填欄位
|
||||
- 狀態:NEW, DOING, BLOCKED, DONE
|
||||
- 優先級:LOW, MEDIUM, HIGH, URGENT
|
||||
- 日期格式:YYYY/MM/DD
|
||||
- 人員:使用帳號,多人用逗號分隔
|
||||
### 角色說明
|
||||
|
||||
#### Step 3:上傳檔案
|
||||
1. **選擇檔案**:支援 .xlsx, .xls, .csv
|
||||
2. **系統驗證**:
|
||||
- 格式檢查
|
||||
- 欄位驗證
|
||||
- 人員帳號確認
|
||||
- 重複項目檢查
|
||||
#### 建立者 (Creator)
|
||||
- 對待辦事項擁有**完全控制權**
|
||||
- 可以執行所有操作:編輯、刪除、狀態變更、指派等
|
||||
- 是唯一可以將任務設為公開或私人的角色
|
||||
|
||||
3. **預覽確認**:
|
||||
- 顯示將匯入的資料
|
||||
- 標示錯誤項目
|
||||
- 提供修正建議
|
||||
#### 負責人 (Responsible User)
|
||||
- 被指派執行任務的人員
|
||||
- **只能查看**任務內容,無法編輯
|
||||
- 可以看到非公開的任務(因為被指派)
|
||||
- 無法變更任務狀態或內容
|
||||
|
||||
4. **執行匯入**:
|
||||
- 成功項目批量建立
|
||||
- 失敗項目詳細報告
|
||||
- 自動發送通知
|
||||
#### 追蹤者 (Follower)
|
||||
- 關注公開任務進展的人員
|
||||
- **只能存在於公開任務**
|
||||
- 僅有查看權限,無法編輯
|
||||
- 非公開任務不支援追蹤功能
|
||||
|
||||
### 📤 匯出待辦事項
|
||||
#### 其他使用者
|
||||
- 只能查看和追蹤**公開任務**
|
||||
- 無法查看非公開任務
|
||||
- 無任何編輯權限
|
||||
|
||||
#### 匯出選項
|
||||
1. **全部匯出**:所有可存取的待辦事項
|
||||
2. **篩選匯出**:根據目前篩選條件
|
||||
3. **選取匯出**:勾選特定項目
|
||||
4. **範圍匯出**:指定日期範圍
|
||||
### 重要提醒
|
||||
|
||||
#### 檔案格式
|
||||
- **Excel 格式**:完整資料和格式
|
||||
- **CSV 格式**:純資料,相容性好
|
||||
- **PDF 報表**:列印友好格式
|
||||
|
||||
#### 匯出內容
|
||||
- 基本資訊:標題、描述、狀態、優先級
|
||||
- 時間資訊:建立時間、到期日、完成時間
|
||||
- 人員資訊:建立者、負責人、追蹤人員
|
||||
- 進階資訊:標籤、備註、操作記錄
|
||||
1. **非公開任務**預設只有建立者和被指派的負責人能看到
|
||||
2. **公開任務**所有系統使用者都能查看和追蹤
|
||||
3. **編輯權限**僅限建立者,確保任務內容的一致性
|
||||
4. **追蹤功能**僅適用於公開任務,私人任務不開放追蹤
|
||||
|
||||
---
|
||||
|
||||
## 進階功能
|
||||
## 🔔 通知設定
|
||||
|
||||
### 📊 統計報表
|
||||
### 通知類型
|
||||
系統提供以下通知:
|
||||
- **任務指派通知**:被指派新任務時
|
||||
- **狀態變更通知**:任務狀態改變時
|
||||
- **到期提醒**:任務即將到期時
|
||||
- **逾期通知**:任務已逾期時
|
||||
- **評論通知**:任務有新評論時
|
||||
|
||||
#### 個人統計
|
||||
- **任務完成率**:本週/本月完成統計
|
||||
- **平均處理時間**:從建立到完成的時間
|
||||
- **優先級分佈**:高/中/低優先級比例
|
||||
- **逾期分析**:逾期項目和原因分析
|
||||
### 通知設定
|
||||
1. 點擊右上角頭像 → 「通知設定」
|
||||
2. 選擇接收通知的類型:
|
||||
- **即時通知**:系統內彈出通知
|
||||
- **郵件通知**:發送到註冊郵箱
|
||||
- **提醒時間**:設定提前提醒時間
|
||||
3. 儲存設定
|
||||
|
||||
#### 團隊統計
|
||||
- **部門效率**:各部門完成情況
|
||||
- **工作量分析**:人員任務負荷
|
||||
- **協作頻率**:跨部門合作統計
|
||||
- **趨勢分析**:月度/季度趨勢
|
||||
|
||||
#### 系統統計
|
||||
- **使用者活躍度**:登入和操作頻率
|
||||
- **功能使用率**:各功能使用統計
|
||||
- **效能監控**:系統回應時間
|
||||
- **錯誤率統計**:系統穩定性指標
|
||||
|
||||
### 📅 行事曆整合
|
||||
|
||||
#### 檢視模式
|
||||
- **月檢視**:整月任務概覽
|
||||
- **週檢視**:週計劃詳細檢視
|
||||
- **日檢視**:單日任務清單
|
||||
- **時間線**:甘特圖式顯示
|
||||
|
||||
#### 互動功能
|
||||
- **拖拽調整**:直接拖動改變日期
|
||||
- **快速編輯**:點選事項快速修改
|
||||
- **顏色標示**:優先級和狀態色彩
|
||||
- **提醒設定**:行事曆提醒整合
|
||||
|
||||
### 🏷️ 標籤管理
|
||||
|
||||
#### 標籤功能
|
||||
- **自訂標籤**:建立個人化分類
|
||||
- **顏色標示**:視覺化識別
|
||||
- **快速篩選**:點選標籤快速篩選
|
||||
- **統計分析**:標籤使用統計
|
||||
|
||||
#### 常用標籤建議
|
||||
- 專案類:`#Project-A`, `#年度計劃`
|
||||
- 部門類:`#IT`, `#HR`, `#Sales`
|
||||
- 性質類:`#會議`, `#報告`, `#檢查`
|
||||
- 狀態類:`#待確認`, `#進行中`, `#暫停`
|
||||
|
||||
### 🔍 進階搜尋
|
||||
|
||||
#### 搜尋語法
|
||||
```
|
||||
標題搜尋:title:報告
|
||||
描述搜尋:desc:系統
|
||||
人員搜尋:user:張三
|
||||
標籤搜尋:tag:專案
|
||||
狀態搜尋:status:DOING
|
||||
日期搜尋:due:2024-01-15
|
||||
```
|
||||
|
||||
#### 組合搜尋
|
||||
```
|
||||
複合條件:title:報告 AND user:張三
|
||||
排除條件:title:會議 NOT status:DONE
|
||||
日期範圍:due:2024-01-01..2024-01-31
|
||||
```
|
||||
### 通知管理
|
||||
- **通知面板**:點擊通知鈴鐺查看所有通知
|
||||
- **標記已讀**:點擊通知標記為已讀
|
||||
- **清除通知**:清除所有已讀通知
|
||||
|
||||
---
|
||||
|
||||
## 常見問題
|
||||
## ⚙️ 系統設定
|
||||
|
||||
### ❓ 登入相關
|
||||
### 個人設定
|
||||
1. 點擊右上角頭像 → 「個人設定」
|
||||
2. 可調整:
|
||||
- **顯示語言**:系統介面語言
|
||||
- **時區設定**:本地時區
|
||||
- **每頁顯示數量**:列表每頁項目數
|
||||
- **預設視圖**:登入後預設頁面
|
||||
|
||||
**Q: 忘記密碼怎麼辦?**
|
||||
A: 系統使用公司 AD 帳號,請聯繫 IT 部門重設 Windows 密碼。
|
||||
### 主題設定
|
||||
- **亮色模式**:白色背景主題
|
||||
- **暗色模式**:深色背景主題
|
||||
- **自動切換**:跟隨系統設定
|
||||
|
||||
**Q: 帳號被鎖定無法登入?**
|
||||
A: 通常是密碼輸入錯誤多次,請聯繫 IT 部門解鎖帳號。
|
||||
### 隱私設定
|
||||
- **個人資料可見性**:設定其他使用者能看到的資訊
|
||||
- **任務預設權限**:新建任務的預設可見範圍
|
||||
|
||||
**Q: 可以在家裡使用嗎?**
|
||||
A: 需要連接公司 VPN 後才能存取內部系統。
|
||||
---
|
||||
|
||||
### ❓ 功能使用
|
||||
## ❓ 常見問題
|
||||
|
||||
**Q: 為什麼找不到某個同事?**
|
||||
### 登入相關
|
||||
**Q: 忘記密碼怎麼辦?**
|
||||
A: 請聯繫 IT 部門重設 AD 密碼,系統使用公司 Active Directory 認證。
|
||||
|
||||
**Q: 為什麼無法登入?**
|
||||
A: 請確認:
|
||||
1. 輸入正確的姓名或帳號
|
||||
2. 該同事帳號未被停用
|
||||
3. 有權限查看該同事資訊
|
||||
- AD 帳號和密碼正確
|
||||
- 帳號未被停用
|
||||
- 網路連線正常
|
||||
|
||||
**Q: 郵件通知沒有收到?**
|
||||
### 使用功能
|
||||
**Q: 無法建立待辦事項**
|
||||
A: 請檢查:
|
||||
1. 垃圾信件匣
|
||||
2. 通知設定是否開啟
|
||||
3. 郵箱容量是否已滿
|
||||
4. 公司郵件伺服器是否正常
|
||||
- 標題欄位是否已填寫
|
||||
- 網路連線是否正常
|
||||
- 是否有足夠的權限
|
||||
|
||||
**Q: Excel 匯入失敗?**
|
||||
**Q: 找不到之前建立的任務**
|
||||
A: 請嘗試:
|
||||
- 清除所有篩選條件
|
||||
- 使用搜尋功能
|
||||
- 檢查不同狀態分類
|
||||
|
||||
**Q: Excel 匯入失敗**
|
||||
A: 常見原因:
|
||||
1. 檔案格式不正確
|
||||
2. 必填欄位為空
|
||||
3. 日期格式錯誤
|
||||
4. 人員帳號不存在
|
||||
- 檔案格式不正確(請使用 .xlsx)
|
||||
- 必填欄位未填寫
|
||||
- 日期格式錯誤
|
||||
- 使用者帳號不存在
|
||||
|
||||
### ❓ 效能相關
|
||||
### 通知問題
|
||||
**Q: 收不到郵件通知**
|
||||
A: 請檢查:
|
||||
- 郵件地址是否正確
|
||||
- 垃圾郵件資料夾
|
||||
- 通知設定是否開啟
|
||||
|
||||
**Q: 系統載入很慢?**
|
||||
**Q: 通知太多怎麼辦?**
|
||||
A: 可以在通知設定中:
|
||||
- 關閉不需要的通知類型
|
||||
- 調整提醒時間
|
||||
- 設定免打擾時段
|
||||
|
||||
### 效能問題
|
||||
**Q: 系統載入很慢**
|
||||
A: 建議:
|
||||
1. 使用較新版本的瀏覽器
|
||||
2. 清除瀏覽器快取
|
||||
3. 檢查網路連線品質
|
||||
4. 關閉不必要的分頁
|
||||
- 清除瀏覽器快取
|
||||
- 使用較新版本的瀏覽器
|
||||
- 檢查網路連線品質
|
||||
|
||||
**Q: 行動裝置使用不順?**
|
||||
A: 建議:
|
||||
1. 使用手機瀏覽器而非APP
|
||||
2. 確保網路訊號穩定
|
||||
3. 定期清理瀏覽器快取
|
||||
|
||||
### ❓ 資料安全
|
||||
|
||||
**Q: 資料會不會外洩?**
|
||||
A: 系統安全措施:
|
||||
1. 資料存放於公司內部伺服器
|
||||
2. 使用 HTTPS 加密傳輸
|
||||
3. AD 統一身份驗證
|
||||
4. 定期備份和監控
|
||||
|
||||
**Q: 刪除的資料可以復原嗎?**
|
||||
A:
|
||||
1. 系統有30天的回收站功能
|
||||
2. 重要資料有定期備份
|
||||
3. 可聯繫管理員協助復原
|
||||
**Q: 手機上使用體驗不佳**
|
||||
A: 系統提供響應式設計:
|
||||
- 支援手機瀏覽器存取
|
||||
- 建議使用 Chrome 或 Safari
|
||||
- 確保瀏覽器為最新版本
|
||||
|
||||
---
|
||||
|
||||
## 🆘 技術支援
|
||||
### 問題回報
|
||||
回報問題時請提供:
|
||||
1. 問題發生時間
|
||||
2. 操作步驟描述
|
||||
3. 錯誤訊息截圖
|
||||
4. 使用的瀏覽器和版本
|
||||
5. 作業系統資訊
|
||||
|
||||
### 聯繫方式
|
||||
- **IT服務台**:分機 XXX
|
||||
- **Email**:it-support@panjit.com.tw
|
||||
- **內部聊天群**:IT技術支援群組
|
||||
|
||||
### 支援時間
|
||||
- **工作日**:09:00 - 18:00
|
||||
- **緊急問題**:24小時 (電話支援)
|
||||
- **系統維護時間**:每週日 02:00 - 04:00
|
||||
|
||||
### 提報問題時請提供
|
||||
1. **問題描述**:詳細說明遇到的狀況
|
||||
2. **操作步驟**:問題發生前的操作流程
|
||||
3. **錯誤訊息**:如有錯誤訊息請完整記錄
|
||||
4. **環境資訊**:使用的瀏覽器和作業系統
|
||||
5. **螢幕截圖**:有助於快速診斷問題
|
||||
### 建議與回饋
|
||||
歡迎提供系統改善建議:
|
||||
- 功能需求
|
||||
- 介面優化建議
|
||||
- 使用體驗改善
|
||||
|
||||
---
|
||||
|
||||
**使用者手冊版本**:1.0
|
||||
**最後更新**:2025年1月
|
||||
**適用系統版本**:PANJIT To-Do System v1.0
|
||||
## 📋 快速參考
|
||||
|
||||
### 鍵盤快捷鍵
|
||||
- `Ctrl + N`:建立新待辦事項
|
||||
- `Ctrl + F`:開啟搜尋
|
||||
- `Ctrl + /`:顯示快捷鍵說明
|
||||
- `Esc`:關閉對話框
|
||||
|
||||
### 狀態代碼
|
||||
- **NEW**:新建立
|
||||
- **DOING**:進行中
|
||||
- **BLOCKED**:已阻塞
|
||||
- **DONE**:已完成
|
||||
|
||||
### 優先級
|
||||
- **HIGH**:高優先級(紅色)
|
||||
- **MEDIUM**:中優先級(橙色)
|
||||
- **LOW**:低優先級(綠色)
|
||||
|
||||
如有任何問題或建議,歡迎隨時聯繫 IT 部門。
|
Reference in New Issue
Block a user