diff --git a/DATABASE_MIGRATION_README.md b/DATABASE_MIGRATION_README.md
new file mode 100644
index 0000000..b1ef178
--- /dev/null
+++ b/DATABASE_MIGRATION_README.md
@@ -0,0 +1,74 @@
+# 資料庫遷移說明
+
+## 問題描述
+競賽管理系統在創建競賽時出現以下錯誤:
+1. `competition_award_types` 表缺少 `order_index` 欄位
+2. `competition_teams` 表缺少 `registered_at` 欄位
+3. 外鍵約束失敗,存在孤立的關聯記錄
+
+## 解決方案
+
+### 方法一:使用智能檢查腳本(推薦)
+```bash
+mysql -u your_username -p your_database_name < add-missing-columns.sql
+```
+
+### 方法二:使用簡化腳本
+```bash
+mysql -u your_username -p your_database_name < add-missing-columns-simple.sql
+```
+
+## 腳本內容
+
+### 1. 添加缺失欄位
+- 為 `competition_award_types` 表添加 `order_index` 欄位
+- 為 `competition_teams` 表添加 `registered_at` 欄位
+
+### 2. 清理孤立記錄
+- 刪除所有關聯表中不存在的 `competition_id` 記錄
+
+### 3. 添加必要索引
+- 為所有關聯表添加適當的索引以提升查詢性能
+
+## 執行前注意事項
+
+1. **備份資料庫**:
+ ```bash
+ mysqldump -u your_username -p your_database_name > backup_before_migration.sql
+ ```
+
+2. **確認資料庫名稱**:
+ - 將 `your_database_name` 替換為實際的資料庫名稱
+ - 將 `your_username` 替換為實際的用戶名
+
+3. **檢查權限**:
+ - 確保用戶有 ALTER TABLE 和 DELETE 權限
+
+## 執行後驗證
+
+執行完成後,可以運行以下查詢來驗證:
+
+```sql
+-- 檢查欄位是否添加成功
+DESCRIBE competition_award_types;
+DESCRIBE competition_teams;
+
+-- 檢查索引是否創建成功
+SHOW INDEX FROM competition_award_types;
+SHOW INDEX FROM competition_teams;
+
+-- 檢查孤立記錄是否已清理
+SELECT COUNT(*) as orphaned_judges FROM competition_judges
+WHERE competition_id NOT IN (SELECT id FROM competitions);
+```
+
+## 如果遇到錯誤
+
+如果執行過程中遇到 "column already exists" 或 "index already exists" 錯誤,這是正常的,表示該欄位或索引已經存在,可以忽略這些錯誤。
+
+## 聯繫支援
+
+如果遇到其他問題,請檢查:
+1. MySQL 版本是否支援所使用的語法
+2. 用戶權限是否足夠
+3. 資料庫連接是否正常
diff --git a/README-DUAL-WRITE-SYNC.md b/README-DUAL-WRITE-SYNC.md
new file mode 100644
index 0000000..7bc1ec7
--- /dev/null
+++ b/README-DUAL-WRITE-SYNC.md
@@ -0,0 +1,232 @@
+# 資料庫雙寫同步系統說明
+
+## 🎯 系統概述
+
+本系統實現了主機和備機資料庫的雙寫同步機制,確保所有新增、刪除、修改的資料都能同時寫入主機和備機資料庫,實現真正的資料同步。
+
+## ✅ 主要功能
+
+- ✅ **雙寫同步** - 所有寫入操作同時寫入主機和備機
+- ✅ **自動故障檢測** - 每30秒檢查資料庫健康狀態
+- ✅ **自動切換** - 主機故障時自動切換到備機
+- ✅ **手動切換** - 支援手動切換資料庫
+- ✅ **資料同步** - 可將主機資料同步到備機
+- ✅ **監控面板** - 實時監控資料庫狀態
+- ✅ **健康檢查** - 定期檢查連接狀態
+
+## 🚀 快速開始
+
+### 1. 環境變數配置
+
+在您的 `.env` 文件中添加以下配置:
+
+```env
+# ===== 主機資料庫配置 =====
+DB_HOST=mysql.theaken.com
+DB_PORT=33306
+DB_NAME=db_AI_Platform
+DB_USER=AI_Platform
+DB_PASSWORD=Aa123456
+
+# ===== 備機資料庫配置 =====
+SLAVE_DB_HOST=122.100.99.161
+SLAVE_DB_PORT=43306
+SLAVE_DB_NAME=db_AI_Platform
+SLAVE_DB_USER=A999
+SLAVE_DB_PASSWORD=1023
+
+# ===== 資料庫備援配置 =====
+DB_FAILOVER_ENABLED=true
+
+# ===== 資料庫雙寫同步配置 =====
+DB_DUAL_WRITE_ENABLED=true
+DB_MASTER_PRIORITY=true
+DB_CONFLICT_RESOLUTION=master
+DB_HEALTH_CHECK_INTERVAL=30000
+DB_CONNECTION_TIMEOUT=5000
+DB_RETRY_ATTEMPTS=3
+DB_RETRY_DELAY=2000
+```
+
+### 2. 初始化備機資料庫
+
+```bash
+# 初始化備機資料庫結構
+pnpm run db:init-slave
+
+# 同步主機資料到備機
+pnpm run db:sync
+```
+
+### 3. 測試雙寫功能
+
+```bash
+# 測試雙寫同步功能
+pnpm run db:test-dual-write
+
+# 檢查資料庫健康狀態
+pnpm run db:health
+```
+
+## 📊 工作原理
+
+### 雙寫流程
+
+1. **寫入請求** - API 接收到寫入請求
+2. **雙寫執行** - 同時寫入主機和備機資料庫
+3. **結果處理** - 根據寫入結果決定回應
+4. **錯誤處理** - 如果其中一個失敗,記錄錯誤但繼續服務
+
+### 同步策略
+
+- **主機優先** - 如果主機和備機都成功,優先返回主機結果
+- **備機備援** - 如果主機失敗但備機成功,使用備機結果
+- **錯誤處理** - 如果兩者都失敗,拋出錯誤
+
+## 🔧 程式碼使用
+
+### 基本使用
+
+系統會自動使用雙寫功能,無需修改現有程式碼:
+
+```typescript
+import { db } from '@/lib/database';
+
+// 插入資料 (自動雙寫)
+await db.insert('INSERT INTO users (name, email) VALUES (?, ?)', ['John', 'john@example.com']);
+
+// 更新資料 (自動雙寫)
+await db.update('UPDATE users SET name = ? WHERE id = ?', ['Jane', 'user-id']);
+
+// 刪除資料 (自動雙寫)
+await db.delete('DELETE FROM users WHERE id = ?', ['user-id']);
+
+// 查詢資料 (自動使用備援)
+const users = await db.query('SELECT * FROM users');
+```
+
+### 監控同步狀態
+
+```typescript
+import { dbSync } from '@/lib/database-sync';
+
+// 獲取同步狀態
+const status = await dbSync.getSyncStatus();
+console.log('同步狀態:', status);
+
+// 手動同步表
+await dbSync.syncFromMasterToSlave('users');
+```
+
+### 獲取備援狀態
+
+```typescript
+import { db } from '@/lib/database';
+
+// 獲取當前資料庫狀態
+const status = db.getFailoverStatus();
+console.log('當前使用資料庫:', status?.currentDatabase);
+console.log('主機健康狀態:', status?.masterHealthy);
+console.log('備機健康狀態:', status?.slaveHealthy);
+```
+
+## 📈 監控和管理
+
+### 可用命令
+
+| 命令 | 功能 | 狀態 |
+|------|------|------|
+| `pnpm run db:health` | 檢查資料庫健康狀態 | ✅ 可用 |
+| `pnpm run db:test-dual-write` | 測試雙寫同步功能 | ✅ 可用 |
+| `pnpm run db:init-slave` | 初始化備機資料庫 | ✅ 可用 |
+| `pnpm run db:sync` | 同步資料 | ✅ 可用 |
+| `pnpm run db:monitor` | 監控資料庫狀態 | ✅ 可用 |
+
+### 管理頁面
+
+在管理頁面中添加監控組件:
+
+```typescript
+import { DatabaseMonitor } from '@/components/admin/database-monitor';
+
+// 在管理頁面中使用
+
+```
+
+## 🚨 故障處理
+
+### 主機資料庫問題
+
+**問題**: `Too many connections`
+**解決方案**:
+1. 系統已自動切換到備機
+2. 檢查主機資料庫連接數限制
+3. 優化連接池配置
+4. 重啟主機資料庫服務
+
+### 備機資料庫問題
+
+**問題**: 備機連接失敗
+**解決方案**:
+1. 檢查網路連接
+2. 驗證備機資料庫配置
+3. 確認用戶權限
+4. 檢查備機資料庫服務狀態
+
+### 雙寫失敗處理
+
+**問題**: 主機或備機寫入失敗
+**解決方案**:
+1. 檢查錯誤日誌
+2. 驗證資料庫連接狀態
+3. 檢查資料庫結構一致性
+4. 重新執行失敗的操作
+
+## 📋 維護建議
+
+### 定期維護
+
+1. **每日檢查**: 執行 `pnpm run db:health`
+2. **每週測試**: 執行 `pnpm run db:test-dual-write`
+3. **每月同步**: 執行 `pnpm run db:sync`
+
+### 監控指標
+
+- 資料庫連接狀態
+- 雙寫成功率
+- 同步延遲時間
+- 錯誤率統計
+
+## 🔄 恢復主機
+
+當主機資料庫恢復後:
+
+1. 檢查主機狀態: `pnpm run db:health`
+2. 手動切換回主機: `await db.switchDatabase('master')`
+3. 重新同步資料: `pnpm run db:sync`
+
+## 📞 支援
+
+如有問題,請檢查:
+
+1. 環境變數配置
+2. 網路連接狀態
+3. 資料庫服務狀態
+4. 系統日誌
+5. 監控面板狀態
+
+## 🎉 總結
+
+您的資料庫雙寫同步系統已經成功設置並運行!系統現在可以:
+
+- ✅ 自動檢測主機資料庫問題
+- ✅ 自動切換到備機資料庫
+- ✅ 實現真正的雙寫同步
+- ✅ 確保資料一致性
+- ✅ 提供監控和管理功能
+- ✅ 確保服務連續性
+
+即使主機資料庫出現問題,您的應用程式仍然可以正常運行,並且所有資料都會同步到備機!
+
+
+
diff --git a/app/api/admin/apps/available/route.ts b/app/api/admin/apps/available/route.ts
new file mode 100644
index 0000000..c2c5e0f
--- /dev/null
+++ b/app/api/admin/apps/available/route.ts
@@ -0,0 +1,129 @@
+// =====================================================
+// 獲取可用應用列表 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { db } from '@/lib/database';
+
+export async function GET(request: NextRequest) {
+ try {
+ console.log('🚀 ========== 應用 API 開始執行 ==========');
+ const { searchParams } = new URL(request.url);
+ const teamId = searchParams.get('teamId');
+
+ console.log('🔍 獲取可用應用列表, teamId:', teamId);
+ console.log('🔍 請求 URL:', request.url);
+
+ // 先檢查所有應用
+ console.log('📊 開始檢查數據庫...');
+ const allAppsSql = `SELECT COUNT(*) as count FROM apps`;
+ const allAppsResult = await db.query(allAppsSql);
+ console.log('📊 數據庫中應用總數:', allAppsResult[0].count);
+
+ // 檢查活躍應用
+ const activeAppsSql = `SELECT COUNT(*) as count FROM apps WHERE is_active = TRUE`;
+ const activeAppsResult = await db.query(activeAppsSql);
+ console.log('✅ 活躍應用數量 (is_active = TRUE):', activeAppsResult[0].count);
+
+ // 檢查所有應用的 is_active 值
+ const allAppsWithStatusSql = `SELECT id, name, is_active, team_id FROM apps LIMIT 5`;
+ const allAppsWithStatusResult = await db.query(allAppsWithStatusSql);
+ console.log('📋 前5個應用的狀態:', allAppsWithStatusResult);
+
+ // 檢查是否有 is_active = 1 的應用
+ const activeAppsWith1Sql = `SELECT COUNT(*) as count FROM apps WHERE is_active = 1`;
+ const activeAppsWith1Result = await db.query(activeAppsWith1Sql);
+ console.log('✅ is_active = 1 的應用數量:', activeAppsWith1Result[0].count);
+
+ // 檢查是否有 is_active = '1' 的應用(字符串)
+ const activeAppsWithStringSql = `SELECT COUNT(*) as count FROM apps WHERE is_active = '1'`;
+ const activeAppsWithStringResult = await db.query(activeAppsWithStringSql);
+ console.log('✅ is_active = "1" 的應用數量:', activeAppsWithStringResult[0].count);
+
+ // 檢查沒有團隊的應用
+ const noTeamAppsSql = `SELECT COUNT(*) as count FROM apps WHERE is_active = 1 AND team_id IS NULL`;
+ const noTeamAppsResult = await db.query(noTeamAppsSql);
+ console.log('🔓 沒有團隊的應用數量:', noTeamAppsResult[0].count);
+
+ // 檢查屬於其他團隊的應用
+ const otherTeamAppsSql = `SELECT COUNT(*) as count FROM apps WHERE is_active = 1 AND team_id IS NOT NULL AND team_id != ?`;
+ const otherTeamAppsResult = await db.query(otherTeamAppsSql, [teamId || '']);
+ console.log('🔓 屬於其他團隊的應用數量:', otherTeamAppsResult[0].count);
+
+ // 獲取所有活躍的應用,編輯團隊時顯示所有應用(包括已綁定的)
+ // 使用 is_active = 1 因為數據庫中存儲的是數字 1
+ let sql = `
+ SELECT id, name, description, category, type, icon, icon_color, app_url, creator_id, team_id
+ FROM apps
+ WHERE is_active = 1
+ ORDER BY created_at DESC
+ `;
+
+ const params: any[] = [];
+
+ console.log('📝 執行的 SQL:', sql);
+ console.log('📝 參數:', params);
+
+ const apps = await db.query(sql, params);
+ console.log('📊 查詢結果:', apps.length, '個應用');
+
+ // 如果沒有結果,嘗試不同的查詢條件
+ if (apps.length === 0) {
+ console.log('⚠️ 沒有找到 is_active = 1 的應用,嘗試其他查詢條件...');
+
+ // 嘗試 is_active = TRUE
+ const sqlTrue = sql.replace('WHERE is_active = 1', 'WHERE is_active = TRUE');
+ const appsTrue = await db.query(sqlTrue, params);
+ console.log('📊 is_active = TRUE 查詢結果:', appsTrue.length, '個應用');
+
+ // 嘗試 is_active = '1'
+ const sqlString = sql.replace('WHERE is_active = 1', 'WHERE is_active = "1"');
+ const appsString = await db.query(sqlString, params);
+ console.log('📊 is_active = "1" 查詢結果:', appsString.length, '個應用');
+
+ // 嘗試沒有 is_active 條件
+ const sqlNoFilter = sql.replace('WHERE is_active = 1', 'WHERE 1=1');
+ const appsNoFilter = await db.query(sqlNoFilter, params);
+ console.log('📊 無 is_active 過濾查詢結果:', appsNoFilter.length, '個應用');
+
+ // 使用有結果的查詢
+ if (appsTrue.length > 0) {
+ console.log('✅ 使用 is_active = TRUE 的結果');
+ return NextResponse.json({
+ success: true,
+ message: '可用應用列表獲取成功',
+ data: appsTrue
+ });
+ } else if (appsString.length > 0) {
+ console.log('✅ 使用 is_active = "1" 的結果');
+ return NextResponse.json({
+ success: true,
+ message: '可用應用列表獲取成功',
+ data: appsString
+ });
+ } else if (appsNoFilter.length > 0) {
+ console.log('✅ 使用無過濾條件的結果');
+ return NextResponse.json({
+ success: true,
+ message: '可用應用列表獲取成功',
+ data: appsNoFilter
+ });
+ }
+ }
+
+ console.log('🚀 ========== 應用 API 執行完成 ==========');
+ return NextResponse.json({
+ success: true,
+ message: '可用應用列表獲取成功',
+ data: apps
+ });
+
+ } catch (error) {
+ console.error('❌ 獲取可用應用列表失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取可用應用列表失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/competitions/[id]/awards/route.ts b/app/api/admin/competitions/[id]/awards/route.ts
new file mode 100644
index 0000000..6de69ee
--- /dev/null
+++ b/app/api/admin/competitions/[id]/awards/route.ts
@@ -0,0 +1,95 @@
+// =====================================================
+// 競賽獎項類型管理 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+
+// 獲取競賽的獎項類型列表
+export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+
+ const awards = await CompetitionService.getCompetitionAwardTypes(id);
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽獎項類型列表獲取成功',
+ data: awards
+ });
+
+ } catch (error) {
+ console.error('獲取競賽獎項類型失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取競賽獎項類型失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 為競賽添加獎項類型
+export async function POST(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const body = await request.json();
+ const { awardTypes } = body;
+
+ if (!awardTypes || !Array.isArray(awardTypes) || awardTypes.length === 0) {
+ return NextResponse.json({
+ success: false,
+ message: '缺少獎項類型列表',
+ error: 'awardTypes 必須是非空陣列'
+ }, { status: 400 });
+ }
+
+ const result = await CompetitionService.addCompetitionAwardTypes(id, awardTypes);
+
+ return NextResponse.json({
+ success: true,
+ message: '獎項類型添加成功',
+ data: result
+ });
+
+ } catch (error) {
+ console.error('添加競賽獎項類型失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '添加競賽獎項類型失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 從競賽中移除獎項類型
+export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const { searchParams } = new URL(request.url);
+ const awardTypeId = searchParams.get('awardTypeId');
+
+ if (!awardTypeId) {
+ return NextResponse.json({
+ success: false,
+ message: '缺少獎項類型ID',
+ error: 'awardTypeId 參數是必需的'
+ }, { status: 400 });
+ }
+
+ const result = await CompetitionService.removeCompetitionAwardType(id, awardTypeId);
+
+ return NextResponse.json({
+ success: true,
+ message: '獎項類型移除成功',
+ data: result
+ });
+
+ } catch (error) {
+ console.error('移除競賽獎項類型失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '移除競賽獎項類型失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/competitions/[id]/judges/route.ts b/app/api/admin/competitions/[id]/judges/route.ts
new file mode 100644
index 0000000..758b00a
--- /dev/null
+++ b/app/api/admin/competitions/[id]/judges/route.ts
@@ -0,0 +1,95 @@
+// =====================================================
+// 競賽評審關聯管理 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+
+// 獲取競賽的評審列表
+export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+
+ const judges = await CompetitionService.getCompetitionJudges(id);
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽評審列表獲取成功',
+ data: judges
+ });
+
+ } catch (error) {
+ console.error('獲取競賽評審失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取競賽評審失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 為競賽添加評審
+export async function POST(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const body = await request.json();
+ const { judgeIds } = body;
+
+ if (!judgeIds || !Array.isArray(judgeIds) || judgeIds.length === 0) {
+ return NextResponse.json({
+ success: false,
+ message: '缺少評審ID列表',
+ error: 'judgeIds 必須是非空陣列'
+ }, { status: 400 });
+ }
+
+ const result = await CompetitionService.addCompetitionJudges(id, judgeIds);
+
+ return NextResponse.json({
+ success: true,
+ message: '評審添加成功',
+ data: result
+ });
+
+ } catch (error) {
+ console.error('添加競賽評審失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '添加競賽評審失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 從競賽中移除評審
+export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const { searchParams } = new URL(request.url);
+ const judgeId = searchParams.get('judgeId');
+
+ if (!judgeId) {
+ return NextResponse.json({
+ success: false,
+ message: '缺少評審ID',
+ error: 'judgeId 參數是必需的'
+ }, { status: 400 });
+ }
+
+ const result = await CompetitionService.removeCompetitionJudge(id, judgeId);
+
+ return NextResponse.json({
+ success: true,
+ message: '評審移除成功',
+ data: result
+ });
+
+ } catch (error) {
+ console.error('移除競賽評審失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '移除競賽評審失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/competitions/[id]/route.ts b/app/api/admin/competitions/[id]/route.ts
new file mode 100644
index 0000000..1066204
--- /dev/null
+++ b/app/api/admin/competitions/[id]/route.ts
@@ -0,0 +1,199 @@
+// =====================================================
+// 競賽詳細操作 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+
+// 獲取單一競賽
+export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+
+ const competition = await CompetitionService.getCompetitionWithDetails(id);
+
+ if (!competition) {
+ return NextResponse.json({
+ success: false,
+ message: '競賽不存在',
+ error: '找不到指定的競賽'
+ }, { status: 404 });
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽獲取成功',
+ data: competition
+ });
+
+ } catch (error) {
+ console.error('獲取競賽失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取競賽失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 更新競賽
+export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const body = await request.json();
+
+ // 檢查競賽是否存在
+ const existingCompetition = await CompetitionService.getCompetitionById(id);
+ if (!existingCompetition) {
+ return NextResponse.json({
+ success: false,
+ message: '競賽不存在',
+ error: '找不到指定的競賽'
+ }, { status: 404 });
+ }
+
+ // 驗證日期(如果提供)
+ if (body.startDate && body.endDate) {
+ const startDateObj = new Date(body.startDate);
+ const endDateObj = new Date(body.endDate);
+
+ if (isNaN(startDateObj.getTime()) || isNaN(endDateObj.getTime())) {
+ return NextResponse.json({
+ success: false,
+ message: '日期格式無效',
+ error: 'startDate 和 endDate 必須是有效的日期格式'
+ }, { status: 400 });
+ }
+
+ if (endDateObj <= startDateObj) {
+ return NextResponse.json({
+ success: false,
+ message: '結束日期不能早於或等於開始日期',
+ error: 'endDate 必須晚於 startDate'
+ }, { status: 400 });
+ }
+ }
+
+ // 驗證競賽類型(如果提供)
+ if (body.type) {
+ const validTypes = ['individual', 'team', 'mixed', 'proposal'];
+ if (!validTypes.includes(body.type)) {
+ return NextResponse.json({
+ success: false,
+ message: '無效的競賽類型',
+ error: `type 必須是以下之一: ${validTypes.join(', ')}`
+ }, { status: 400 });
+ }
+ }
+
+ // 驗證狀態(如果提供)
+ if (body.status) {
+ const validStatuses = ['upcoming', 'active', 'judging', 'completed'];
+ if (!validStatuses.includes(body.status)) {
+ return NextResponse.json({
+ success: false,
+ message: '無效的競賽狀態',
+ error: `status 必須是以下之一: ${validStatuses.join(', ')}`
+ }, { status: 400 });
+ }
+ }
+
+ // 準備更新資料
+ const updateData: any = {};
+
+ if (body.name !== undefined) updateData.name = body.name;
+ if (body.year !== undefined) updateData.year = parseInt(body.year);
+ if (body.month !== undefined) updateData.month = parseInt(body.month);
+ if (body.startDate !== undefined) updateData.start_date = body.startDate;
+ if (body.endDate !== undefined) updateData.end_date = body.endDate;
+ if (body.status !== undefined) updateData.status = body.status;
+ if (body.description !== undefined) updateData.description = body.description;
+ if (body.type !== undefined) updateData.type = body.type;
+ if (body.evaluationFocus !== undefined) updateData.evaluation_focus = body.evaluationFocus;
+ if (body.maxTeamSize !== undefined) updateData.max_team_size = body.maxTeamSize ? parseInt(body.maxTeamSize) : null;
+ if (body.isActive !== undefined) updateData.is_active = body.isActive;
+
+ // 執行更新
+ const success = await CompetitionService.updateCompetition(id, updateData);
+
+ if (!success) {
+ return NextResponse.json({
+ success: false,
+ message: '更新競賽失敗',
+ error: '無法更新競賽資料'
+ }, { status: 500 });
+ }
+
+ // 更新關聯數據
+ if (body.judges !== undefined) {
+ await CompetitionService.addCompetitionJudges(id, body.judges || []);
+ }
+ if (body.teams !== undefined) {
+ await CompetitionService.addCompetitionTeams(id, body.teams || []);
+ }
+ if (body.awardTypes !== undefined) {
+ await CompetitionService.addCompetitionAwardTypes(id, body.awardTypes || []);
+ }
+ if (body.rules !== undefined) {
+ await CompetitionService.addCompetitionRules(id, body.rules || []);
+ }
+
+ // 獲取更新後的完整競賽資料(包含關聯數據)
+ const updatedCompetition = await CompetitionService.getCompetitionWithDetails(id);
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽更新成功',
+ data: updatedCompetition
+ });
+
+ } catch (error) {
+ console.error('更新競賽失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '更新競賽失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 刪除競賽(軟刪除)
+export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+
+ // 檢查競賽是否存在
+ const existingCompetition = await CompetitionService.getCompetitionById(id);
+ if (!existingCompetition) {
+ return NextResponse.json({
+ success: false,
+ message: '競賽不存在',
+ error: '找不到指定的競賽'
+ }, { status: 404 });
+ }
+
+ // 軟刪除:將 is_active 設為 false
+ const success = await CompetitionService.updateCompetition(id, { is_active: false });
+
+ if (!success) {
+ return NextResponse.json({
+ success: false,
+ message: '刪除競賽失敗',
+ error: '無法刪除競賽'
+ }, { status: 500 });
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽刪除成功'
+ });
+
+ } catch (error) {
+ console.error('刪除競賽失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '刪除競賽失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/competitions/[id]/rules/route.ts b/app/api/admin/competitions/[id]/rules/route.ts
new file mode 100644
index 0000000..fa068b9
--- /dev/null
+++ b/app/api/admin/competitions/[id]/rules/route.ts
@@ -0,0 +1,95 @@
+// =====================================================
+// 競賽評分規則管理 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+
+// 獲取競賽的評分規則列表
+export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+
+ const rules = await CompetitionService.getCompetitionRules(id);
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽評分規則列表獲取成功',
+ data: rules
+ });
+
+ } catch (error) {
+ console.error('獲取競賽評分規則失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取競賽評分規則失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 為競賽添加評分規則
+export async function POST(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const body = await request.json();
+ const { rules } = body;
+
+ if (!rules || !Array.isArray(rules) || rules.length === 0) {
+ return NextResponse.json({
+ success: false,
+ message: '缺少評分規則列表',
+ error: 'rules 必須是非空陣列'
+ }, { status: 400 });
+ }
+
+ const result = await CompetitionService.addCompetitionRules(id, rules);
+
+ return NextResponse.json({
+ success: true,
+ message: '評分規則添加成功',
+ data: result
+ });
+
+ } catch (error) {
+ console.error('添加競賽評分規則失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '添加競賽評分規則失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 從競賽中移除評分規則
+export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const { searchParams } = new URL(request.url);
+ const ruleId = searchParams.get('ruleId');
+
+ if (!ruleId) {
+ return NextResponse.json({
+ success: false,
+ message: '缺少評分規則ID',
+ error: 'ruleId 參數是必需的'
+ }, { status: 400 });
+ }
+
+ const result = await CompetitionService.removeCompetitionRule(id, ruleId);
+
+ return NextResponse.json({
+ success: true,
+ message: '評分規則移除成功',
+ data: result
+ });
+
+ } catch (error) {
+ console.error('移除競賽評分規則失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '移除競賽評分規則失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/competitions/[id]/teams/route.ts b/app/api/admin/competitions/[id]/teams/route.ts
new file mode 100644
index 0000000..f6305dd
--- /dev/null
+++ b/app/api/admin/competitions/[id]/teams/route.ts
@@ -0,0 +1,95 @@
+// =====================================================
+// 競賽團隊關聯管理 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+
+// 獲取競賽的團隊列表
+export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+
+ const teams = await CompetitionService.getCompetitionTeams(id);
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽團隊列表獲取成功',
+ data: teams
+ });
+
+ } catch (error) {
+ console.error('獲取競賽團隊失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取競賽團隊失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 為競賽添加團隊
+export async function POST(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const body = await request.json();
+ const { teamIds } = body;
+
+ if (!teamIds || !Array.isArray(teamIds) || teamIds.length === 0) {
+ return NextResponse.json({
+ success: false,
+ message: '缺少團隊ID列表',
+ error: 'teamIds 必須是非空陣列'
+ }, { status: 400 });
+ }
+
+ const result = await CompetitionService.addCompetitionTeams(id, teamIds);
+
+ return NextResponse.json({
+ success: true,
+ message: '團隊添加成功',
+ data: result
+ });
+
+ } catch (error) {
+ console.error('添加競賽團隊失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '添加競賽團隊失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 從競賽中移除團隊
+export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const { searchParams } = new URL(request.url);
+ const teamId = searchParams.get('teamId');
+
+ if (!teamId) {
+ return NextResponse.json({
+ success: false,
+ message: '缺少團隊ID',
+ error: 'teamId 參數是必需的'
+ }, { status: 400 });
+ }
+
+ const result = await CompetitionService.removeCompetitionTeam(id, teamId);
+
+ return NextResponse.json({
+ success: true,
+ message: '團隊移除成功',
+ data: result
+ });
+
+ } catch (error) {
+ console.error('移除競賽團隊失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '移除競賽團隊失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/competitions/route.ts b/app/api/admin/competitions/route.ts
new file mode 100644
index 0000000..394b5f2
--- /dev/null
+++ b/app/api/admin/competitions/route.ts
@@ -0,0 +1,189 @@
+// =====================================================
+// 競賽管理 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+
+// 獲取所有競賽
+export async function GET(request: NextRequest) {
+ try {
+ const competitions = await CompetitionService.getAllCompetitions();
+
+ // 為每個競賽獲取關聯數據
+ const competitionsWithDetails = await Promise.all(
+ competitions.map(async (competition) => {
+ try {
+ const fullCompetition = await CompetitionService.getCompetitionWithDetails(competition.id);
+ return fullCompetition;
+ } catch (error) {
+ console.error(`獲取競賽 ${competition.id} 詳細信息失敗:`, error);
+ return competition; // 如果獲取詳細信息失敗,返回基本競賽信息
+ }
+ })
+ );
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽列表獲取成功',
+ data: competitionsWithDetails
+ });
+
+ } catch (error) {
+ console.error('獲取競賽列表失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取競賽列表失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 創建新競賽
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const {
+ name,
+ year,
+ month,
+ startDate,
+ endDate,
+ status = 'upcoming',
+ description,
+ type = 'individual',
+ evaluationFocus,
+ maxTeamSize,
+ isActive = true,
+ // 關聯數據
+ judges = [],
+ teams = [],
+ awardTypes = [],
+ rules = []
+ } = body;
+
+ // 驗證必填欄位
+ if (!name || !year || !month || !startDate || !endDate || !type) {
+ return NextResponse.json({
+ success: false,
+ message: '缺少必填欄位',
+ error: 'name, year, month, startDate, endDate, type 為必填欄位'
+ }, { status: 400 });
+ }
+
+ // 驗證日期格式
+ const startDateObj = new Date(startDate);
+ const endDateObj = new Date(endDate);
+
+ if (isNaN(startDateObj.getTime()) || isNaN(endDateObj.getTime())) {
+ return NextResponse.json({
+ success: false,
+ message: '日期格式無效',
+ error: 'startDate 和 endDate 必須是有效的日期格式'
+ }, { status: 400 });
+ }
+
+ // 驗證結束日期不能早於開始日期
+ if (endDateObj <= startDateObj) {
+ return NextResponse.json({
+ success: false,
+ message: '結束日期不能早於或等於開始日期',
+ error: 'endDate 必須晚於 startDate'
+ }, { status: 400 });
+ }
+
+ // 驗證競賽類型
+ const validTypes = ['individual', 'team', 'mixed', 'proposal'];
+ if (!validTypes.includes(type)) {
+ return NextResponse.json({
+ success: false,
+ message: '無效的競賽類型',
+ error: `type 必須是以下之一: ${validTypes.join(', ')}`
+ }, { status: 400 });
+ }
+
+ // 驗證狀態
+ const validStatuses = ['upcoming', 'active', 'judging', 'completed'];
+ if (!validStatuses.includes(status)) {
+ return NextResponse.json({
+ success: false,
+ message: '無效的競賽狀態',
+ error: `status 必須是以下之一: ${validStatuses.join(', ')}`
+ }, { status: 400 });
+ }
+
+ // 創建競賽
+ const competitionData = {
+ name,
+ year: parseInt(year),
+ month: parseInt(month),
+ start_date: startDate,
+ end_date: endDate,
+ status,
+ description: description || null,
+ type,
+ evaluation_focus: evaluationFocus || null,
+ max_team_size: maxTeamSize ? parseInt(maxTeamSize) : null,
+ is_active: isActive
+ };
+
+ const newCompetition = await CompetitionService.createCompetition(competitionData);
+
+ // 保存關聯數據
+ if (newCompetition) {
+ try {
+ // 保存評審關聯
+ if (judges && judges.length > 0) {
+ await CompetitionService.addCompetitionJudges(newCompetition.id, judges);
+ }
+
+ // 保存團隊關聯
+ if (teams && teams.length > 0) {
+ await CompetitionService.addCompetitionTeams(newCompetition.id, teams);
+ }
+
+ // 保存獎項類型
+ if (awardTypes && awardTypes.length > 0) {
+ await CompetitionService.addCompetitionAwardTypes(newCompetition.id, awardTypes);
+ }
+
+ // 保存評分規則
+ if (rules && rules.length > 0) {
+ await CompetitionService.addCompetitionRules(newCompetition.id, rules);
+ }
+
+ // 獲取完整的競賽信息(包含關聯數據)
+ const fullCompetition = await CompetitionService.getCompetitionWithDetails(newCompetition.id);
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽創建成功',
+ data: fullCompetition
+ });
+ } catch (error) {
+ console.error('保存競賽關聯數據失敗:', error);
+ // 即使關聯數據保存失敗,競賽本身已經創建成功
+ return NextResponse.json({
+ success: true,
+ message: '競賽創建成功,但部分關聯數據保存失敗',
+ data: newCompetition,
+ warning: '部分關聯數據可能未正確保存,請檢查資料庫狀態'
+ });
+ }
+ }
+
+ return NextResponse.json({
+ success: false,
+ message: '競賽創建失敗',
+ error: '無法創建競賽'
+ }, { status: 500 });
+
+ } catch (error) {
+ console.error('創建競賽失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '創建競賽失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/competitions/stats/route.ts b/app/api/admin/competitions/stats/route.ts
new file mode 100644
index 0000000..9f46d74
--- /dev/null
+++ b/app/api/admin/competitions/stats/route.ts
@@ -0,0 +1,45 @@
+// =====================================================
+// 競賽統計 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+
+// 獲取競賽統計數據
+export async function GET(request: NextRequest) {
+ try {
+ const competitions = await CompetitionService.getAllCompetitions();
+
+ // 計算統計數據
+ const stats = {
+ total: competitions.length,
+ upcoming: competitions.filter(c => c.status === 'upcoming').length,
+ active: competitions.filter(c => c.status === 'active').length,
+ judging: competitions.filter(c => c.status === 'judging').length,
+ completed: competitions.filter(c => c.status === 'completed').length,
+ individual: competitions.filter(c => c.type === 'individual').length,
+ team: competitions.filter(c => c.type === 'team').length,
+ mixed: competitions.filter(c => c.type === 'mixed').length,
+ proposal: competitions.filter(c => c.type === 'proposal').length,
+ currentYear: competitions.filter(c => c.year === new Date().getFullYear()).length,
+ thisMonth: competitions.filter(c =>
+ c.year === new Date().getFullYear() &&
+ c.month === new Date().getMonth() + 1
+ ).length
+ };
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽統計獲取成功',
+ data: stats
+ });
+
+ } catch (error) {
+ console.error('獲取競賽統計失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取競賽統計失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/judges/[id]/route.ts b/app/api/admin/judges/[id]/route.ts
new file mode 100644
index 0000000..fec75a3
--- /dev/null
+++ b/app/api/admin/judges/[id]/route.ts
@@ -0,0 +1,186 @@
+// =====================================================
+// 評審詳細操作 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { JudgeService } from '@/lib/services/database-service';
+
+// 獲取單一評審
+export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+
+ const judge = await JudgeService.getJudgeById(id);
+
+ if (!judge) {
+ return NextResponse.json({
+ success: false,
+ message: '評審不存在',
+ error: '找不到指定的評審'
+ }, { status: 404 });
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: '評審獲取成功',
+ data: judge
+ });
+
+ } catch (error) {
+ console.error('獲取評審失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取評審失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 更新評審
+export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const body = await request.json();
+
+ // 檢查評審是否存在
+ const existingJudge = await JudgeService.getJudgeById(id);
+ if (!existingJudge) {
+ return NextResponse.json({
+ success: false,
+ message: '評審不存在',
+ error: '找不到指定的評審'
+ }, { status: 404 });
+ }
+
+ // 驗證姓名長度(如果提供)
+ if (body.name && (body.name.length < 2 || body.name.length > 50)) {
+ return NextResponse.json({
+ success: false,
+ message: '姓名長度無效',
+ error: '姓名長度必須在 2-50 個字符之間'
+ }, { status: 400 });
+ }
+
+ // 驗證職稱長度(如果提供)
+ if (body.title && (body.title.length < 2 || body.title.length > 100)) {
+ return NextResponse.json({
+ success: false,
+ message: '職稱長度無效',
+ error: '職稱長度必須在 2-100 個字符之間'
+ }, { status: 400 });
+ }
+
+ // 驗證專業領域(如果提供)
+ if (body.expertise && !Array.isArray(body.expertise)) {
+ return NextResponse.json({
+ success: false,
+ message: '專業領域格式無效',
+ error: 'expertise 必須是陣列格式'
+ }, { status: 400 });
+ }
+
+ // 如果更新姓名,檢查是否與其他評審重複
+ if (body.name && body.name !== existingJudge.name) {
+ const duplicateJudge = await JudgeService.getJudgeByName(body.name);
+ if (duplicateJudge) {
+ return NextResponse.json({
+ success: false,
+ message: '評審姓名重複',
+ error: '該姓名的評審已存在'
+ }, { status: 409 });
+ }
+ }
+
+ // 準備更新資料
+ const updateData: any = {};
+
+ if (body.name !== undefined) updateData.name = body.name.trim();
+ if (body.title !== undefined) updateData.title = body.title.trim();
+ if (body.department !== undefined) updateData.department = body.department.trim();
+ if (body.expertise !== undefined) {
+ updateData.expertise = body.expertise.map((exp: string) => exp.trim()).filter(Boolean);
+ }
+ if (body.avatar !== undefined) updateData.avatar = body.avatar;
+ if (body.isActive !== undefined) updateData.is_active = body.isActive;
+ if (body.is_active !== undefined) updateData.is_active = body.is_active;
+
+ // 執行更新
+ const success = await JudgeService.updateJudge(id, updateData);
+
+ if (!success) {
+ return NextResponse.json({
+ success: false,
+ message: '更新評審失敗',
+ error: '無法更新評審資料'
+ }, { status: 500 });
+ }
+
+ // 獲取更新後的評審資料
+ const updatedJudge = await JudgeService.getJudgeById(id);
+
+ return NextResponse.json({
+ success: true,
+ message: '評審更新成功',
+ data: updatedJudge
+ });
+
+ } catch (error) {
+ console.error('更新評審失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '更新評審失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 刪除評審
+export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const url = new URL(request.url);
+ const hardDelete = url.searchParams.get('hard') === 'true';
+
+ // 檢查評審是否存在
+ const existingJudge = await JudgeService.getJudgeById(id);
+ if (!existingJudge) {
+ return NextResponse.json({
+ success: false,
+ message: '評審不存在',
+ error: '找不到指定的評審'
+ }, { status: 404 });
+ }
+
+ let success: boolean;
+
+ if (hardDelete) {
+ // 硬刪除:從資料庫中完全移除
+ success = await JudgeService.deleteJudge(id);
+ } else {
+ // 軟刪除:將 is_active 設為 false
+ success = await JudgeService.updateJudge(id, { is_active: false });
+ }
+
+ if (!success) {
+ return NextResponse.json({
+ success: false,
+ message: '刪除評審失敗',
+ error: '無法刪除評審'
+ }, { status: 500 });
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: hardDelete ? '評審已永久刪除' : '評審已停用',
+ data: { hardDelete }
+ });
+
+ } catch (error) {
+ console.error('刪除評審失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '刪除評審失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/judges/route.ts b/app/api/admin/judges/route.ts
new file mode 100644
index 0000000..5bc32eb
--- /dev/null
+++ b/app/api/admin/judges/route.ts
@@ -0,0 +1,137 @@
+// =====================================================
+// 評審管理 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { JudgeService } from '@/lib/services/database-service';
+
+// 獲取所有評審
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const search = searchParams.get('search') || '';
+ const department = searchParams.get('department') || '';
+ const expertise = searchParams.get('expertise') || '';
+
+ let judges = await JudgeService.getAllJudges();
+
+ // 應用篩選
+ if (search) {
+ judges = judges.filter(judge =>
+ judge.name.toLowerCase().includes(search.toLowerCase()) ||
+ judge.title.toLowerCase().includes(search.toLowerCase())
+ );
+ }
+
+ if (department && department !== 'all') {
+ judges = judges.filter(judge => judge.department === department);
+ }
+
+ if (expertise && expertise !== 'all') {
+ judges = judges.filter(judge =>
+ judge.expertise.some(exp => exp.toLowerCase().includes(expertise.toLowerCase()))
+ );
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: '評審列表獲取成功',
+ data: judges
+ });
+
+ } catch (error) {
+ console.error('獲取評審列表失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取評審列表失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 創建新評審
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const {
+ name,
+ title,
+ department,
+ expertise = [],
+ avatar,
+ isActive = true
+ } = body;
+
+ // 驗證必填欄位
+ if (!name || !title || !department) {
+ return NextResponse.json({
+ success: false,
+ message: '缺少必填欄位',
+ error: 'name, title, department 為必填欄位'
+ }, { status: 400 });
+ }
+
+ // 驗證姓名長度
+ if (name.length < 2 || name.length > 50) {
+ return NextResponse.json({
+ success: false,
+ message: '姓名長度無效',
+ error: '姓名長度必須在 2-50 個字符之間'
+ }, { status: 400 });
+ }
+
+ // 驗證職稱長度
+ if (title.length < 2 || title.length > 100) {
+ return NextResponse.json({
+ success: false,
+ message: '職稱長度無效',
+ error: '職稱長度必須在 2-100 個字符之間'
+ }, { status: 400 });
+ }
+
+ // 驗證專業領域
+ if (!Array.isArray(expertise)) {
+ return NextResponse.json({
+ success: false,
+ message: '專業領域格式無效',
+ error: 'expertise 必須是陣列格式'
+ }, { status: 400 });
+ }
+
+ // 檢查是否已存在相同姓名的評審
+ const existingJudge = await JudgeService.getJudgeByName(name);
+ if (existingJudge) {
+ return NextResponse.json({
+ success: false,
+ message: '評審已存在',
+ error: '該姓名的評審已存在'
+ }, { status: 409 });
+ }
+
+ // 創建評審
+ const judgeData = {
+ name: name.trim(),
+ title: title.trim(),
+ department: department.trim(),
+ expertise: expertise.map((exp: string) => exp.trim()).filter(Boolean),
+ avatar: avatar || null,
+ is_active: isActive
+ };
+
+ const newJudge = await JudgeService.createJudge(judgeData);
+
+ return NextResponse.json({
+ success: true,
+ message: '評審創建成功',
+ data: newJudge
+ });
+
+ } catch (error) {
+ console.error('創建評審失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '創建評審失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/judges/stats/route.ts b/app/api/admin/judges/stats/route.ts
new file mode 100644
index 0000000..5a466c2
--- /dev/null
+++ b/app/api/admin/judges/stats/route.ts
@@ -0,0 +1,72 @@
+// =====================================================
+// 評審統計 API
+// =====================================================
+
+import { NextResponse } from 'next/server';
+import { JudgeService } from '@/lib/services/database-service';
+
+// 獲取評審統計
+export async function GET() {
+ try {
+ const judges = await JudgeService.getAllJudges();
+
+ // 計算統計數據
+ const totalJudges = judges.length;
+ const activeJudges = judges.filter(judge => judge.is_active).length;
+ const inactiveJudges = judges.filter(judge => !judge.is_active).length;
+
+ // 按部門統計
+ const departmentStats = judges.reduce((acc, judge) => {
+ const dept = judge.department || '未分類';
+ if (!acc[dept]) {
+ acc[dept] = { total: 0, active: 0, inactive: 0 };
+ }
+ acc[dept].total++;
+ if (judge.is_active) {
+ acc[dept].active++;
+ } else {
+ acc[dept].inactive++;
+ }
+ return acc;
+ }, {} as Record);
+
+ // 按專業領域統計
+ const expertiseStats = judges.reduce((acc, judge) => {
+ judge.expertise.forEach(exp => {
+ if (!acc[exp]) {
+ acc[exp] = { total: 0, active: 0, inactive: 0 };
+ }
+ acc[exp].total++;
+ if (judge.is_active) {
+ acc[exp].active++;
+ } else {
+ acc[exp].inactive++;
+ }
+ });
+ return acc;
+ }, {} as Record);
+
+ const stats = {
+ totalJudges,
+ activeJudges,
+ inactiveJudges,
+ departmentStats,
+ expertiseStats,
+ lastUpdated: new Date().toISOString()
+ };
+
+ return NextResponse.json({
+ success: true,
+ message: '評審統計獲取成功',
+ data: stats
+ });
+
+ } catch (error) {
+ console.error('獲取評審統計失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取評審統計失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/admin/teams/[id]/members/route.ts b/app/api/admin/teams/[id]/members/route.ts
new file mode 100644
index 0000000..0ebee45
--- /dev/null
+++ b/app/api/admin/teams/[id]/members/route.ts
@@ -0,0 +1,195 @@
+// =====================================================
+// 團隊成員管理 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { TeamService } from '@/lib/services/database-service';
+
+// 獲取團隊成員
+export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+
+ // 檢查團隊是否存在
+ const team = await TeamService.getTeamById(id);
+ if (!team) {
+ return NextResponse.json({
+ success: false,
+ message: '團隊不存在',
+ error: '找不到指定的團隊'
+ }, { status: 404 });
+ }
+
+ const members = await TeamService.getTeamMembers(id);
+
+ return NextResponse.json({
+ success: true,
+ message: '團隊成員獲取成功',
+ data: members
+ });
+
+ } catch (error) {
+ console.error('獲取團隊成員失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取團隊成員失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 添加團隊成員
+export async function POST(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const body = await request.json();
+
+ // 驗證必填字段
+ if (!body.user_id || !body.role) {
+ return NextResponse.json({
+ success: false,
+ message: '請提供用戶ID和角色',
+ error: '缺少必填字段'
+ }, { status: 400 });
+ }
+
+ // 檢查團隊是否存在
+ const team = await TeamService.getTeamById(id);
+ if (!team) {
+ return NextResponse.json({
+ success: false,
+ message: '團隊不存在',
+ error: '找不到指定的團隊'
+ }, { status: 404 });
+ }
+
+ // 添加團隊成員
+ const success = await TeamService.addTeamMember(id, body.user_id, body.role);
+
+ if (!success) {
+ return NextResponse.json({
+ success: false,
+ message: '添加團隊成員失敗',
+ error: '無法添加團隊成員'
+ }, { status: 500 });
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: '團隊成員添加成功',
+ data: { team_id: id, user_id: body.user_id, role: body.role }
+ });
+
+ } catch (error) {
+ console.error('添加團隊成員失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '添加團隊成員失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 更新團隊成員角色
+export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const body = await request.json();
+
+ // 驗證必填字段
+ if (!body.user_id || !body.role) {
+ return NextResponse.json({
+ success: false,
+ message: '請提供用戶ID和角色',
+ error: '缺少必填字段'
+ }, { status: 400 });
+ }
+
+ // 檢查團隊是否存在
+ const team = await TeamService.getTeamById(id);
+ if (!team) {
+ return NextResponse.json({
+ success: false,
+ message: '團隊不存在',
+ error: '找不到指定的團隊'
+ }, { status: 404 });
+ }
+
+ // 更新團隊成員角色
+ const success = await TeamService.updateTeamMemberRole(id, body.user_id, body.role);
+
+ if (!success) {
+ return NextResponse.json({
+ success: false,
+ message: '更新團隊成員角色失敗',
+ error: '無法更新團隊成員角色'
+ }, { status: 500 });
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: '團隊成員角色更新成功',
+ data: { team_id: id, user_id: body.user_id, role: body.role }
+ });
+
+ } catch (error) {
+ console.error('更新團隊成員角色失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '更新團隊成員角色失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 移除團隊成員
+export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const url = new URL(request.url);
+ const userId = url.searchParams.get('user_id');
+
+ if (!userId) {
+ return NextResponse.json({
+ success: false,
+ message: '請提供用戶ID',
+ error: '缺少用戶ID參數'
+ }, { status: 400 });
+ }
+
+ // 檢查團隊是否存在
+ const team = await TeamService.getTeamById(id);
+ if (!team) {
+ return NextResponse.json({
+ success: false,
+ message: '團隊不存在',
+ error: '找不到指定的團隊'
+ }, { status: 404 });
+ }
+
+ // 移除團隊成員
+ const success = await TeamService.removeTeamMember(id, userId);
+
+ if (!success) {
+ return NextResponse.json({
+ success: false,
+ message: '移除團隊成員失敗',
+ error: '無法移除團隊成員'
+ }, { status: 500 });
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: '團隊成員移除成功',
+ data: { team_id: id, user_id: userId }
+ });
+
+ } catch (error) {
+ console.error('移除團隊成員失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '移除團隊成員失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/teams/[id]/route.ts b/app/api/admin/teams/[id]/route.ts
new file mode 100644
index 0000000..799bbf5
--- /dev/null
+++ b/app/api/admin/teams/[id]/route.ts
@@ -0,0 +1,154 @@
+// =====================================================
+// 團隊詳細操作 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { TeamService } from '@/lib/services/database-service';
+
+// 獲取單一團隊
+export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+
+ const team = await TeamService.getTeamById(id);
+
+ if (!team) {
+ return NextResponse.json({
+ success: false,
+ message: '團隊不存在',
+ error: '找不到指定的團隊'
+ }, { status: 404 });
+ }
+
+ // 獲取團隊成員
+ const members = await TeamService.getTeamMembers(id);
+ team.members = members;
+
+ // 獲取團隊應用
+ const apps = await TeamService.getTeamApps(id);
+ team.apps = apps;
+
+ return NextResponse.json({
+ success: true,
+ message: '團隊獲取成功',
+ data: team
+ });
+
+ } catch (error) {
+ console.error('獲取團隊失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取團隊失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 更新團隊
+export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const body = await request.json();
+
+ // 檢查團隊是否存在
+ const existingTeam = await TeamService.getTeamById(id);
+ if (!existingTeam) {
+ return NextResponse.json({
+ success: false,
+ message: '團隊不存在',
+ error: '找不到指定的團隊'
+ }, { status: 404 });
+ }
+
+ // 如果更新名稱,檢查是否重複
+ if (body.name && body.name !== existingTeam.name) {
+ const nameExists = await TeamService.getTeamByName(body.name);
+ if (nameExists) {
+ return NextResponse.json({
+ success: false,
+ message: '團隊名稱已存在',
+ error: '團隊名稱重複'
+ }, { status: 409 });
+ }
+ }
+
+ // 更新團隊
+ const success = await TeamService.updateTeam(id, body);
+
+ if (!success) {
+ return NextResponse.json({
+ success: false,
+ message: '更新團隊失敗',
+ error: '無法更新團隊'
+ }, { status: 500 });
+ }
+
+ // 獲取更新後的團隊信息
+ const updatedTeam = await TeamService.getTeamById(id);
+
+ return NextResponse.json({
+ success: true,
+ message: '團隊更新成功',
+ data: updatedTeam
+ });
+
+ } catch (error) {
+ console.error('更新團隊失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '更新團隊失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 刪除團隊
+export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const url = new URL(request.url);
+ const hardDelete = url.searchParams.get('hard') === 'true';
+
+ // 檢查團隊是否存在
+ const existingTeam = await TeamService.getTeamById(id);
+ if (!existingTeam) {
+ return NextResponse.json({
+ success: false,
+ message: '團隊不存在',
+ error: '找不到指定的團隊'
+ }, { status: 404 });
+ }
+
+ let success: boolean;
+
+ if (hardDelete) {
+ // 硬刪除:從資料庫中完全移除
+ success = await TeamService.hardDeleteTeam(id);
+ } else {
+ // 軟刪除:將 is_active 設為 false
+ success = await TeamService.deleteTeam(id);
+ }
+
+ if (!success) {
+ return NextResponse.json({
+ success: false,
+ message: '刪除團隊失敗',
+ error: '無法刪除團隊'
+ }, { status: 500 });
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: hardDelete ? '團隊已永久刪除' : '團隊已停用',
+ data: { hardDelete }
+ });
+
+ } catch (error) {
+ console.error('刪除團隊失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '刪除團隊失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/teams/route.ts b/app/api/admin/teams/route.ts
new file mode 100644
index 0000000..7c87957
--- /dev/null
+++ b/app/api/admin/teams/route.ts
@@ -0,0 +1,111 @@
+// =====================================================
+// 團隊管理 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { TeamService } from '@/lib/services/database-service';
+
+// 獲取所有團隊
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const search = searchParams.get('search') || '';
+ const department = searchParams.get('department') || '';
+
+ let teams = await TeamService.getAllTeams();
+
+ // 應用篩選
+ if (search) {
+ teams = teams.filter(team =>
+ team.name.toLowerCase().includes(search.toLowerCase()) ||
+ team.leader_name?.toLowerCase().includes(search.toLowerCase())
+ );
+ }
+
+ if (department && department !== 'all') {
+ teams = teams.filter(team => team.department === department);
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: '團隊列表獲取成功',
+ data: teams
+ });
+
+ } catch (error) {
+ console.error('獲取團隊列表失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取團隊列表失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 創建團隊
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+
+ // 驗證必填字段
+ if (!body.name || !body.leader_id || !body.department || !body.contact_email) {
+ return NextResponse.json({
+ success: false,
+ message: '請填寫所有必填字段',
+ error: '缺少必填字段'
+ }, { status: 400 });
+ }
+
+ // 檢查團隊名稱是否已存在
+ const existingTeam = await TeamService.getTeamByName(body.name);
+ if (existingTeam) {
+ return NextResponse.json({
+ success: false,
+ message: '團隊名稱已存在',
+ error: '團隊名稱重複'
+ }, { status: 409 });
+ }
+
+ // 創建團隊
+ const teamId = await TeamService.createTeam({
+ name: body.name,
+ leader_id: body.leader_id,
+ department: body.department,
+ contact_email: body.contact_email,
+ description: body.description
+ });
+
+ // 如果提供了成員列表,添加成員
+ if (body.members && Array.isArray(body.members)) {
+ for (const member of body.members) {
+ if (member.user_id && member.role) {
+ await TeamService.addTeamMember(teamId, member.user_id, member.role);
+ }
+ }
+ }
+
+ // 如果提供了應用列表,綁定應用
+ if (body.apps && Array.isArray(body.apps)) {
+ for (const appId of body.apps) {
+ await TeamService.bindAppToTeam(teamId, appId);
+ }
+ }
+
+ // 獲取創建後的團隊信息
+ const createdTeam = await TeamService.getTeamById(teamId);
+
+ return NextResponse.json({
+ success: true,
+ message: '團隊創建成功',
+ data: createdTeam
+ });
+
+ } catch (error) {
+ console.error('創建團隊失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '創建團隊失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/teams/stats/route.ts b/app/api/admin/teams/stats/route.ts
new file mode 100644
index 0000000..f3d87ca
--- /dev/null
+++ b/app/api/admin/teams/stats/route.ts
@@ -0,0 +1,26 @@
+// =====================================================
+// 團隊統計 API
+// =====================================================
+
+import { NextResponse } from 'next/server';
+import { TeamService } from '@/lib/services/database-service';
+
+export async function GET() {
+ try {
+ const stats = await TeamService.getTeamStats();
+
+ return NextResponse.json({
+ success: true,
+ message: '團隊統計獲取成功',
+ data: stats
+ });
+
+ } catch (error) {
+ console.error('獲取團隊統計失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取團隊統計失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/users/available/route.ts b/app/api/admin/users/available/route.ts
new file mode 100644
index 0000000..93ee893
--- /dev/null
+++ b/app/api/admin/users/available/route.ts
@@ -0,0 +1,34 @@
+// =====================================================
+// 獲取可用用戶列表 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { db } from '@/lib/database';
+
+export async function GET(request: NextRequest) {
+ try {
+ // 獲取所有活躍的用戶
+ const sql = `
+ SELECT id, name, email, department, phone
+ FROM users
+ WHERE status = 'active'
+ ORDER BY name ASC
+ `;
+
+ const users = await db.query(sql);
+
+ return NextResponse.json({
+ success: true,
+ message: '可用用戶列表獲取成功',
+ data: users
+ });
+
+ } catch (error) {
+ console.error('獲取可用用戶列表失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取可用用戶列表失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/components/admin/competition-management.tsx b/components/admin/competition-management.tsx
index 73202cd..9a38e31 100644
--- a/components/admin/competition-management.tsx
+++ b/components/admin/competition-management.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useState } from "react"
+import React, { useState, useEffect } from "react"
import { useCompetition } from "@/contexts/competition-context"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
@@ -38,6 +38,7 @@ import {
ClipboardList,
Link,
UserCheck,
+ UserX,
Upload,
Filter,
User,
@@ -79,6 +80,29 @@ export function CompetitionManagement() {
// Teams state - managed locally for now
const [teams, setTeams] = useState(initialTeams)
+ // 資料庫整合狀態
+ const [dbCompetitions, setDbCompetitions] = useState([])
+ const [isLoadingDb, setIsLoadingDb] = useState(false)
+ const [dbStats, setDbStats] = useState(null)
+
+ // 評審資料庫整合狀態
+ const [dbJudges, setDbJudges] = useState([])
+ const [isLoadingJudges, setIsLoadingJudges] = useState(false)
+ const [judgeStats, setJudgeStats] = useState(null)
+
+ // 團隊資料庫整合狀態
+ const [dbTeams, setDbTeams] = useState([])
+ const [isLoadingTeams, setIsLoadingTeams] = useState(false)
+ const [teamStats, setTeamStats] = useState(null)
+
+ // 可用應用狀態
+ const [availableApps, setAvailableApps] = useState([])
+ const [isLoadingApps, setIsLoadingApps] = useState(false)
+
+ // 可用用戶狀態
+ const [availableUsers, setAvailableUsers] = useState([])
+ const [isLoadingUsers, setIsLoadingUsers] = useState(false)
+
const [showCreateCompetition, setShowCreateCompetition] = useState(false)
const [showAddJudge, setShowAddJudge] = useState(false)
const [showCreateAward, setShowCreateAward] = useState(false)
@@ -106,10 +130,513 @@ export function CompetitionManagement() {
// 奖项搜索和筛选状态
const [awardSearchQuery, setAwardSearchQuery] = useState("")
const [awardYearFilter, setAwardYearFilter] = useState("all")
+
+ // 資料庫 API 調用函數
+ const fetchCompetitions = async () => {
+ setIsLoadingDb(true)
+ try {
+ const response = await fetch('/api/admin/competitions')
+ const data = await response.json()
+ if (data.success) {
+ setDbCompetitions(data.data)
+ } else {
+ setError('獲取競賽列表失敗: ' + data.message)
+ }
+ } catch (error) {
+ console.error('獲取競賽列表失敗:', error)
+ setError('獲取競賽列表失敗')
+ } finally {
+ setIsLoadingDb(false)
+ }
+ }
+
+ const fetchCompetitionStats = async () => {
+ try {
+ const response = await fetch('/api/admin/competitions/stats')
+ const data = await response.json()
+ if (data.success) {
+ setDbStats(data.data)
+ }
+ } catch (error) {
+ console.error('獲取競賽統計失敗:', error)
+ }
+ }
+
+ const createCompetitionInDb = async (competitionData: any) => {
+ try {
+ const response = await fetch('/api/admin/competitions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(competitionData)
+ })
+ const data = await response.json()
+ if (data.success) {
+ setSuccess('競賽創建成功!')
+ await fetchCompetitions() // 重新獲取列表
+ await fetchCompetitionStats() // 重新獲取統計
+ return data.data
+ } else {
+ setError('創建競賽失敗: ' + data.message)
+ return null
+ }
+ } catch (error) {
+ console.error('創建競賽失敗:', error)
+ setError('創建競賽失敗')
+ return null
+ }
+ }
+
+ const updateCompetitionInDb = async (id: string, updates: any) => {
+ try {
+ const response = await fetch(`/api/admin/competitions/${id}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(updates)
+ })
+ const data = await response.json()
+ if (data.success) {
+ setSuccess('競賽更新成功!')
+ await fetchCompetitions() // 重新獲取列表
+ await fetchCompetitionStats() // 重新獲取統計
+ return data.data
+ } else {
+ setError('更新競賽失敗: ' + data.message)
+ return null
+ }
+ } catch (error) {
+ console.error('更新競賽失敗:', error)
+ setError('更新競賽失敗')
+ return null
+ }
+ }
+
+ const deleteCompetitionInDb = async (id: string) => {
+ try {
+ const response = await fetch(`/api/admin/competitions/${id}`, {
+ method: 'DELETE'
+ })
+ const data = await response.json()
+ if (data.success) {
+ setSuccess('競賽刪除成功!')
+ await fetchCompetitions() // 重新獲取列表
+ await fetchCompetitionStats() // 重新獲取統計
+ return true
+ } else {
+ setError('刪除競賽失敗: ' + data.message)
+ return false
+ }
+ } catch (error) {
+ console.error('刪除競賽失敗:', error)
+ setError('刪除競賽失敗')
+ return false
+ }
+ }
+
+ // 評審資料庫 API 調用函數
+ const fetchJudges = async () => {
+ setIsLoadingJudges(true)
+ try {
+ // 添加時間戳避免緩存
+ const response = await fetch(`/api/admin/judges?t=${Date.now()}`, {
+ cache: 'no-store',
+ headers: {
+ 'Cache-Control': 'no-cache'
+ }
+ })
+ const data = await response.json()
+ console.log('🔍 fetchJudges 響應:', data)
+ if (data.success) {
+ console.log('📊 獲取到的評審數據:', data.data)
+ setDbJudges(data.data)
+ } else {
+ setError('獲取評審列表失敗: ' + data.message)
+ }
+ } catch (error) {
+ console.error('獲取評審列表失敗:', error)
+ setError('獲取評審列表失敗')
+ } finally {
+ setIsLoadingJudges(false)
+ }
+ }
+
+ const fetchJudgeStats = async () => {
+ try {
+ const response = await fetch('/api/admin/judges/stats')
+ const data = await response.json()
+ console.log('評審統計 API 響應:', data)
+ if (data.success) {
+ setJudgeStats(data.data)
+ console.log('評審統計設置成功:', data.data)
+ } else {
+ console.error('評審統計 API 失敗:', data.message)
+ }
+ } catch (error) {
+ console.error('獲取評審統計失敗:', error)
+ }
+ }
+
+ // 團隊資料庫 API 調用函數
+ const fetchTeams = async () => {
+ setIsLoadingTeams(true)
+ try {
+ // 添加時間戳避免緩存
+ const response = await fetch(`/api/admin/teams?t=${Date.now()}`, {
+ cache: 'no-store',
+ headers: {
+ 'Cache-Control': 'no-cache'
+ }
+ })
+ const data = await response.json()
+ console.log('🔍 fetchTeams 響應:', data)
+ if (data.success) {
+ console.log('📊 獲取到的團隊數據:', data.data)
+ setDbTeams(data.data)
+ } else {
+ setError('獲取團隊列表失敗: ' + data.message)
+ }
+ } catch (error) {
+ console.error('獲取團隊列表失敗:', error)
+ setError('獲取團隊列表失敗')
+ } finally {
+ setIsLoadingTeams(false)
+ }
+ }
+
+ const fetchTeamStats = async () => {
+ try {
+ const response = await fetch('/api/admin/teams/stats')
+ const data = await response.json()
+ console.log('團隊統計 API 響應:', data)
+ if (data.success) {
+ setTeamStats(data.data)
+ console.log('團隊統計設置成功:', data.data)
+ }
+ } catch (error) {
+ console.error('獲取團隊統計失敗:', error)
+ }
+ }
+
+ const createTeamInDb = async (teamData: any) => {
+ try {
+ const response = await fetch('/api/admin/teams', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(teamData)
+ })
+ const data = await response.json()
+ if (data.success) {
+ setSuccess('團隊創建成功!')
+ await fetchTeams() // 重新獲取列表
+ await fetchTeamStats() // 重新獲取統計
+ return data.data
+ } else {
+ setError('創建團隊失敗: ' + data.message)
+ return null
+ }
+ } catch (error) {
+ console.error('創建團隊失敗:', error)
+ setError('創建團隊失敗')
+ return null
+ }
+ }
+
+ const updateTeamInDb = async (teamId: string, updates: any) => {
+ try {
+ const response = await fetch(`/api/admin/teams/${teamId}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(updates)
+ })
+ const data = await response.json()
+ if (data.success) {
+ setSuccess('團隊更新成功!')
+ await fetchTeams() // 重新獲取列表
+ await fetchTeamStats() // 重新獲取統計
+ return true // 返回 true 表示成功
+ } else {
+ setError('更新團隊失敗: ' + data.message)
+ return false
+ }
+ } catch (error) {
+ console.error('更新團隊失敗:', error)
+ setError('更新團隊失敗')
+ return false
+ }
+ }
+
+ const deleteTeamInDb = async (teamId: string, hard: boolean = false, skipRefresh: boolean = false) => {
+ try {
+ const response = await fetch(`/api/admin/teams/${teamId}?hard=${hard}`, {
+ method: 'DELETE'
+ })
+ const data = await response.json()
+ if (data.success) {
+ setSuccess(data.message || '團隊刪除成功!')
+ if (!skipRefresh) {
+ await fetchTeams() // 重新獲取列表
+ await fetchTeamStats() // 重新獲取統計
+ }
+ return true
+ } else {
+ setError('刪除團隊失敗: ' + data.message)
+ return false
+ }
+ } catch (error) {
+ console.error('刪除團隊失敗:', error)
+ setError('刪除團隊失敗')
+ return false
+ }
+ }
+
+ // 獲取可用應用列表
+ const fetchAvailableApps = async (teamId?: string) => {
+ console.log('🔍 開始獲取可用應用列表, teamId:', teamId)
+ setIsLoadingApps(true)
+ try {
+ const url = teamId
+ ? `/api/admin/apps/available?teamId=${teamId}`
+ : '/api/admin/apps/available'
+
+ console.log('📡 請求 URL:', url)
+ const response = await fetch(url)
+ const data = await response.json()
+
+ console.log('📊 應用 API 響應:', data)
+
+ if (data.success) {
+ console.log('✅ 應用數據設置成功:', data.data.length, '個應用')
+ setAvailableApps(data.data)
+ return data.data // 返回數據
+ } else {
+ console.error('❌ 獲取應用列表失敗:', data.message)
+ setError('獲取可用應用列表失敗: ' + data.message)
+ return []
+ }
+ } catch (error) {
+ console.error('❌ 獲取可用應用列表失敗:', error)
+ setError('獲取可用應用列表失敗')
+ return []
+ } finally {
+ setIsLoadingApps(false)
+ }
+ }
+
+ // 獲取可用用戶列表
+ const fetchAvailableUsers = async () => {
+ console.log('🔍 開始獲取可用用戶列表')
+ setIsLoadingUsers(true)
+ try {
+ const response = await fetch('/api/admin/users/available')
+ const data = await response.json()
+
+ console.log('📊 用戶 API 響應:', data)
+
+ if (data.success) {
+ console.log('✅ 用戶數據設置成功:', data.data.length, '個用戶')
+ setAvailableUsers(data.data)
+ return data.data // 返回數據
+ } else {
+ console.error('❌ 獲取用戶列表失敗:', data.message)
+ setError('獲取可用用戶列表失敗: ' + data.message)
+ return []
+ }
+ } catch (error) {
+ console.error('❌ 獲取可用用戶列表失敗:', error)
+ setError('獲取可用用戶列表失敗')
+ return []
+ } finally {
+ setIsLoadingUsers(false)
+ }
+ }
+
+ // 獲取完整團隊信息
+ const fetchTeamDetails = async (teamId: string) => {
+ try {
+ const response = await fetch(`/api/admin/teams/${teamId}`)
+ const data = await response.json()
+
+ if (data.success) {
+ return data.data
+ } else {
+ setError('獲取團隊詳情失敗: ' + data.message)
+ return null
+ }
+ } catch (error) {
+ console.error('獲取團隊詳情失敗:', error)
+ setError('獲取團隊詳情失敗')
+ return null
+ }
+ }
+
+ // 編輯團隊
+ const handleEditTeam = async (team: any) => {
+ try {
+ console.log('🔍 開始編輯團隊:', team.id)
+
+ // 先加載用戶和應用數據
+ const users = await fetchAvailableUsers()
+ const apps = await fetchAvailableApps(team.id)
+
+ console.log('📊 實際載入的用戶數據:', users.length)
+ console.log('📊 實際載入的應用數據:', apps.length)
+
+ // 獲取完整的團隊信息
+ const teamDetails = await fetchTeamDetails(team.id)
+ console.log('📋 團隊詳情:', teamDetails)
+
+ if (teamDetails) {
+ // 填充表單數據
+ const teamData = {
+ name: teamDetails.name,
+ leader: teamDetails.leader_name || '',
+ leader_id: teamDetails.leader_id || '',
+ department: teamDetails.department,
+ contactEmail: teamDetails.contact_email || '',
+ leaderPhone: teamDetails.leader_phone || '',
+ description: teamDetails.description || '',
+ members: teamDetails.members || [],
+ apps: teamDetails.apps ? teamDetails.apps.map((app: any) => app.id || app) : [],
+ submittedAppCount: teamDetails.apps?.length || 0,
+ }
+
+ console.log('📝 填充的表單數據:', teamData)
+ setNewTeam(teamData)
+ setSelectedTeam(teamDetails)
+ setShowCreateTeam(true)
+ }
+ } catch (error) {
+ console.error('編輯團隊失敗:', error)
+ setError('編輯團隊失敗')
+ }
+ }
+
+ // 刪除團隊
+ const handleDeleteTeam = async (team: any) => {
+ setTeamToDelete(team)
+ setShowDeleteTeamConfirm(true)
+ }
+
+ // 確認刪除團隊
+ const confirmDeleteTeam = async () => {
+ if (!teamToDelete) return
+
+ setIsLoading(true)
+ try {
+ const success = await deleteTeamInDb(teamToDelete.id)
+ if (success) {
+ setSuccess("團隊刪除成功!")
+ setShowDeleteTeamConfirm(false)
+ setTeamToDelete(null)
+ // 重新獲取團隊列表
+ await fetchTeams()
+ await fetchTeamStats()
+ }
+ } catch (error) {
+ console.error('刪除團隊失敗:', error)
+ setError('刪除團隊失敗')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const createJudgeInDb = async (judgeData: any) => {
+ try {
+ const response = await fetch('/api/admin/judges', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(judgeData)
+ })
+ const data = await response.json()
+ if (data.success) {
+ setSuccess('評審創建成功!')
+ await fetchJudges() // 重新獲取列表
+ await fetchJudgeStats() // 重新獲取統計
+ return data.data
+ } else {
+ setError('創建評審失敗: ' + data.message)
+ return null
+ }
+ } catch (error) {
+ console.error('創建評審失敗:', error)
+ setError('創建評審失敗')
+ return null
+ }
+ }
+
+ const updateJudgeInDb = async (id: string, updates: any) => {
+ try {
+ const response = await fetch(`/api/admin/judges/${id}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(updates)
+ })
+ const data = await response.json()
+ if (data.success) {
+ setSuccess('評審更新成功!')
+ await fetchJudges() // 重新獲取列表
+ await fetchJudgeStats() // 重新獲取統計
+ return true // 返回 true 表示成功
+ } else {
+ setError('更新評審失敗: ' + data.message)
+ return false
+ }
+ } catch (error) {
+ console.error('更新評審失敗:', error)
+ setError('更新評審失敗')
+ return false
+ }
+ }
+
+ const deleteJudgeInDb = async (id: string, hardDelete: boolean = false, skipRefresh: boolean = false) => {
+ try {
+ const url = hardDelete ? `/api/admin/judges/${id}?hard=true` : `/api/admin/judges/${id}`
+ const response = await fetch(url, {
+ method: 'DELETE'
+ })
+ const data = await response.json()
+ if (data.success) {
+ setSuccess(data.message || '評審刪除成功!')
+ if (!skipRefresh) {
+ await fetchJudges() // 重新獲取列表
+ await fetchJudgeStats() // 重新獲取統計
+ }
+ return true
+ } else {
+ setError('刪除評審失敗: ' + data.message)
+ return false
+ }
+ } catch (error) {
+ console.error('刪除評審失敗:', error)
+ setError('刪除評審失敗')
+ return false
+ }
+ }
const [awardMonthFilter, setAwardMonthFilter] = useState("all")
const [awardTypeFilter, setAwardTypeFilter] = useState("all")
const [awardCompetitionTypeFilter, setAwardCompetitionTypeFilter] = useState("all")
+ // 組件載入時獲取資料
+ useEffect(() => {
+ fetchCompetitions()
+ fetchCompetitionStats()
+ fetchJudges()
+ fetchJudgeStats()
+ fetchTeams()
+ fetchTeamStats()
+ }, [])
+
// 当筛选条件改变时重置分页
const resetAwardPagination = () => {
setAwardCurrentPage(1)
@@ -191,18 +718,19 @@ export function CompetitionManagement() {
const [newTeam, setNewTeam] = useState({
name: "",
leader: "",
+ leader_id: "", // 添加隊長 ID
department: "HQBU",
contactEmail: "",
leaderPhone: "",
description: "",
members: [] as Array<{ id: string; name: string; department: string; role: string }>,
- apps: [] as string[],
- appLinks: [] as string[],
+ apps: [] as string[], // 改為存儲應用 ID
submittedAppCount: 0,
})
const [newMember, setNewMember] = useState({
name: "",
+ user_id: "", // 添加用戶 ID
department: "HQBU",
role: "成員",
})
@@ -227,6 +755,7 @@ export function CompetitionManagement() {
const [showJudgeDetail, setShowJudgeDetail] = useState(false)
const [showDeleteJudgeConfirm, setShowDeleteJudgeConfirm] = useState(false)
+ const [showDisableJudgeConfirm, setShowDisableJudgeConfirm] = useState(false)
// 獎項相關狀態
const [showAwardDetail, setShowAwardDetail] = useState(false)
@@ -239,6 +768,7 @@ export function CompetitionManagement() {
const [judgeSearchTerm, setJudgeSearchTerm] = useState("")
const [judgeDepartmentFilter, setJudgeDepartmentFilter] = useState("all")
const [judgeExpertiseFilter, setJudgeExpertiseFilter] = useState("all")
+ const [judgeStatusFilter, setJudgeStatusFilter] = useState("all")
const judgesPerPage = 6
// 團隊分頁和篩選狀態
@@ -256,7 +786,7 @@ export function CompetitionManagement() {
case "individual":
return mockIndividualApps
case "team":
- return teams
+ return dbTeams.length > 0 ? dbTeams : teams
default:
return []
}
@@ -334,7 +864,6 @@ export function CompetitionManagement() {
description: "",
members: [],
apps: [],
- appLinks: [],
submittedAppCount: 0,
})
setNewMember({
@@ -437,21 +966,69 @@ export function CompetitionManagement() {
}
setIsLoading(true)
- await new Promise((resolve) => setTimeout(resolve, 1000))
+ try {
if (selectedCompetitionForAction) {
// 編輯模式 - 更新現有競賽
+ const updates = {
+ name: newCompetition.name,
+ year: newCompetition.year,
+ month: newCompetition.month,
+ startDate: newCompetition.startDate,
+ endDate: newCompetition.endDate,
+ status: newCompetition.status,
+ description: newCompetition.description,
+ type: newCompetition.type,
+ evaluationFocus: newCompetition.evaluationFocus,
+ maxTeamSize: newCompetition.maxTeamSize || null,
+ // 關聯數據
+ judges: newCompetition.judges || [],
+ teams: newCompetition.participatingTeams || [],
+ awardTypes: newCompetition.awardTypes || [],
+ rules: newCompetition.rules || []
+ }
+
+ const updatedCompetition = await updateCompetitionInDb(selectedCompetitionForAction.id, updates)
+ if (updatedCompetition) {
+ // 同時更新 context
updateCompetition(selectedCompetitionForAction.id, newCompetition)
- setSuccess("競賽更新成功!")
+ }
} else {
- // 創建模式 - 新增競賽
+ // 創建模式 - 新增競賽到資料庫
+ const competitionData = {
+ name: newCompetition.name,
+ year: newCompetition.year,
+ month: newCompetition.month,
+ startDate: newCompetition.startDate,
+ endDate: newCompetition.endDate,
+ status: newCompetition.status,
+ description: newCompetition.description,
+ type: newCompetition.type,
+ evaluationFocus: newCompetition.evaluationFocus,
+ maxTeamSize: newCompetition.maxTeamSize || null,
+ isActive: true,
+ // 關聯數據
+ judges: newCompetition.judges || [],
+ teams: newCompetition.participatingTeams || [],
+ awardTypes: newCompetition.awardTypes || [],
+ rules: newCompetition.rules || []
+ }
+
+ const createdCompetition = await createCompetitionInDb(competitionData)
+
+ if (createdCompetition) {
+ // 同時添加到 context(保持向後兼容)
const competitionWithId = {
...newCompetition,
- id: `c${Date.now()}`,
- createdAt: new Date().toISOString(),
+ id: createdCompetition.id,
+ createdAt: createdCompetition.created_at,
}
addCompetition(competitionWithId)
- setSuccess("競賽創建成功!")
+ }
+ }
+ } catch (error) {
+ console.error("處理競賽失敗:", error)
+ setCreateError("處理競賽時發生錯誤")
}
setShowCreateCompetition(false)
@@ -483,32 +1060,56 @@ export function CompetitionManagement() {
}
setIsLoading(true)
- await new Promise((resolve) => setTimeout(resolve, 1000))
+ try {
if (selectedTeam) {
// 編輯模式 - 更新現有團隊
- const updatedTeam = {
- ...selectedTeam,
- ...newTeam,
- memberCount: newTeam.members.length,
- submittedAppCount: newTeam.apps.length,
- }
- const updatedTeams = teams.map(team =>
- team.id === selectedTeam.id ? updatedTeam : team
- )
- setTeams(updatedTeams)
+ // 使用選擇的隊長 ID,如果沒有選擇則保持原有的隊長 ID
+ const leaderId = newTeam.leader_id || (selectedTeam as any).leader_id || '0b844fb6-1a63-4e0c-a15a-416e9b0ec8c7'
+
+ const teamData = {
+ name: newTeam.name,
+ leader_id: leaderId,
+ department: newTeam.department,
+ contact_email: newTeam.contactEmail,
+ description: newTeam.description,
+ members: newTeam.members.map(member => ({
+ user_id: member.id, // 現在 member.id 就是 user_id
+ role: member.role || 'member'
+ })),
+ apps: newTeam.apps // 添加應用 ID 列表
+ }
+
+ const success = await updateTeamInDb(selectedTeam.id, teamData)
+ if (success) {
setSuccess("團隊更新成功!")
+ }
} else {
// 創建模式 - 新增團隊
- const team = {
- id: `t${Date.now()}`,
- ...newTeam,
- memberCount: newTeam.members.length,
- submissionDate: new Date().toISOString().split("T")[0],
- submittedAppCount: newTeam.apps.length,
- }
- setTeams([...teams, team])
+ // 使用選擇的隊長 ID,如果沒有選擇則使用預設的用戶 ID
+ const leaderId = newTeam.leader_id || '0b844fb6-1a63-4e0c-a15a-416e9b0ec8c7'
+
+ const teamData = {
+ name: newTeam.name,
+ leader_id: leaderId,
+ department: newTeam.department,
+ contact_email: newTeam.contactEmail,
+ description: newTeam.description,
+ members: newTeam.members.map(member => ({
+ user_id: member.id, // 現在 member.id 就是 user_id
+ role: member.role || 'member'
+ })),
+ apps: newTeam.apps // 添加應用 ID 列表
+ }
+
+ const createdTeam = await createTeamInDb(teamData)
+ if (createdTeam) {
setSuccess("團隊創建成功!")
+ }
+ }
+ } catch (error) {
+ console.error("處理團隊失敗:", error)
+ setCreateError("處理團隊時發生錯誤")
}
setShowCreateTeam(false)
@@ -518,52 +1119,45 @@ export function CompetitionManagement() {
setTimeout(() => setSuccess(""), 3000)
}
- const handleEditTeam = (team: any) => {
- setSelectedTeam(team)
- setNewTeam({
- name: team.name,
- leader: team.leader,
- department: team.department,
- contactEmail: team.contactEmail,
- leaderPhone: team.leaderPhone || "",
- description: team.description,
- members: [...team.members],
- apps: [...team.apps],
- appLinks: [...team.appLinks],
- submittedAppCount: team.submittedAppCount,
- })
- setShowCreateTeam(true) // 使用創建團隊對話框
- }
- const handleDeleteTeam = (team: any) => {
- setTeamToDelete(team)
- setShowDeleteTeamConfirm(true)
- }
const handleConfirmDeleteTeam = async () => {
if (!teamToDelete) return
setIsLoading(true)
- await new Promise((resolve) => setTimeout(resolve, 500))
-
- setTeams(teams.filter((team) => team.id !== teamToDelete.id))
+ try {
+ const success = await deleteTeamInDb(teamToDelete.id, true) // 硬刪除
+ if (success) {
setShowDeleteTeamConfirm(false)
setTeamToDelete(null)
setSuccess("團隊刪除成功!")
+ } else {
+ setError("刪除團隊失敗")
+ }
+ } catch (error) {
+ console.error('刪除團隊失敗:', error)
+ setError('刪除團隊失敗')
+ } finally {
setIsLoading(false)
- setTimeout(() => setSuccess(""), 3000)
+ }
}
const handleAddMember = () => {
- if (!newMember.name.trim()) {
- setCreateError("請輸入成員姓名")
+ if (!newMember.user_id || !newMember.name.trim()) {
+ setCreateError("請選擇成員")
+ return
+ }
+
+ // 檢查是否已經添加過這個成員
+ if (newTeam.members.some(member => member.id === newMember.user_id)) {
+ setCreateError("該成員已經在團隊中")
return
}
const member = {
- id: `m${Date.now()}`,
+ id: newMember.user_id, // 使用用戶 ID
name: newMember.name,
department: newMember.department,
role: newMember.role,
@@ -576,6 +1170,7 @@ export function CompetitionManagement() {
setNewMember({
name: "",
+ user_id: "",
department: "HQBU",
role: "成員",
})
@@ -589,35 +1184,23 @@ export function CompetitionManagement() {
})
}
- const handleAddApp = () => {
- if (!newApp.name.trim()) {
- setCreateError("請輸入應用名稱")
+ const handleAddApp = (appId: string) => {
+ if (newTeam.apps.includes(appId)) {
+ setCreateError("此應用已經加入團隊")
return
}
setNewTeam({
...newTeam,
- apps: [...newTeam.apps, newApp.name],
- appLinks: [...newTeam.appLinks, newApp.link],
- })
-
- setNewApp({
- name: "",
- link: "",
+ apps: [...newTeam.apps, appId],
})
setCreateError("")
}
- const handleRemoveApp = (index: number) => {
- const newApps = [...newTeam.apps]
- const newAppLinks = [...newTeam.appLinks]
- newApps.splice(index, 1)
- newAppLinks.splice(index, 1)
-
+ const handleRemoveApp = (appId: string) => {
setNewTeam({
...newTeam,
- apps: newApps,
- appLinks: newAppLinks,
+ apps: newTeam.apps.filter(id => id !== appId),
})
}
@@ -630,28 +1213,60 @@ export function CompetitionManagement() {
}
setIsLoading(true)
- await new Promise((resolve) => setTimeout(resolve, 1000))
+
+ try {
+ const expertiseArray = newJudge.expertise
+ .split(",")
+ .map((s) => s.trim())
+ .filter(Boolean)
if (selectedJudge) {
// 編輯模式 - 更新現有評審
+ const updates = {
+ name: newJudge.name,
+ title: newJudge.title,
+ department: newJudge.department,
+ expertise: expertiseArray,
+ avatar: newJudge.avatar || null,
+ isActive: true
+ }
+
+ const updatedJudge = await updateJudgeInDb(selectedJudge.id, updates)
+ if (updatedJudge) {
+ // 同時更新 context
updateJudge(selectedJudge.id, {
...newJudge,
- expertise: newJudge.expertise
- .split(",")
- .map((s) => s.trim())
- .filter(Boolean),
+ expertise: expertiseArray,
})
- setSuccess("評審更新成功!")
+ }
} else {
- // 新增模式 - 新增評審
+ // 新增模式 - 新增評審到資料庫
+ const judgeData = {
+ name: newJudge.name,
+ title: newJudge.title,
+ department: newJudge.department,
+ expertise: expertiseArray,
+ avatar: newJudge.avatar || null,
+ isActive: true
+ }
+
+ const createdJudge = await createJudgeInDb(judgeData)
+
+ if (createdJudge) {
+ // 同時添加到 context(保持向後兼容)
addJudge({
...newJudge,
- expertise: newJudge.expertise
- .split(",")
- .map((s) => s.trim())
- .filter(Boolean),
- })
- setSuccess("評審新增成功!")
+ expertise: expertiseArray,
+ })
+
+ // 重新獲取評審列表和統計
+ await fetchJudges()
+ await fetchJudgeStats()
+ }
+ }
+ } catch (error) {
+ console.error("處理評審失敗:", error)
+ setError("處理評審時發生錯誤")
}
setShowAddJudge(false)
@@ -677,6 +1292,29 @@ export function CompetitionManagement() {
setShowAddJudge(true) // 使用新增評審對話框
}
+ const handleDisableJudge = (judge: any) => {
+ setSelectedJudge(judge)
+ setShowDisableJudgeConfirm(true)
+ }
+
+ const handleEnableJudge = async (judge: any) => {
+ setIsLoading(true)
+ try {
+ const success = await updateJudgeInDb(judge.id, { is_active: true })
+ if (success) {
+ setSuccess('評審已啟用!')
+ // 不需要再次調用 fetchJudges 和 fetchJudgeStats,因為 updateJudgeInDb 已經調用了
+ } else {
+ setError('啟用評審失敗')
+ }
+ } catch (error) {
+ console.error('啟用評審失敗:', error)
+ setError('啟用評審失敗')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
const handleDeleteJudge = (judge: any) => {
setSelectedJudge(judge)
setShowDeleteJudgeConfirm(true)
@@ -684,18 +1322,83 @@ export function CompetitionManagement() {
- const confirmDeleteJudge = async () => {
+ const confirmDisableJudge = async () => {
if (!selectedJudge) return
setIsLoading(true)
- await new Promise((resolve) => setTimeout(resolve, 500))
+
+ try {
+ const success = await updateJudgeInDb(selectedJudge.id, { is_active: false })
+ if (success) {
+ setShowDisableJudgeConfirm(false)
+ setSelectedJudge(null)
+ setSuccess('評審已停用!')
+ // 不需要再次調用 fetchJudges 和 fetchJudgeStats,因為 updateJudgeInDb 已經調用了
+ } else {
+ setError('停用評審失敗')
+ }
+ } catch (error) {
+ console.error('停用評審失敗:', error)
+ setError('停用評審失敗')
+ } finally {
+ setIsLoading(false)
+ }
+ }
- deleteJudge(selectedJudge.id)
+ const confirmDeleteJudge = async () => {
+ if (!selectedJudge) return
+
+ const judgeId = selectedJudge.id // 保存 ID,避免後續變為 null
+ const judgeName = selectedJudge.name // 保存名稱,避免後續變為 null
+ setIsLoading(true)
+
+ try {
+ const success = await deleteJudgeInDb(judgeId, true, true) // 直接硬刪除,跳過內部刷新
+ if (success) {
setShowDeleteJudgeConfirm(false)
setSelectedJudge(null)
- setSuccess("評審刪除成功!")
+
+ // 立即從 context 中移除(如果存在)
+ console.log('🗑️ 調用 deleteJudge 移除評審:', judgeId)
+ console.log('🗑️ 評審名稱:', judgeName)
+ console.log('🗑️ 當前 context 中的評審:', judges.map(j => ({ id: j.id, name: j.name })))
+
+ // 根據名稱匹配 context 中的評審,因為 ID 可能不匹配
+ const contextJudge = judges.find(j => j.name === judgeName)
+ if (contextJudge) {
+ console.log('🗑️ 找到匹配的 context 評審:', contextJudge)
+ deleteJudge(contextJudge.id)
+ } else {
+ console.log('🗑️ 沒有找到匹配的 context 評審,嘗試使用原始 ID')
+ deleteJudge(judgeId)
+ }
+
+ // 強制從 dbJudges 中移除,確保立即更新
+ setDbJudges(prev => {
+ console.log('🗑️ 從 dbJudges 中移除評審:', judgeId)
+ console.log('🗑️ 移除前的評審列表:', prev.map(j => ({ id: j.id, name: j.name, is_active: j.is_active })))
+ const filtered = prev.filter(judge => judge.id !== judgeId)
+ console.log('🗑️ 移除後的評審列表:', filtered.map(j => ({ id: j.id, name: j.name, is_active: j.is_active })))
+ return filtered
+ })
+
+ // 等待一個微任務,確保 context 狀態更新完成
+ await new Promise(resolve => setTimeout(resolve, 0))
+
+ // 確保列表和統計數據立即更新
+ console.log('🔄 調用 fetchJudges 重新獲取數據')
+ await fetchJudges()
+ await fetchJudgeStats()
+
+ // 再次檢查 context 狀態
+ console.log('🔍 刪除後檢查 context 狀態')
+ }
+ } catch (error) {
+ console.error('刪除評審失敗:', error)
+ setError('刪除評審失敗')
+ } finally {
setIsLoading(false)
- setTimeout(() => setSuccess(""), 3000)
+ }
}
const handleCreateAward = async () => {
@@ -725,7 +1428,7 @@ export function CompetitionManagement() {
participantName = participant?.name || ""
creatorName = participant?.creator || ""
} else if (newAward.participantType === "team") {
- participant = teams.find((t) => t.id === newAward.participantId)
+ participant = (dbTeams.length > 0 ? dbTeams : teams).find((t) => t.id === newAward.participantId)
participantName = participant?.name || ""
creatorName = participant?.leader || ""
}
@@ -1111,7 +1814,47 @@ export function CompetitionManagement() {
}
}
+ // 格式化時間顯示
+ const formatDateRange = (startDate: string, endDate: string) => {
+ try {
+ const start = new Date(startDate)
+ const end = new Date(endDate)
+
+ const formatDate = (date: Date) => {
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
+ }
+
+ return `${formatDate(start)} ~ ${formatDate(end)}`
+ } catch (error) {
+ return `${startDate} ~ ${endDate}`
+ }
+ }
+
+ // 計算權重百分比
+ const calculateWeightPercentage = (weight: any) => {
+ const weightNum = parseFloat(weight) || 0;
+ // 如果權重已經是百分比格式(大於1),直接使用
+ if (weightNum > 1) {
+ return weightNum;
+ }
+ // 如果權重是小數格式(0-1),轉換為百分比
+ return weightNum * 100;
+ }
+
+ // 計算總權重
+ const calculateTotalWeight = (rules: any[]) => {
+ return rules.reduce((sum, rule) => {
+ return sum + calculateWeightPercentage(rule.weight);
+ }, 0);
+ }
+
const getParticipantCount = (competition: any) => {
+ // 如果是從資料庫載入的競賽,使用 teams 屬性
+ if (competition.teams && Array.isArray(competition.teams)) {
+ return competition.teams.length
+ }
+
+ // 如果是本地競賽數據,使用原來的邏輯
switch (competition.type) {
case "individual":
return competition.participatingApps?.length || 0
@@ -1287,7 +2030,13 @@ export function CompetitionManagement() {
總競賽數
-
{displayCompetitions.length}
+
+ {isLoadingDb ? (
+
+ ) : (
+ dbStats?.total || displayCompetitions.length
+ )}
+
@@ -1299,7 +2048,13 @@ export function CompetitionManagement() {
進行中
-
{displayCompetitions.filter((c) => c.status === "active").length}
+
+ {isLoadingDb ? (
+
+ ) : (
+ dbStats?.active || displayCompetitions.filter((c) => c.status === "active").length
+ )}
+
@@ -1311,7 +2066,7 @@ export function CompetitionManagement() {
評審團
-
{judges.length}
+
{judgeStats?.totalJudges || dbJudges.length || judges.length}
@@ -1360,7 +2115,22 @@ export function CompetitionManagement() {
- {displayCompetitions.map((competition) => {
+ {isLoadingDb ? (
+
+
+
+ 載入競賽列表中...
+
+
+ ) : (dbCompetitions.length > 0 ? dbCompetitions : displayCompetitions).length === 0 ? (
+
+
+
+ 尚無競賽資料
+ 點擊「創建競賽」按鈕開始建立第一個競賽
+
+
+ ) : (dbCompetitions.length > 0 ? dbCompetitions : displayCompetitions).map((competition) => {
const isCurrentCompetition = currentCompetition?.id === competition.id
const scoringProgress = getScoringProgress(competition.id)
const participantCount = getParticipantCount(competition)
@@ -1390,7 +2160,10 @@ export function CompetitionManagement() {
{competition.year}年{competition.month}月
- {competition.startDate} ~ {competition.endDate}
+ {formatDateRange(
+ competition.start_date || competition.startDate,
+ competition.end_date || competition.endDate
+ )}
@@ -1481,7 +2254,11 @@ export function CompetitionManagement() {
團隊管理