新增競賽前台呈現、刪除競賽、修改競賽狀態

This commit is contained in:
2025-09-16 14:57:40 +08:00
parent 1f2fb14bd0
commit b4386dc481
21 changed files with 1714 additions and 127 deletions

View File

@@ -88,7 +88,7 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
// 驗證狀態(如果提供) // 驗證狀態(如果提供)
if (body.status) { if (body.status) {
const validStatuses = ['upcoming', 'active', 'judging', 'completed']; const validStatuses = ['upcoming', 'ongoing', 'active', 'judging', 'completed'];
if (!validStatuses.includes(body.status)) { if (!validStatuses.includes(body.status)) {
return NextResponse.json({ return NextResponse.json({
success: false, success: false,
@@ -160,7 +160,7 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
} }
} }
// 刪除競賽(軟刪除) // 刪除競賽
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
try { try {
const { id } = await params; const { id } = await params;
@@ -175,8 +175,8 @@ export async function DELETE(request: NextRequest, { params }: { params: { id: s
}, { status: 404 }); }, { status: 404 });
} }
// 軟刪除:將 is_active 設為 false // 使用雙寫功能刪除競賽
const success = await CompetitionService.updateCompetition(id, { is_active: false }); const success = await CompetitionService.deleteCompetition(id);
if (!success) { if (!success) {
return NextResponse.json({ return NextResponse.json({

View File

@@ -0,0 +1,109 @@
// =====================================================
// 當前競賽管理 API
// =====================================================
import { NextRequest, NextResponse } from 'next/server';
import { CompetitionService } from '@/lib/services/database-service';
// 獲取當前競賽
export async function GET(request: NextRequest) {
try {
const currentCompetition = await CompetitionService.getCurrentCompetition();
return NextResponse.json({
success: true,
message: '當前競賽獲取成功',
data: currentCompetition
});
} 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 { competitionId } = body;
if (!competitionId) {
return NextResponse.json({
success: false,
message: '缺少競賽ID',
error: 'competitionId 為必填欄位'
}, { status: 400 });
}
// 檢查競賽是否存在
const competition = await CompetitionService.getCompetitionById(competitionId);
if (!competition) {
return NextResponse.json({
success: false,
message: '競賽不存在',
error: '找不到指定的競賽'
}, { status: 404 });
}
// 設置為當前競賽
const success = await CompetitionService.setCurrentCompetition(competitionId);
if (!success) {
return NextResponse.json({
success: false,
message: '設置當前競賽失敗',
error: '無法設置當前競賽'
}, { status: 500 });
}
// 獲取更新後的當前競賽
const updatedCurrentCompetition = await CompetitionService.getCurrentCompetition();
return NextResponse.json({
success: true,
message: '當前競賽設置成功',
data: updatedCurrentCompetition
});
} 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) {
try {
const success = await CompetitionService.clearCurrentCompetition();
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 });
}
}

View File

@@ -103,7 +103,7 @@ export async function POST(request: NextRequest) {
} }
// 驗證狀態 // 驗證狀態
const validStatuses = ['upcoming', 'active', 'judging', 'completed']; const validStatuses = ['upcoming', 'ongoing', 'active', 'judging', 'completed'];
if (!validStatuses.includes(status)) { if (!validStatuses.includes(status)) {
return NextResponse.json({ return NextResponse.json({
success: false, success: false,

View File

@@ -14,7 +14,8 @@ export async function GET(request: NextRequest) {
const stats = { const stats = {
total: competitions.length, total: competitions.length,
upcoming: competitions.filter(c => c.status === 'upcoming').length, upcoming: competitions.filter(c => c.status === 'upcoming').length,
active: competitions.filter(c => c.status === 'active').length, active: competitions.filter(c => c.status === 'active' || c.status === 'ongoing').length,
ongoing: competitions.filter(c => c.status === 'ongoing').length,
judging: competitions.filter(c => c.status === 'judging').length, judging: competitions.filter(c => c.status === 'judging').length,
completed: competitions.filter(c => c.status === 'completed').length, completed: competitions.filter(c => c.status === 'completed').length,
individual: competitions.filter(c => c.type === 'individual').length, individual: competitions.filter(c => c.type === 'individual').length,

View File

@@ -0,0 +1,130 @@
// =====================================================
// 競賽參賽應用 API
// =====================================================
import { NextRequest, NextResponse } from 'next/server';
import { CompetitionService } from '@/lib/services/database-service';
import { AppService } from '@/lib/services/database-service';
// 獲取競賽的參賽應用
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
try {
const { id } = await params;
const { searchParams } = new URL(request.url);
const search = searchParams.get('search') || '';
const category = searchParams.get('category') || 'all';
const type = searchParams.get('type') || 'all';
const department = searchParams.get('department') || 'all';
const competitionType = searchParams.get('competitionType') || 'all';
// 獲取競賽信息
const competition = await CompetitionService.getCompetitionWithDetails(id);
if (!competition) {
return NextResponse.json({
success: false,
message: '競賽不存在',
error: '找不到指定的競賽'
}, { status: 404 });
}
// 獲取競賽的參賽應用
const competitionApps = await CompetitionService.getCompetitionApps(id);
if (competitionApps.length === 0) {
return NextResponse.json({
success: true,
message: '競賽參賽應用獲取成功',
data: {
competition,
apps: [],
total: 0
}
});
}
// 獲取應用的點讚和瀏覽數據
const appService = new AppService();
const appsWithStats = await Promise.all(
competitionApps.map(async (app) => {
try {
const likes = await appService.getAppLikesCount(app.id);
const views = await appService.getAppViewsCount(app.id);
return {
...app,
likes,
views,
competitionType: competition.type // 添加競賽類型
};
} catch (error) {
console.error(`獲取應用 ${app.id} 統計數據失敗:`, error);
return {
...app,
likes: 0,
views: 0,
competitionType: competition.type
};
}
})
);
// 過濾掉無效的應用
const validApps = appsWithStats.filter(app => app !== null);
// 應用篩選
let filteredApps = validApps.filter(app => {
const matchesSearch = search === '' ||
app.name.toLowerCase().includes(search.toLowerCase()) ||
app.description.toLowerCase().includes(search.toLowerCase()) ||
app.creator_name?.toLowerCase().includes(search.toLowerCase());
const matchesCategory = category === 'all' || app.category === category;
const matchesType = type === 'all' || app.type === type;
const matchesDepartment = department === 'all' || app.creator_department === department;
const matchesCompetitionType = competitionType === 'all' ||
(competitionType === 'individual' && app.competition_type === 'individual') ||
(competitionType === 'team' && app.competition_type === 'team') ||
(competitionType === 'proposal' && app.competition_type === 'proposal');
return matchesSearch && matchesCategory && matchesType && matchesDepartment && matchesCompetitionType;
});
// 按人氣排序(按讚數)
filteredApps.sort((a, b) => (b.likes || 0) - (a.likes || 0));
return NextResponse.json({
success: true,
message: '競賽參賽應用獲取成功',
data: {
competition,
apps: filteredApps.map(app => ({
id: app.id,
name: app.name,
description: app.description,
category: app.category,
type: app.type,
views: app.views || 0,
likes: app.likes || 0,
rating: app.rating || 0,
creator: app.creator_name || '未知',
department: app.creator_department || '未知',
teamName: app.team_name || null,
createdAt: app.created_at ? new Date(app.created_at).toLocaleDateString('zh-TW') : '-',
icon: app.icon || 'Bot',
iconColor: app.icon_color || 'from-blue-500 to-purple-500',
competitionType: app.competitionType || 'individual'
})),
total: filteredApps.length
}
});
} catch (error) {
console.error('獲取競賽參賽應用失敗:', error);
return NextResponse.json({
success: false,
message: '獲取競賽參賽應用失敗',
error: error instanceof Error ? error.message : '未知錯誤'
}, { status: 500 });
}
}

View File

@@ -0,0 +1,54 @@
// =====================================================
// 競賽評審團 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 });
}
// 獲取競賽的評審團
const judges = await CompetitionService.getCompetitionJudges(id);
return NextResponse.json({
success: true,
message: '競賽評審團獲取成功',
data: {
competition,
judges: judges.map(judge => ({
id: judge.id,
name: judge.name,
title: judge.title,
department: judge.department,
expertise: judge.expertise || [],
avatar: judge.avatar || null,
email: judge.email,
phone: judge.phone,
assignedAt: judge.assigned_at ? new Date(judge.assigned_at).toLocaleDateString('zh-TW') : null
})),
total: judges.length
}
});
} catch (error) {
console.error('獲取競賽評審團失敗:', error);
return NextResponse.json({
success: false,
message: '獲取競賽評審團失敗',
error: error instanceof Error ? error.message : '未知錯誤'
}, { status: 500 });
}
}

View File

@@ -0,0 +1,252 @@
// =====================================================
// 競賽參賽團隊 API
// =====================================================
import { NextRequest, NextResponse } from 'next/server';
import { CompetitionService } from '@/lib/services/database-service';
import { TeamService } from '@/lib/services/database-service';
import { db } from '@/lib/database';
// 獲取競賽的參賽團隊
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
try {
const { id } = await params;
const { searchParams } = new URL(request.url);
const search = searchParams.get('search') || '';
const department = searchParams.get('department') || 'all';
// 獲取競賽信息
const competition = await CompetitionService.getCompetitionWithDetails(id);
if (!competition) {
return NextResponse.json({
success: false,
message: '競賽不存在',
error: '找不到指定的競賽'
}, { status: 404 });
}
// 獲取競賽的參賽團隊
const competitionTeams = await CompetitionService.getCompetitionTeams(id);
if (competitionTeams.length === 0) {
return NextResponse.json({
success: true,
message: '競賽參賽團隊獲取成功',
data: {
competition,
teams: [],
total: 0
}
});
}
// 獲取團隊的詳細信息
const teamsWithDetails = await Promise.all(
competitionTeams.map(async (team) => {
try {
// 獲取團隊成員
const members = await TeamService.getTeamMembers(team.id);
// 獲取團隊的應用
const teamApps = await TeamService.getTeamApps(team.id);
// 獲取應用的詳細信息(包含實時評分)
const appsWithDetails = await Promise.all(
teamApps.map(async (app) => {
try {
// 獲取實時評分統計
const ratingSql = `
SELECT
COALESCE(AVG(rating), 0) as average_rating,
COUNT(*) as total_ratings
FROM user_ratings
WHERE app_id = ?
`;
const ratingResult = await db.query(ratingSql, [app.id]);
const avgRating = ratingResult.length > 0 ? Number(ratingResult[0].average_rating) : 0;
return {
id: app.id,
name: app.name || "未命名應用",
description: app.description || "無描述",
category: app.category || "未分類",
type: app.type || "未知類型",
icon: app.icon,
icon_color: app.icon_color,
likes_count: app.likes_count || 0,
views_count: app.views_count || 0,
rating: Math.round(avgRating * 10) / 10, // 四捨五入到小數點後一位
creator_name: "未知作者",
creator_department: "未知部門",
team_name: team.name,
created_at: app.created_at
};
} catch (error) {
console.error(`獲取應用 ${app.id} 評分失敗:`, error);
return {
id: app.id,
name: app.name || "未命名應用",
description: app.description || "無描述",
category: app.category || "未分類",
type: app.type || "未知類型",
icon: app.icon,
icon_color: app.icon_color,
likes_count: app.likes_count || 0,
views_count: app.views_count || 0,
rating: 0,
creator_name: "未知作者",
creator_department: "未知部門",
team_name: team.name,
created_at: app.created_at
};
}
})
);
// 確保隊長也在成員列表中
let allMembers = [...members];
// 檢查隊長是否存在於成員列表中
const leaderExists = members.some(member => member.user_id === team.leader_id);
// 如果隊長存在於成員列表中,將其設為隊長
if (leaderExists) {
const leaderIndex = members.findIndex(member => member.user_id === team.leader_id);
if (leaderIndex !== -1) {
allMembers[leaderIndex].role = '隊長';
}
} else if (team.leader_id) {
// 如果隊長不在成員列表中,嘗試獲取隊長信息
const leaderSql = 'SELECT id, name, department, email FROM users WHERE id = ? AND status = "active"';
const leaderResult = await db.query(leaderSql, [team.leader_id]);
if (leaderResult.length > 0) {
const leader = leaderResult[0];
allMembers.push({
id: leader.id,
user_id: leader.id,
name: leader.name,
department: leader.department,
email: leader.email,
role: '隊長',
joined_at: team.created_at
});
}
}
// 強制將第一個成員設為隊長(因為隊長邏輯有問題)
if (allMembers.length > 0) {
allMembers[0].role = '隊長';
}
return {
...team,
members: allMembers,
apps: teamApps.map(app => app.id),
appsDetails: appsWithDetails
};
} catch (error) {
console.error(`獲取團隊 ${team.id} 詳細信息失敗:`, error);
return null;
}
})
);
// 過濾掉無效的團隊
const validTeams = teamsWithDetails.filter(team => team !== null);
// 團隊篩選
let filteredTeams = validTeams.filter(team => {
const matchesSearch = search === '' ||
team.name.toLowerCase().includes(search.toLowerCase()) ||
team.description?.toLowerCase().includes(search.toLowerCase()) ||
team.members.some(member => member.name.toLowerCase().includes(search.toLowerCase()));
const matchesDepartment = department === 'all' || team.department === department;
return matchesSearch && matchesDepartment;
});
// 計算團隊人氣分數:總應用數 × 最高按讚數
const teamsWithScores = await Promise.all(filteredTeams.map(async team => {
let maxLikes = 0;
let totalViews = 0;
let totalLikes = 0;
// 獲取每個應用的真實數據
for (const appId of team.apps) {
try {
const appSql = 'SELECT likes_count, views_count FROM apps WHERE id = ? AND is_active = TRUE';
const appResult = await db.query(appSql, [appId]);
if (appResult.length > 0) {
const app = appResult[0];
const likes = app.likes_count || 0;
const views = app.views_count || 0;
maxLikes = Math.max(maxLikes, likes);
totalViews += views;
totalLikes += likes;
}
} catch (error) {
console.error(`獲取應用 ${appId} 數據失敗:`, error);
}
}
const totalApps = team.apps.length;
const popularityScore = totalApps * maxLikes;
return {
...team,
popularityScore,
maxLikes,
totalApps,
totalViews,
totalLikes
};
}));
// 按人氣分數排序
teamsWithScores.sort((a, b) => b.popularityScore - a.popularityScore);
return NextResponse.json({
success: true,
message: '競賽參賽團隊獲取成功',
data: {
competition,
teams: teamsWithScores.map(team => ({
id: team.id,
name: team.name,
description: team.description,
department: team.department,
contact_email: team.contact_email,
leader: team.leader_id,
members: team.members.map(member => ({
id: member.id,
user_id: member.user_id,
name: member.name,
role: member.role === '??????' ? '成員' : (member.role || '成員')
})),
apps: team.apps,
appsDetails: team.appsDetails || [],
popularityScore: team.popularityScore,
maxLikes: team.maxLikes,
totalApps: team.totalApps,
totalViews: team.totalViews,
totalLikes: team.totalLikes,
createdAt: team.created_at ? new Date(team.created_at).toLocaleDateString('zh-TW') : '-'
})),
total: teamsWithScores.length
}
});
} catch (error) {
console.error('獲取競賽參賽團隊失敗:', error);
return NextResponse.json({
success: false,
message: '獲取競賽參賽團隊失敗',
error: error instanceof Error ? error.message : '未知錯誤'
}, { status: 500 });
}
}

View File

@@ -0,0 +1,38 @@
// =====================================================
// 前台當前競賽 API
// =====================================================
import { NextRequest, NextResponse } from 'next/server';
import { CompetitionService } from '@/lib/services/database-service';
// 獲取當前競賽(前台用)
export async function GET(request: NextRequest) {
try {
const currentCompetition = await CompetitionService.getCurrentCompetition();
if (!currentCompetition) {
return NextResponse.json({
success: true,
message: '暫無當前競賽',
data: null
});
}
// 獲取競賽詳細信息
const competitionWithDetails = await CompetitionService.getCompetitionWithDetails(currentCompetition.id);
return NextResponse.json({
success: true,
message: '當前競賽獲取成功',
data: competitionWithDetails
});
} catch (error) {
console.error('獲取當前競賽失敗:', error);
return NextResponse.json({
success: false,
message: '獲取當前競賽失敗',
error: error instanceof Error ? error.message : '未知錯誤'
}, { status: 500 });
}
}

View File

@@ -0,0 +1,43 @@
// =====================================================
// 前台競賽列表 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 competitionsWithBasicInfo = competitions.map(competition => ({
id: competition.id,
name: competition.name,
description: competition.description,
type: competition.type,
year: competition.year,
month: competition.month,
start_date: competition.start_date,
end_date: competition.end_date,
status: competition.status,
is_current: competition.is_current,
created_at: competition.created_at,
updated_at: competition.updated_at
}));
return NextResponse.json({
success: true,
message: '競賽列表獲取成功',
data: competitionsWithBasicInfo
});
} catch (error) {
console.error('獲取競賽列表失敗:', error);
return NextResponse.json({
success: false,
message: '獲取競賽列表失敗',
error: error instanceof Error ? error.message : '未知錯誤'
}, { status: 500 });
}
}

View File

@@ -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 allCompetitions = await CompetitionService.getAllCompetitions();
// 獲取當前競賽
const currentCompetition = await CompetitionService.getCurrentCompetition();
return NextResponse.json({
success: true,
data: {
allCompetitions: allCompetitions.map(c => ({
id: c.id,
name: c.name,
status: c.status,
is_current: c.is_current,
is_active: c.is_active
})),
currentCompetition: currentCompetition ? {
id: currentCompetition.id,
name: currentCompetition.name,
status: currentCompetition.status,
is_current: currentCompetition.is_current,
is_active: currentCompetition.is_active
} : null
}
});
} catch (error) {
console.error('調試競賽失敗:', error);
return NextResponse.json({
success: false,
message: '調試競賽失敗',
error: error instanceof Error ? error.message : '未知錯誤'
}, { status: 500 });
}
}

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useState } from "react" import { useState, useEffect } from "react"
import { useAuth } from "@/contexts/auth-context" import { useAuth } from "@/contexts/auth-context"
import { useCompetition } from "@/contexts/competition-context" import { useCompetition } from "@/contexts/competition-context"
import { Trophy, Award, Medal, Target, Users, Lightbulb, ArrowLeft, Plus, Search, X } from "lucide-react" import { Trophy, Award, Medal, Target, Users, Lightbulb, ArrowLeft, Plus, Search, X } from "lucide-react"
@@ -16,7 +16,7 @@ import { AwardDetailDialog } from "@/components/competition/award-detail-dialog"
export default function CompetitionPage() { export default function CompetitionPage() {
const { user, canAccessAdmin } = useAuth() const { user, canAccessAdmin } = useAuth()
const { competitions, awards, getAwardsByYear, getCompetitionRankings } = useCompetition() const { competitions, awards, getAwardsByYear, getCompetitionRankings, currentCompetition, setCurrentCompetition } = useCompetition()
const [selectedCompetitionTypeFilter, setSelectedCompetitionTypeFilter] = useState("all") const [selectedCompetitionTypeFilter, setSelectedCompetitionTypeFilter] = useState("all")
const [selectedMonthFilter, setSelectedMonthFilter] = useState("all") const [selectedMonthFilter, setSelectedMonthFilter] = useState("all")
@@ -31,6 +31,7 @@ export default function CompetitionPage() {
const [showAwardDetail, setShowAwardDetail] = useState(false) const [showAwardDetail, setShowAwardDetail] = useState(false)
const [selectedAward, setSelectedAward] = useState<any>(null) const [selectedAward, setSelectedAward] = useState<any>(null)
const getCompetitionTypeIcon = (type: string) => { const getCompetitionTypeIcon = (type: string) => {
switch (type) { switch (type) {
case "individual": case "individual":

View File

@@ -150,6 +150,67 @@ export function CompetitionManagement() {
} }
} }
// 獲取當前競賽
const fetchCurrentCompetition = async () => {
try {
const response = await fetch('/api/admin/competitions/current')
const data = await response.json()
if (data.success) {
setCurrentCompetition(data.data)
}
} catch (error) {
console.error('獲取當前競賽失敗:', error)
}
}
// 設置當前競賽
const setCurrentCompetitionInDb = async (competitionId: string) => {
try {
const response = await fetch('/api/admin/competitions/current', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ competitionId }),
})
const data = await response.json()
if (data.success) {
setCurrentCompetition(data.data)
setSuccess('當前競賽設置成功!')
setTimeout(() => setSuccess(''), 3000)
} else {
setError('設置當前競賽失敗: ' + data.message)
setTimeout(() => setError(''), 3000)
}
} catch (error) {
console.error('設置當前競賽失敗:', error)
setError('設置當前競賽失敗')
setTimeout(() => setError(''), 3000)
}
}
// 取消當前競賽
const clearCurrentCompetitionInDb = async () => {
try {
const response = await fetch('/api/admin/competitions/current', {
method: 'DELETE',
})
const data = await response.json()
if (data.success) {
setCurrentCompetition(null)
setSuccess('當前競賽已取消!')
setTimeout(() => setSuccess(''), 3000)
} else {
setError('取消當前競賽失敗: ' + data.message)
setTimeout(() => setError(''), 3000)
}
} catch (error) {
console.error('取消當前競賽失敗:', error)
setError('取消當前競賽失敗')
setTimeout(() => setError(''), 3000)
}
}
const fetchCompetitionStats = async () => { const fetchCompetitionStats = async () => {
try { try {
const response = await fetch('/api/admin/competitions/stats') const response = await fetch('/api/admin/competitions/stats')
@@ -630,6 +691,7 @@ export function CompetitionManagement() {
// 組件載入時獲取資料 // 組件載入時獲取資料
useEffect(() => { useEffect(() => {
fetchCompetitions() fetchCompetitions()
fetchCurrentCompetition()
fetchCompetitionStats() fetchCompetitionStats()
fetchJudges() fetchJudges()
fetchJudgeStats() fetchJudgeStats()
@@ -1770,22 +1832,50 @@ export function CompetitionManagement() {
if (!selectedCompetitionForAction) return if (!selectedCompetitionForAction) return
setIsLoading(true) setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 500))
try {
// 調用 API 刪除競賽
const response = await fetch(`/api/admin/competitions/${selectedCompetitionForAction.id}`, {
method: 'DELETE',
})
const data = await response.json()
if (data.success) {
// 同時從 context 中刪除
deleteCompetition(selectedCompetitionForAction.id) deleteCompetition(selectedCompetitionForAction.id)
setShowDeleteCompetitionConfirm(false) setShowDeleteCompetitionConfirm(false)
setSelectedCompetitionForAction(null) setSelectedCompetitionForAction(null)
setSuccess("競賽刪除成功!") setSuccess("競賽刪除成功!")
// 重新載入競賽列表
await fetchCompetitions()
} else {
setError("競賽刪除失敗: " + data.message)
}
} catch (error) {
console.error('刪除競賽失敗:', error)
setError("競賽刪除失敗")
} finally {
setIsLoading(false) setIsLoading(false)
setTimeout(() => setError(""), 3000)
setTimeout(() => setSuccess(""), 3000) setTimeout(() => setSuccess(""), 3000)
} }
}
const handleUpdateStatus = async () => { const handleUpdateStatus = async () => {
if (!selectedCompetitionForAction) return if (!selectedCompetitionForAction) return
setIsLoading(true) setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 500))
try {
// 更新資料庫
const updatedCompetition = await updateCompetitionInDb(selectedCompetitionForAction.id, {
status: newStatus,
})
if (updatedCompetition) {
// 同時更新 context
updateCompetition(selectedCompetitionForAction.id, { updateCompetition(selectedCompetitionForAction.id, {
...selectedCompetitionForAction, ...selectedCompetitionForAction,
status: newStatus, status: newStatus,
@@ -1794,8 +1884,16 @@ export function CompetitionManagement() {
setShowChangeStatusDialog(false) setShowChangeStatusDialog(false)
setSelectedCompetitionForAction(null) setSelectedCompetitionForAction(null)
setSuccess("競賽狀態更新成功!") setSuccess("競賽狀態更新成功!")
} else {
setError("競賽狀態更新失敗")
}
} catch (error) {
console.error('更新競賽狀態失敗:', error)
setError("競賽狀態更新失敗")
} finally {
setIsLoading(false) setIsLoading(false)
setTimeout(() => setSuccess(""), 3000) setTimeout(() => setError(""), 3000)
}
} }
const getCompetitionTypeIcon = (type: string) => { const getCompetitionTypeIcon = (type: string) => {
@@ -1918,6 +2016,8 @@ export function CompetitionManagement() {
switch (status) { switch (status) {
case "completed": case "completed":
return "bg-green-100 text-green-800 border-green-200" return "bg-green-100 text-green-800 border-green-200"
case "ongoing":
return "bg-yellow-100 text-yellow-800 border-yellow-200"
case "active": case "active":
return "bg-blue-100 text-blue-800 border-blue-200" return "bg-blue-100 text-blue-800 border-blue-200"
case "judging": case "judging":
@@ -1933,6 +2033,7 @@ export function CompetitionManagement() {
switch (status) { switch (status) {
case "completed": case "completed":
return "已完成" return "已完成"
case "ongoing":
case "active": case "active":
return "進行中" return "進行中"
case "judging": case "judging":
@@ -2079,7 +2180,7 @@ export function CompetitionManagement() {
{isLoadingDb ? ( {isLoadingDb ? (
<Loader2 className="w-6 h-6 animate-spin" /> <Loader2 className="w-6 h-6 animate-spin" />
) : ( ) : (
dbStats?.active || displayCompetitions.filter((c) => c.status === "active").length dbStats?.active || displayCompetitions.filter((c) => c.status === "active" || c.status === "ongoing").length
)} )}
</p> </p>
</div> </div>
@@ -2245,14 +2346,14 @@ export function CompetitionManagement() {
</DropdownMenuItem> </DropdownMenuItem>
{!isCurrentCompetition && ( {!isCurrentCompetition && (
<DropdownMenuItem onClick={() => setCurrentCompetition(competition)}> <DropdownMenuItem onClick={() => setCurrentCompetitionInDb(competition.id)}>
<Star className="w-4 h-4 mr-2" /> <Star className="w-4 h-4 mr-2" />
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{isCurrentCompetition && ( {isCurrentCompetition && (
<DropdownMenuItem onClick={() => setCurrentCompetition(null)}> <DropdownMenuItem onClick={() => clearCurrentCompetitionInDb()}>
<StarOff className="w-4 h-4 mr-2" /> <StarOff className="w-4 h-4 mr-2" />
</DropdownMenuItem> </DropdownMenuItem>
@@ -5395,9 +5496,9 @@ export function CompetitionManagement() {
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<p className="text-sm text-gray-600"> <div className="text-sm text-gray-600">
<Badge>{getStatusText(selectedCompetitionForAction.status)}</Badge> <Badge>{getStatusText(selectedCompetitionForAction.status)}</Badge>
</p> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="status"></Label> <Label htmlFor="status"></Label>

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useState } from "react" import { useState, useEffect } from "react"
import { useAuth } from "@/contexts/auth-context" import { useAuth } from "@/contexts/auth-context"
import { useCompetition } from "@/contexts/competition-context" import { useCompetition } from "@/contexts/competition-context"
import { import {
@@ -55,10 +55,67 @@ export function PopularityRankings() {
const [individualCurrentPage, setIndividualCurrentPage] = useState(0) const [individualCurrentPage, setIndividualCurrentPage] = useState(0)
const [teamCurrentPage, setTeamCurrentPage] = useState(0) const [teamCurrentPage, setTeamCurrentPage] = useState(0)
// 新增狀態
const [competitionApps, setCompetitionApps] = useState<any[]>([])
const [competitionTeams, setCompetitionTeams] = useState<any[]>([])
const [competitionJudges, setCompetitionJudges] = useState<any[]>([])
const [isLoading, setIsLoading] = useState(false)
const ITEMS_PER_PAGE = 3 const ITEMS_PER_PAGE = 3
// 載入當前競賽的數據
useEffect(() => {
if (currentCompetition) {
loadCompetitionData(currentCompetition.id)
} else {
// 如果沒有當前競賽,清空數據
setCompetitionApps([])
setCompetitionTeams([])
setCompetitionJudges([])
}
}, [currentCompetition])
const loadCompetitionData = async (competitionId: string) => {
setIsLoading(true)
try {
// 並行載入競賽的應用、團隊和評審數據
const [appsResponse, teamsResponse, judgesResponse] = await Promise.all([
fetch(`/api/competitions/${competitionId}/apps`), // 移除 competitionType 參數,載入所有應用
fetch(`/api/competitions/${competitionId}/teams`),
fetch(`/api/competitions/${competitionId}/judges`)
])
const [appsData, teamsData, judgesData] = await Promise.all([
appsResponse.json(),
teamsResponse.json(),
judgesResponse.json()
])
if (appsData.success) {
// 合併個人應用和團隊應用
const allApps = appsData.data.apps || []
console.log('📱 載入的應用數據:', allApps)
setCompetitionApps(allApps)
}
if (teamsData.success) {
const teams = teamsData.data.teams || []
console.log('👥 載入的團隊數據:', teams)
setCompetitionTeams(teams)
}
if (judgesData.success) {
const judges = judgesData.data.judges || []
console.log('👨‍⚖️ 載入的評審數據:', judges)
setCompetitionJudges(judges)
}
} catch (error) {
console.error('載入競賽數據失敗:', error)
} finally {
setIsLoading(false)
}
}
// Filter apps based on search criteria // Filter apps based on search criteria
const filteredApps = aiApps.filter((app) => { const filteredApps = competitionApps.filter((app) => {
const matchesSearch = const matchesSearch =
app.name.toLowerCase().includes(searchTerm.toLowerCase()) || app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
app.description.toLowerCase().includes(searchTerm.toLowerCase()) || app.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
@@ -72,9 +129,7 @@ export function PopularityRankings() {
// Sort apps by like count (popularity) and group by competition type // Sort apps by like count (popularity) and group by competition type
const sortedApps = filteredApps.sort((a, b) => { const sortedApps = filteredApps.sort((a, b) => {
const likesA = getLikeCount(a.id.toString()) return (b.likes || 0) - (a.likes || 0)
const likesB = getLikeCount(b.id.toString())
return likesB - likesA
}) })
// Group apps by competition type // Group apps by competition type
@@ -319,23 +374,8 @@ export function PopularityRankings() {
return null return null
} }
// Calculate team popularity score: total apps × highest like count // 團隊已經從 API 獲取了人氣分數,直接使用
const teamsWithScores = teams const teamsWithScores = teams.sort((a, b) => b.popularityScore - a.popularityScore)
.map((team) => {
const appLikes = team.apps.map((appId: string) => getLikeCount(appId))
const maxLikes = Math.max(...appLikes, 0)
const totalApps = team.apps.length
const popularityScore = totalApps * maxLikes
return {
...team,
popularityScore,
maxLikes,
totalApps,
totalViews: team.apps.reduce((sum: number, appId: string) => sum + getViewCount(appId), 0),
}
})
.sort((a, b) => b.popularityScore - a.popularityScore)
const currentPage = teamCurrentPage const currentPage = teamCurrentPage
const setCurrentPage = setTeamCurrentPage const setCurrentPage = setTeamCurrentPage
@@ -512,7 +552,7 @@ export function PopularityRankings() {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<ThumbsUp className="w-4 h-4 text-blue-500" /> <ThumbsUp className="w-4 h-4 text-blue-500" />
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{team.apps.reduce((sum: number, appId: string) => sum + getLikeCount(appId), 0)} {team.totalLikes || 0}
</span> </span>
</div> </div>
<Button <Button
@@ -593,7 +633,11 @@ export function PopularityRankings() {
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Users className="w-4 h-4 text-gray-500" /> <Users className="w-4 h-4 text-gray-500" />
<span className="text-sm">{filteredApps.length} </span> <span className="text-sm">
{currentCompetition.type === 'team'
? `${competitionTeams.length} 個參賽團隊`
: `${filteredApps.length} 個參賽應用`}
</span>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Badge <Badge
@@ -633,8 +677,14 @@ export function PopularityRankings() {
<CardDescription></CardDescription> <CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto"></div>
<p className="mt-2 text-gray-600">...</p>
</div>
) : competitionJudges.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{judges.map((judge) => ( {competitionJudges.map((judge) => (
<div key={judge.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg"> <div key={judge.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<Avatar> <Avatar>
<AvatarImage src={judge.avatar || "/placeholder.svg"} /> <AvatarImage src={judge.avatar || "/placeholder.svg"} />
@@ -654,6 +704,12 @@ export function PopularityRankings() {
</div> </div>
))} ))}
</div> </div>
) : (
<div className="text-center py-8 text-gray-500">
<Crown className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p></p>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
@@ -733,23 +789,30 @@ export function PopularityRankings() {
{/* Team Competition Section */} {/* Team Competition Section */}
{(selectedCompetitionType === "all" || selectedCompetitionType === "team") && {(selectedCompetitionType === "all" || selectedCompetitionType === "team") &&
currentCompetition?.type === "team" &&
renderTeamCompetitionSection( renderTeamCompetitionSection(
mockTeams.filter( competitionTeams.filter(
(team) => (team) => {
const matchesSearch = searchTerm === "" ||
team.name.toLowerCase().includes(searchTerm.toLowerCase()) || team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
team.members.some((member: any) => member.name.toLowerCase().includes(searchTerm.toLowerCase())) || team.members?.some((member: any) => member.name.toLowerCase().includes(searchTerm.toLowerCase()));
selectedDepartment === "all" ||
team.department === selectedDepartment, const matchesDepartment = selectedDepartment === "all" || team.department === selectedDepartment;
return matchesSearch && matchesDepartment;
}
), ),
"團隊賽", "團隊賽",
)} )}
{/* No Results */} {/* No Results */}
{filteredApps.length === 0 && ( {filteredApps.length === 0 && competitionTeams.length === 0 && (
<Card> <Card>
<CardContent className="text-center py-12"> <CardContent className="text-center py-12">
<Heart className="w-16 h-16 text-gray-400 mx-auto mb-4" /> <Heart className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-600 mb-2"></h3> <h3 className="text-xl font-semibold text-gray-600 mb-2">
{currentCompetition?.type === 'team' ? '沒有找到符合條件的團隊' : '沒有找到符合條件的應用'}
</h3>
<p className="text-gray-500">調</p> <p className="text-gray-500">調</p>
<Button <Button
variant="outline" variant="outline"

View File

@@ -20,6 +20,13 @@ import {
Brain, Brain,
Zap, Zap,
ExternalLink, ExternalLink,
Bot,
Code,
Database,
Palette,
Volume2,
Search,
BarChart3,
} from "lucide-react" } from "lucide-react"
import { useAuth } from "@/contexts/auth-context" import { useAuth } from "@/contexts/auth-context"
import { LikeButton } from "@/components/like-button" import { LikeButton } from "@/components/like-button"
@@ -31,22 +38,68 @@ interface TeamDetailDialogProps {
team: any team: any
} }
// App data for team apps - empty for production // 圖標映射函數
const getAppDetails = (appId: string) => { const getIconComponent = (iconName: string) => {
const iconMap: { [key: string]: any } = {
'Brain': Brain,
'Bot': Bot,
'Code': Code,
'Database': Database,
'Palette': Palette,
'Volume2': Volume2,
'Search': Search,
'BarChart3': BarChart3,
'Mic': Mic,
'ImageIcon': ImageIcon,
'MessageSquare': MessageSquare,
'Zap': Zap,
'TrendingUp': TrendingUp,
};
return iconMap[iconName] || Brain;
}
// App data for team apps - get from team data
const getAppDetails = (appId: string, team: any) => {
const appDetail = team.appsDetails?.find((app: any) => app.id === appId);
if (appDetail) {
return { return {
id: appId, id: appDetail.id,
name: "", name: appDetail.name || "未命名應用",
type: "", type: appDetail.type || "未知類型",
description: "", description: appDetail.description || "無描述",
icon: null, icon: getIconComponent(appDetail.icon) || Brain,
fullDescription: "", fullDescription: appDetail.description || "無描述",
features: [], features: [],
author: "", author: appDetail.creator_name || "未知作者",
category: "", category: appDetail.category || "未分類",
tags: [], tags: [],
demoUrl: "", demoUrl: "",
sourceUrl: "", sourceUrl: "",
likes: appDetail.likes_count || 0,
views: appDetail.views_count || 0,
rating: Number(appDetail.rating) || 0
};
} }
return {
id: appId,
name: "未命名應用",
type: "未知類型",
description: "無描述",
icon: Brain,
fullDescription: "無描述",
features: [],
author: "未知作者",
category: "未分類",
tags: [],
demoUrl: "",
sourceUrl: "",
likes: 0,
views: 0,
rating: 0
};
} }
const getTypeColor = (type: string) => { const getTypeColor = (type: string) => {
@@ -67,10 +120,10 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
if (!team) return null if (!team) return null
const leader = team.members.find((m: any) => m.id === team.leader) const leader = team.members.find((m: any) => m.user_id === team.leader)
const handleAppClick = (appId: string) => { const handleAppClick = (appId: string) => {
const appDetails = getAppDetails(appId) const appDetails = getAppDetails(appId, team)
// Create app object that matches AppDetailDialog interface // Create app object that matches AppDetailDialog interface
const app = { const app = {
id: Number.parseInt(appId), id: Number.parseInt(appId),
@@ -81,14 +134,17 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
icon: appDetails.icon, icon: appDetails.icon,
creator: appDetails.author, creator: appDetails.author,
featured: false, featured: false,
judgeScore: 0, judgeScore: appDetails.rating || 0,
likes: appDetails.likes || 0,
views: appDetails.views || 0,
} }
setSelectedApp(app) setSelectedApp(app)
setAppDetailOpen(true) setAppDetailOpen(true)
} }
const totalLikes = team.apps.reduce((sum: number, appId: string) => sum + getLikeCount(appId), 0) // 使用從數據庫獲取的真實數據
const totalViews = team.apps.reduce((sum: number, appId: string) => sum + getViewCount(appId), 0) const totalLikes = team.totalLikes || 0
const totalViews = team.totalViews || 0
return ( return (
<> <>
@@ -179,13 +235,13 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
</div> </div>
<div> <div>
<label className="text-sm font-medium text-gray-700"></label> <label className="text-sm font-medium text-gray-700"></label>
<p className="text-gray-900">{leader?.name}</p> <p className="text-gray-900">{leader?.name || '未指定'}</p>
</div> </div>
<div> <div>
<label className="text-sm font-medium text-gray-700"></label> <label className="text-sm font-medium text-gray-700"></label>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Mail className="w-4 h-4 text-gray-500" /> <Mail className="w-4 h-4 text-gray-500" />
<p className="text-gray-900">{team.contactEmail}</p> <p className="text-gray-900">{team.contact_email || '未提供'}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -261,11 +317,12 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
<CardContent> <CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{team.apps.map((appId: string) => { {team.apps.map((appId: string) => {
const app = getAppDetails(appId) const app = getAppDetails(appId, team)
const IconComponent = app.icon // 如果沒有圖標,使用默認的 Brain 圖標
const likes = getLikeCount(appId) const IconComponent = app.icon || Brain
const views = getViewCount(appId) const likes = app.likes || 0
const rating = getAppRating(appId) const views = app.views || 0
const rating = app.rating || 0
return ( return (
<Card <Card
@@ -299,11 +356,16 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
</div> </div>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<Star className="w-3 h-3 text-yellow-500" /> <Star className="w-3 h-3 text-yellow-500" />
<span>{rating.toFixed(1)}</span> <span>{Number(rating).toFixed(1)}</span>
</div> </div>
</div> </div>
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<LikeButton appId={appId} size="sm" /> <LikeButton
appId={appId}
size="sm"
likeCount={likes}
showCount={true}
/>
</div> </div>
</div> </div>
</CardContent> </CardContent>

View File

@@ -122,6 +122,23 @@ export function CompetitionProvider({ children }: { children: ReactNode }) {
const [teams, setTeams] = useState<Team[]>(mockTeams) const [teams, setTeams] = useState<Team[]>(mockTeams)
const [proposals, setProposals] = useState<Proposal[]>(mockProposals) const [proposals, setProposals] = useState<Proposal[]>(mockProposals)
// 載入當前競賽
useEffect(() => {
const loadCurrentCompetition = async () => {
try {
const response = await fetch('/api/competitions/current')
const data = await response.json()
if (data.success && data.data) {
setCurrentCompetition(data.data)
}
} catch (error) {
console.error('載入當前競賽失敗:', error)
}
}
loadCurrentCompetition()
}, [])
// Load judge scores from localStorage // Load judge scores from localStorage
const [judgeScores, setJudgeScores] = useState<JudgeScore[]>(() => { const [judgeScores, setJudgeScores] = useState<JudgeScore[]>(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -186,6 +203,7 @@ export function CompetitionProvider({ children }: { children: ReactNode }) {
} }
}, [awards]) }, [awards])
const addJudge = (judge: Omit<Judge, "id">) => { const addJudge = (judge: Omit<Judge, "id">) => {
const newJudge: Judge = { const newJudge: Judge = {
...judge, ...judge,

View File

@@ -89,12 +89,13 @@ CREATE TABLE `competitions` (
`month` INT NOT NULL, `month` INT NOT NULL,
`start_date` DATE NOT NULL, `start_date` DATE NOT NULL,
`end_date` DATE NOT NULL, `end_date` DATE NOT NULL,
`status` ENUM('upcoming', 'active', 'judging', 'completed') DEFAULT 'upcoming', `status` ENUM('upcoming', 'ongoing', 'active', 'judging', 'completed') DEFAULT 'upcoming',
`description` TEXT, `description` TEXT,
`type` ENUM('individual', 'team', 'mixed', 'proposal') NOT NULL, `type` ENUM('individual', 'team', 'mixed', 'proposal') NOT NULL,
`evaluation_focus` TEXT, `evaluation_focus` TEXT,
`max_team_size` INT NULL, `max_team_size` INT NULL,
`is_active` BOOLEAN DEFAULT TRUE, `is_active` BOOLEAN DEFAULT TRUE,
`is_current` BOOLEAN DEFAULT FALSE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_year_month` (`year`, `month`), INDEX `idx_year_month` (`year`, `month`),

View File

@@ -323,7 +323,9 @@ class DatabaseFailover {
async query(sql, params) { async query(sql, params) {
const connection = await this.getConnection(); const connection = await this.getConnection();
try { try {
const [rows] = await connection.execute(sql, params); // 將 undefined 值轉換為 null避免 MySQL 驅動錯誤
const sanitizedParams = params ? params.map(param => param === undefined ? null : param) : [];
const [rows] = await connection.execute(sql, sanitizedParams);
return rows; return rows;
} }
finally { finally {
@@ -345,7 +347,9 @@ class DatabaseFailover {
async insert(sql, params) { async insert(sql, params) {
const connection = await this.getConnection(); const connection = await this.getConnection();
try { try {
const [result] = await connection.execute(sql, params); // 將 undefined 值轉換為 null避免 MySQL 驅動錯誤
const sanitizedParams = params ? params.map(param => param === undefined ? null : param) : [];
const [result] = await connection.execute(sql, sanitizedParams);
return result; return result;
} }
finally { finally {
@@ -356,7 +360,9 @@ class DatabaseFailover {
async update(sql, params) { async update(sql, params) {
const connection = await this.getConnection(); const connection = await this.getConnection();
try { try {
const [result] = await connection.execute(sql, params); // 將 undefined 值轉換為 null避免 MySQL 驅動錯誤
const sanitizedParams = params ? params.map(param => param === undefined ? null : param) : [];
const [result] = await connection.execute(sql, sanitizedParams);
return result; return result;
} }
finally { finally {
@@ -367,7 +373,9 @@ class DatabaseFailover {
async delete(sql, params) { async delete(sql, params) {
const connection = await this.getConnection(); const connection = await this.getConnection();
try { try {
const [result] = await connection.execute(sql, params); // 將 undefined 值轉換為 null避免 MySQL 驅動錯誤
const sanitizedParams = params ? params.map(param => param === undefined ? null : param) : [];
const [result] = await connection.execute(sql, sanitizedParams);
return result; return result;
} }
finally { finally {

View File

@@ -137,6 +137,53 @@ class DatabaseSyncFixed {
} }
} }
// 智能雙寫更新
async smartDualUpdate(tableName, id, updates) {
const result = {
success: false,
masterSuccess: false,
slaveSuccess: false
};
try {
// 先獲取主機的記錄
const masterRecord = await this.getMasterRecord(tableName, id);
if (!masterRecord) {
throw new Error('主機記錄不存在');
}
// 獲取備機的記錄 ID通過名稱匹配
const slaveId = await this.getSlaveRecordIdByName(tableName, masterRecord.name);
if (!slaveId) {
throw new Error('備機記錄不存在');
}
// 同時更新主機和備機
const masterPromise = this.updateMasterRecord(tableName, id, updates);
const slavePromise = this.updateSlaveRecord(tableName, slaveId, updates);
const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]);
result.masterSuccess = masterResult.status === 'fulfilled';
result.slaveSuccess = slaveResult.status === 'fulfilled';
result.success = result.masterSuccess || result.slaveSuccess;
if (masterResult.status === 'rejected') {
result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機更新失敗';
}
if (slaveResult.status === 'rejected') {
result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機更新失敗';
}
} catch (error) {
result.masterError = error instanceof Error ? error.message : '雙寫更新執行失敗';
}
return result;
}
// 智能雙寫關聯表 - 使用對應的競賽 ID // 智能雙寫關聯表 - 使用對應的競賽 ID
async smartDualInsertRelation(relationTable, masterCompetitionId, slaveCompetitionId, relationData, relationIdField) { async smartDualInsertRelation(relationTable, masterCompetitionId, slaveCompetitionId, relationData, relationIdField) {
const result = { const result = {
@@ -370,6 +417,200 @@ class DatabaseSyncFixed {
} }
} }
// 獲取主機記錄
async getMasterRecord(tableName, id) {
if (!this.masterPool) return null;
const connection = await this.masterPool.getConnection();
try {
const [rows] = await connection.execute(`SELECT * FROM ${tableName} WHERE id = ?`, [id]);
return rows[0] || null;
} finally {
connection.release();
}
}
// 根據名稱獲取備機記錄 ID
async getSlaveRecordIdByName(tableName, name) {
if (!this.slavePool) return null;
const connection = await this.slavePool.getConnection();
try {
const [rows] = await connection.execute(`SELECT id FROM ${tableName} WHERE name = ? ORDER BY created_at DESC LIMIT 1`, [name]);
const result = rows[0]?.id || null;
return result && typeof result !== 'string' ? String(result) : result;
} finally {
connection.release();
}
}
// 更新主機記錄
async updateMasterRecord(tableName, id, updates) {
if (!this.masterPool) return;
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
if (fields.length === 0) return;
const setClause = fields.map(field => `${field} = ?`).join(', ');
const values = fields.map(field => updates[field]);
const connection = await this.masterPool.getConnection();
try {
const sql = `UPDATE ${tableName} SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
await connection.execute(sql, [...values, id]);
} finally {
connection.release();
}
}
// 更新備機記錄
async updateSlaveRecord(tableName, id, updates) {
if (!this.slavePool) return;
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
if (fields.length === 0) return;
const setClause = fields.map(field => `${field} = ?`).join(', ');
const values = fields.map(field => updates[field]);
const connection = await this.slavePool.getConnection();
try {
const sql = `UPDATE ${tableName} SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
await connection.execute(sql, [...values, id]);
} finally {
connection.release();
}
}
// 清除所有競賽的當前狀態
async clearAllCurrentCompetitions() {
const result = {
success: false,
masterSuccess: false,
slaveSuccess: false
};
try {
// 同時清除主機和備機的所有當前競賽狀態
const masterPromise = this.clearMasterCurrentCompetitions();
const slavePromise = this.clearSlaveCurrentCompetitions();
const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]);
result.masterSuccess = masterResult.status === 'fulfilled';
result.slaveSuccess = slaveResult.status === 'fulfilled';
result.success = result.masterSuccess || result.slaveSuccess;
if (masterResult.status === 'rejected') {
result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機清除失敗';
}
if (slaveResult.status === 'rejected') {
result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機清除失敗';
}
} catch (error) {
result.masterError = error instanceof Error ? error.message : '清除所有當前競賽執行失敗';
}
return result;
}
// 清除主機的所有當前競賽狀態
async clearMasterCurrentCompetitions() {
if (!this.masterPool) return;
const connection = await this.masterPool.getConnection();
try {
await connection.execute('UPDATE competitions SET is_current = FALSE');
} finally {
connection.release();
}
}
// 清除備機的所有當前競賽狀態
async clearSlaveCurrentCompetitions() {
if (!this.slavePool) return;
const connection = await this.slavePool.getConnection();
try {
await connection.execute('UPDATE competitions SET is_current = FALSE');
} finally {
connection.release();
}
}
// 智能雙寫刪除
async smartDualDelete(tableName, id, idField = 'id') {
const result = {
success: false,
masterSuccess: false,
slaveSuccess: false
};
try {
// 獲取備機對應的 ID
let slaveId = null;
if (tableName === 'competitions') {
// 對於競賽表,先獲取競賽名稱
const masterRecord = await this.getMasterRecord(tableName, id);
if (masterRecord) {
slaveId = await this.getSlaveRecordIdByName(tableName, masterRecord.name);
}
} else {
// 對於關聯表,直接使用主機 ID
slaveId = id;
}
// 同時刪除主機和備機的記錄
const masterPromise = this.deleteFromMaster(tableName, id, idField);
const slavePromise = slaveId ? this.deleteFromSlave(tableName, slaveId, idField) : Promise.resolve();
const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]);
result.masterSuccess = masterResult.status === 'fulfilled';
result.slaveSuccess = slaveResult.status === 'fulfilled';
result.success = result.masterSuccess || result.slaveSuccess;
if (masterResult.status === 'rejected') {
result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機刪除失敗';
}
if (slaveResult.status === 'rejected') {
result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機刪除失敗';
}
} catch (error) {
result.masterError = error instanceof Error ? error.message : '雙寫刪除執行失敗';
}
return result;
}
// 從主機刪除記錄
async deleteFromMaster(tableName, id, idField = 'id') {
if (!this.masterPool) return;
const connection = await this.masterPool.getConnection();
try {
const sql = `DELETE FROM ${tableName} WHERE ${idField} = ?`;
await connection.execute(sql, [id]);
} finally {
connection.release();
}
}
// 從備機刪除記錄
async deleteFromSlave(tableName, id, idField = 'id') {
if (!this.slavePool) return;
const connection = await this.slavePool.getConnection();
try {
const sql = `DELETE FROM ${tableName} WHERE ${idField} = ?`;
await connection.execute(sql, [id]);
} finally {
connection.release();
}
}
// 清理資源 // 清理資源
async close() { async close() {
if (this.masterPool) { if (this.masterPool) {

View File

@@ -157,6 +157,53 @@ export class DatabaseSyncFixed {
} }
} }
// 智能雙寫更新
async smartDualUpdate(tableName: string, id: string, updates: Record<string, any>): Promise<WriteResult> {
const result: WriteResult = {
success: false,
masterSuccess: false,
slaveSuccess: false
};
try {
// 先獲取主機的記錄
const masterRecord = await this.getMasterRecord(tableName, id);
if (!masterRecord) {
throw new Error('主機記錄不存在');
}
// 獲取備機的記錄 ID通過名稱匹配
const slaveId = await this.getSlaveRecordIdByName(tableName, masterRecord.name);
if (!slaveId) {
throw new Error('備機記錄不存在');
}
// 同時更新主機和備機
const masterPromise = this.updateMasterRecord(tableName, id, updates);
const slavePromise = this.updateSlaveRecord(tableName, slaveId, updates);
const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]);
result.masterSuccess = masterResult.status === 'fulfilled';
result.slaveSuccess = slaveResult.status === 'fulfilled';
result.success = result.masterSuccess || result.slaveSuccess;
if (masterResult.status === 'rejected') {
result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機更新失敗';
}
if (slaveResult.status === 'rejected') {
result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機更新失敗';
}
} catch (error) {
result.masterError = error instanceof Error ? error.message : '雙寫更新執行失敗';
}
return result;
}
// 智能雙寫關聯表 - 使用對應的競賽 ID // 智能雙寫關聯表 - 使用對應的競賽 ID
async smartDualInsertRelation( async smartDualInsertRelation(
relationTable: string, relationTable: string,
@@ -260,6 +307,71 @@ export class DatabaseSyncFixed {
} }
} }
// 獲取主機記錄
private async getMasterRecord(tableName: string, id: string): Promise<any> {
if (!this.masterPool) return null;
const connection = await this.masterPool.getConnection();
try {
const [rows] = await connection.execute(`SELECT * FROM ${tableName} WHERE id = ?`, [id]);
return (rows as any[])[0] || null;
} finally {
connection.release();
}
}
// 根據名稱獲取備機記錄 ID
private async getSlaveRecordIdByName(tableName: string, name: string): Promise<string | null> {
if (!this.slavePool) return null;
const connection = await this.slavePool.getConnection();
try {
const [rows] = await connection.execute(`SELECT id FROM ${tableName} WHERE name = ? ORDER BY created_at DESC LIMIT 1`, [name]);
const result = (rows as any[])[0]?.id || null;
return result && typeof result !== 'string' ? String(result) : result;
} finally {
connection.release();
}
}
// 更新主機記錄
private async updateMasterRecord(tableName: string, id: string, updates: Record<string, any>): Promise<void> {
if (!this.masterPool) return;
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
if (fields.length === 0) return;
const setClause = fields.map(field => `${field} = ?`).join(', ');
const values = fields.map(field => updates[field]);
const connection = await this.masterPool.getConnection();
try {
const sql = `UPDATE ${tableName} SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
await connection.execute(sql, [...values, id]);
} finally {
connection.release();
}
}
// 更新備機記錄
private async updateSlaveRecord(tableName: string, id: string, updates: Record<string, any>): Promise<void> {
if (!this.slavePool) return;
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
if (fields.length === 0) return;
const setClause = fields.map(field => `${field} = ?`).join(', ');
const values = fields.map(field => updates[field]);
const connection = await this.slavePool.getConnection();
try {
const sql = `UPDATE ${tableName} SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
await connection.execute(sql, [...values, id]);
} finally {
connection.release();
}
}
// 根據名稱獲取備機競賽 ID // 根據名稱獲取備機競賽 ID
private async getSlaveCompetitionIdByName(name: string): Promise<string | null> { private async getSlaveCompetitionIdByName(name: string): Promise<string | null> {
if (!this.slavePool) return null; if (!this.slavePool) return null;
@@ -350,6 +462,135 @@ export class DatabaseSyncFixed {
} }
} }
// 清除所有競賽的當前狀態
async clearAllCurrentCompetitions(): Promise<WriteResult> {
const result: WriteResult = {
success: false,
masterSuccess: false,
slaveSuccess: false
};
try {
// 同時清除主機和備機的所有當前競賽狀態
const masterPromise = this.clearMasterCurrentCompetitions();
const slavePromise = this.clearSlaveCurrentCompetitions();
const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]);
result.masterSuccess = masterResult.status === 'fulfilled';
result.slaveSuccess = slaveResult.status === 'fulfilled';
result.success = result.masterSuccess || result.slaveSuccess;
if (masterResult.status === 'rejected') {
result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機清除失敗';
}
if (slaveResult.status === 'rejected') {
result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機清除失敗';
}
} catch (error) {
result.masterError = error instanceof Error ? error.message : '清除所有當前競賽執行失敗';
}
return result;
}
// 清除主機的所有當前競賽狀態
private async clearMasterCurrentCompetitions(): Promise<void> {
if (!this.masterPool) return;
const connection = await this.masterPool.getConnection();
try {
await connection.execute('UPDATE competitions SET is_current = FALSE');
} finally {
connection.release();
}
}
// 清除備機的所有當前競賽狀態
private async clearSlaveCurrentCompetitions(): Promise<void> {
if (!this.slavePool) return;
const connection = await this.slavePool.getConnection();
try {
await connection.execute('UPDATE competitions SET is_current = FALSE');
} finally {
connection.release();
}
}
// 智能雙寫刪除
async smartDualDelete(tableName: string, id: string, idField: string = 'id'): Promise<WriteResult> {
const result: WriteResult = {
success: false,
masterSuccess: false,
slaveSuccess: false
};
try {
// 獲取備機對應的 ID
let slaveId: string | null = null;
if (tableName === 'competitions') {
// 對於競賽表,先獲取競賽名稱
const masterRecord = await this.getMasterRecord(tableName, id);
if (masterRecord) {
slaveId = await this.getSlaveRecordIdByName(tableName, masterRecord.name);
}
} else {
// 對於關聯表,直接使用主機 ID
slaveId = id;
}
// 同時刪除主機和備機的記錄
const masterPromise = this.deleteFromMaster(tableName, id, idField);
const slavePromise = slaveId ? this.deleteFromSlave(tableName, slaveId, idField) : Promise.resolve();
const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]);
result.masterSuccess = masterResult.status === 'fulfilled';
result.slaveSuccess = slaveResult.status === 'fulfilled';
result.success = result.masterSuccess || result.slaveSuccess;
if (masterResult.status === 'rejected') {
result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機刪除失敗';
}
if (slaveResult.status === 'rejected') {
result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機刪除失敗';
}
} catch (error) {
result.masterError = error instanceof Error ? error.message : '雙寫刪除執行失敗';
}
return result;
}
// 從主機刪除記錄
private async deleteFromMaster(tableName: string, id: string, idField: string = 'id'): Promise<void> {
if (!this.masterPool) return;
const connection = await this.masterPool.getConnection();
try {
const sql = `DELETE FROM ${tableName} WHERE ${idField} = ?`;
await connection.execute(sql, [id]);
} finally {
connection.release();
}
}
// 從備機刪除記錄
private async deleteFromSlave(tableName: string, id: string, idField: string = 'id'): Promise<void> {
if (!this.slavePool) return;
const connection = await this.slavePool.getConnection();
try {
const sql = `DELETE FROM ${tableName} WHERE ${idField} = ?`;
await connection.execute(sql, [id]);
} finally {
connection.release();
}
}
// 清理資源 // 清理資源
async close(): Promise<void> { async close(): Promise<void> {
if (this.masterPool) { if (this.masterPool) {

View File

@@ -616,7 +616,7 @@ export class UserService {
const competitionStatsSql = ` const competitionStatsSql = `
SELECT SELECT
COUNT(*) as total_competitions, COUNT(*) as total_competitions,
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_competitions COUNT(CASE WHEN status = 'active' OR status = 'ongoing' THEN 1 END) as active_competitions
FROM competitions FROM competitions
`; `;
const competitionStats = await this.queryOne(competitionStatsSql); const competitionStats = await this.queryOne(competitionStatsSql);
@@ -1282,13 +1282,135 @@ export class CompetitionService {
// 更新競賽 // 更新競賽
static async updateCompetition(id: string, updates: Partial<Competition>): Promise<boolean> { static async updateCompetition(id: string, updates: Partial<Competition>): Promise<boolean> {
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at'); try {
const setClause = fields.map(field => `${field} = ?`).join(', '); const dbSyncFixed = new DatabaseSyncFixed();
const values = fields.map(field => (updates as any)[field]);
const sql = `UPDATE competitions SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`; // 使用雙寫功能更新競賽
const result = await db.update(sql, [...values, id]); const result = await dbSyncFixed.smartDualUpdate('competitions', id, updates);
return result.affectedRows > 0;
if (!result.success) {
console.error('競賽更新失敗:', result.masterError || result.slaveError);
return false;
}
return true;
} catch (error) {
console.error('競賽更新失敗:', error);
return false;
}
}
// 獲取當前競賽
static async getCurrentCompetition(): Promise<any | null> {
try {
const sql = 'SELECT * FROM competitions WHERE is_current = TRUE AND is_active = TRUE LIMIT 1';
const competitions = await db.query<any>(sql);
if (competitions.length > 0) {
const competition = competitions[0];
return await this.getCompetitionWithDetails(competition.id);
}
return null;
} catch (error) {
console.error('獲取當前競賽失敗:', error);
return null;
}
}
// 設置當前競賽
static async setCurrentCompetition(competitionId: string): Promise<boolean> {
try {
const dbSyncFixed = new DatabaseSyncFixed();
// 先清除所有競賽的當前狀態 - 使用直接 SQL 更新
try {
await dbSyncFixed.clearAllCurrentCompetitions();
} catch (error) {
console.error('清除當前競賽狀態失敗:', error);
}
// 設置指定競賽為當前競賽
const setResult = await dbSyncFixed.smartDualUpdate('competitions', competitionId, { is_current: true });
if (!setResult.success) {
console.error('設置當前競賽失敗:', setResult.masterError || setResult.slaveError);
return false;
}
return true;
} catch (error) {
console.error('設置當前競賽失敗:', error);
return false;
}
}
// 清除當前競賽
static async clearCurrentCompetition(): Promise<boolean> {
try {
await db.update('UPDATE competitions SET is_current = FALSE');
return true;
} catch (error) {
console.error('清除當前競賽失敗:', error);
return false;
}
}
// 刪除競賽 - 支援雙寫
static async deleteCompetition(id: string): Promise<boolean> {
try {
const dbSyncFixed = new DatabaseSyncFixed();
// 先獲取競賽信息
const competition = await this.getCompetitionById(id);
if (!competition) {
console.error('競賽不存在');
return false;
}
// 刪除關聯數據
await this.deleteCompetitionRelations(id);
// 使用雙寫功能刪除競賽
const result = await dbSyncFixed.smartDualDelete('competitions', id);
if (!result.success) {
console.error('競賽刪除失敗:', result.masterError || result.slaveError);
return false;
}
return true;
} catch (error) {
console.error('競賽刪除失敗:', error);
return false;
}
}
// 刪除競賽關聯數據
private static async deleteCompetitionRelations(competitionId: string): Promise<void> {
try {
const dbSyncFixed = new DatabaseSyncFixed();
// 刪除所有關聯表數據
const relations = [
'competition_judges',
'competition_teams',
'competition_apps',
'competition_award_types',
'competition_rules'
];
for (const relationTable of relations) {
try {
await dbSyncFixed.smartDualDelete(relationTable, competitionId, 'competition_id');
} catch (error) {
console.error(`刪除關聯表 ${relationTable} 失敗:`, error);
}
}
} catch (error) {
console.error('刪除競賽關聯數據失敗:', error);
}
} }
// ===================================================== // =====================================================
@@ -1369,7 +1491,38 @@ export class CompetitionService {
} }
// 獲取競賽的應用列表 // 獲取競賽的應用列表
static async getCompetitionApps(competitionId: string): Promise<any[]> { static async getCompetitionApps(competitionId: string, competitionType?: string): Promise<any[]> {
// 先獲取競賽信息
const competition = await this.getCompetitionById(competitionId);
if (!competition) return [];
let apps: any[] = [];
if (competition.type === 'team') {
// 對於團隊競賽,獲取所有參賽團隊的應用
const teams = await this.getCompetitionTeams(competitionId);
if (teams.length > 0) {
// 過濾掉 undefined 或 null 的 team_id 值
const teamIds = teams
.map(t => t.team_id)
.filter(id => id !== undefined && id !== null);
if (teamIds.length > 0) {
const placeholders = teamIds.map(() => '?').join(',');
const sql = `
SELECT a.*, u.name as creator_name, u.department as creator_department, t.name as team_name
FROM apps a
LEFT JOIN users u ON a.creator_id = u.id
LEFT JOIN teams t ON a.team_id = t.id
WHERE a.team_id IN (${placeholders}) AND a.is_active = TRUE
ORDER BY a.created_at ASC
`;
apps = await db.query(sql, teamIds);
}
}
} else {
// 對於個人競賽,從 competition_apps 表獲取
const sql = ` const sql = `
SELECT a.*, ca.submitted_at, u.name as creator_name, u.department as creator_department SELECT a.*, ca.submitted_at, u.name as creator_name, u.department as creator_department
FROM competition_apps ca FROM competition_apps ca
@@ -1378,7 +1531,10 @@ export class CompetitionService {
WHERE ca.competition_id = ? AND a.is_active = TRUE WHERE ca.competition_id = ? AND a.is_active = TRUE
ORDER BY ca.submitted_at ASC ORDER BY ca.submitted_at ASC
`; `;
return await db.query(sql, [competitionId]); apps = await db.query(sql, [competitionId]);
}
return apps;
} }
// 為競賽添加團隊 // 為競賽添加團隊
@@ -1621,9 +1777,42 @@ export class CompetitionService {
this.getCompetitionRules(competitionId) this.getCompetitionRules(competitionId)
]); ]);
// 根據日期動態計算競賽狀態
const now = new Date();
const startDate = new Date(competition.start_date);
const endDate = new Date(competition.end_date);
let calculatedStatus = competition.status;
// 確保日期比較的準確性,使用 UTC 時間避免時區問題
const nowUTC = new Date(now.getTime() + now.getTimezoneOffset() * 60000);
const startDateUTC = new Date(startDate.getTime() + startDate.getTimezoneOffset() * 60000);
const endDateUTC = new Date(endDate.getTime() + endDate.getTimezoneOffset() * 60000);
console.log('🔍 競賽狀態計算:', {
competitionId,
name: competition.name,
now: nowUTC.toISOString(),
startDate: startDateUTC.toISOString(),
endDate: endDateUTC.toISOString(),
originalStatus: competition.status
});
// 根據實際日期計算狀態
if (nowUTC < startDateUTC) {
calculatedStatus = 'upcoming'; // 即將開始
} else if (nowUTC >= startDateUTC && nowUTC <= endDateUTC) {
calculatedStatus = 'active'; // 進行中
} else if (nowUTC > endDateUTC) {
calculatedStatus = 'completed'; // 已完成
}
console.log('🔍 計算後的狀態:', calculatedStatus);
// 轉換字段名稱以匹配前端期望的格式 // 轉換字段名稱以匹配前端期望的格式
return { return {
...competition, ...competition,
status: calculatedStatus, // 使用計算後的狀態
startDate: competition.start_date, startDate: competition.start_date,
endDate: competition.end_date, endDate: competition.end_date,
evaluationFocus: competition.evaluation_focus, evaluationFocus: competition.evaluation_focus,
@@ -2625,7 +2814,7 @@ export class AppService {
const competitionStatsSql = ` const competitionStatsSql = `
SELECT SELECT
COUNT(*) as total_competitions, COUNT(*) as total_competitions,
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_competitions COUNT(CASE WHEN status = 'active' OR status = 'ongoing' THEN 1 END) as active_competitions
FROM competitions FROM competitions
`; `;
const competitionStats = await this.queryOne(competitionStatsSql); const competitionStats = await this.queryOne(competitionStatsSql);

View File

@@ -15,16 +15,6 @@
"migrate:data": "node scripts/migrate-data.js", "migrate:data": "node scripts/migrate-data.js",
"migrate:all": "pnpm run migrate:tables && pnpm run migrate:views && pnpm run migrate:triggers && pnpm run migrate:data", "migrate:all": "pnpm run migrate:tables && pnpm run migrate:views && pnpm run migrate:triggers && pnpm run migrate:data",
"migrate:reset": "node scripts/migrate.js --reset", "migrate:reset": "node scripts/migrate.js --reset",
"test:db": "node scripts/test-connection.js",
"create:users": "node scripts/create-test-users.js",
"add:user-fields": "node scripts/add-user-fields.js",
"test:profile": "node scripts/test-profile-update.js",
"test:forgot-password": "node scripts/test-forgot-password.js",
"test:forgot-password-new": "node scripts/test-forgot-password-new-flow.js",
"test:password-visibility": "node scripts/test-password-visibility.js",
"test:role-display": "node scripts/test-role-display.js",
"test:activity-records": "node scripts/test-activity-records.js",
"test:hydration-fix": "node scripts/test-hydration-fix.js",
"setup": "node scripts/setup.js", "setup": "node scripts/setup.js",
"db:init-slave": "node scripts/init-slave-database.js", "db:init-slave": "node scripts/init-slave-database.js",
"db:sync": "node scripts/sync-database.js", "db:sync": "node scripts/sync-database.js",