diff --git a/app/api/admin/awards/route.ts b/app/api/admin/awards/route.ts index fc36d21..a5ea510 100644 --- a/app/api/admin/awards/route.ts +++ b/app/api/admin/awards/route.ts @@ -98,6 +98,7 @@ export async function POST(request: NextRequest) { // 獲取獎項列表 export async function GET(request: NextRequest) { try { + console.log('🔍 開始獲取獎項列表...'); const { searchParams } = new URL(request.url); const competitionId = searchParams.get('competitionId'); const awardType = searchParams.get('awardType'); @@ -105,14 +106,20 @@ export async function GET(request: NextRequest) { const year = searchParams.get('year'); const month = searchParams.get('month'); + console.log('📋 查詢參數:', { competitionId, awardType, category, year, month }); + let awards; if (competitionId) { + console.log('🎯 根據競賽ID獲取獎項:', competitionId); awards = await AwardService.getAwardsByCompetition(competitionId); } else { + console.log('📊 獲取所有獎項'); awards = await AwardService.getAllAwards(); } + console.log('✅ 獲取到獎項數量:', awards?.length || 0); + // 應用篩選條件 if (awardType) { awards = awards.filter(award => award.award_type === awardType); @@ -127,10 +134,27 @@ export async function GET(request: NextRequest) { awards = awards.filter(award => award.month === parseInt(month)); } + // 解析 JSON 欄位 + const processedAwards = awards.map(award => { + console.log('🔍 處理獎項:', { + id: award.id, + competition_name: (award as any).competition_name, + competition_type: (award as any).competition_type, + competition_id: award.competition_id + }); + + return { + ...award, + application_links: award.application_links ? JSON.parse(award.application_links) : null, + documents: award.documents ? JSON.parse(award.documents) : [], + photos: award.photos ? JSON.parse(award.photos) : [], + }; + }); + return NextResponse.json({ success: true, message: '獎項列表獲取成功', - data: awards + data: processedAwards }); } catch (error) { @@ -141,4 +165,4 @@ export async function GET(request: NextRequest) { error: error instanceof Error ? error.message : '未知錯誤' }, { status: 500 }); } -} +} \ No newline at end of file diff --git a/app/api/admin/competitions/[id]/judges/route.ts b/app/api/admin/competitions/[id]/judges/route.ts index 758b00a..06677b8 100644 --- a/app/api/admin/competitions/[id]/judges/route.ts +++ b/app/api/admin/competitions/[id]/judges/route.ts @@ -1,95 +1,43 @@ // ===================================================== -// 競賽評審關聯管理 API +// 競賽評審 API // ===================================================== import { NextRequest, NextResponse } from 'next/server'; -import { CompetitionService } from '@/lib/services/database-service'; +import { AwardService } from '@/lib/services/database-service'; -// 獲取競賽的評審列表 -export async function GET(request: NextRequest, { params }: { params: { id: string } }) { +// 獲取競賽評審列表 +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { try { - const { id } = await params; + const { id: competitionId } = 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) { + if (!competitionId) { return NextResponse.json({ success: false, - message: '缺少評審ID列表', - error: 'judgeIds 必須是非空陣列' + message: '缺少競賽ID參數' }, { status: 400 }); } - const result = await CompetitionService.addCompetitionJudges(id, judgeIds); + console.log('🔍 獲取競賽評審:', competitionId); + + const judges = await AwardService.getCompetitionJudges(competitionId); + + console.log('✅ 獲取到評審數量:', judges?.length || 0); return NextResponse.json({ success: true, - message: '評審添加成功', - data: result + message: '評審列表獲取成功', + data: judges || [] }); } catch (error) { - console.error('添加競賽評審失敗:', error); + console.error('❌ 獲取評審失敗:', error); return NextResponse.json({ success: false, - message: '添加競賽評審失敗', + 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 }); - } -} +} \ No newline at end of file diff --git a/app/api/admin/competitions/[id]/teams/route.ts b/app/api/admin/competitions/[id]/teams/route.ts index f6305dd..d68fa9f 100644 --- a/app/api/admin/competitions/[id]/teams/route.ts +++ b/app/api/admin/competitions/[id]/teams/route.ts @@ -1,95 +1,37 @@ // ===================================================== -// 競賽團隊關聯管理 API +// 競賽參賽團隊 API // ===================================================== import { NextRequest, NextResponse } from 'next/server'; import { CompetitionService } from '@/lib/services/database-service'; -// 獲取競賽的團隊列表 -export async function GET(request: NextRequest, { params }: { params: { id: string } }) { +// 獲取競賽的參賽團隊 +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { - const { id } = await params; - - const teams = await CompetitionService.getCompetitionTeams(id); - + const { id: competitionId } = await params; + + if (!competitionId) { + return NextResponse.json({ + success: false, + message: '缺少競賽ID參數' + }, { status: 400 }); + } + + // 獲取競賽的參賽團隊 + const teams = await CompetitionService.getCompetitionTeams(competitionId); + return NextResponse.json({ success: true, - message: '競賽團隊列表獲取成功', + message: '參賽團隊獲取成功', data: teams }); } catch (error) { - console.error('獲取競賽團隊失敗:', error); + console.error('獲取參賽團隊失敗:', error); return NextResponse.json({ success: false, - message: '獲取競賽團隊失敗', + 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 }); - } -} +} \ No newline at end of file diff --git a/app/api/upload/document/route.ts b/app/api/upload/document/route.ts new file mode 100644 index 0000000..f1a321c --- /dev/null +++ b/app/api/upload/document/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server' +import { writeFile, mkdir } from 'fs/promises' +import { join } from 'path' +import { existsSync } from 'fs' + +export async function POST(request: NextRequest) { + try { + const data = await request.formData() + const file: File | null = data.get('file') as unknown as File + const awardId = data.get('awardId') as string + + if (!file) { + return NextResponse.json({ error: '沒有選擇文件' }, { status: 400 }) + } + + // 驗證文件類型 + const allowedTypes = [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + ] + + if (!allowedTypes.includes(file.type)) { + return NextResponse.json({ + error: '不支持的文件格式,請上傳 PDF、DOC、DOCX、PPT、PPTX 格式的文件' + }, { status: 400 }) + } + + // 驗證文件大小 (10MB) + const maxSize = 10 * 1024 * 1024 + if (file.size > maxSize) { + return NextResponse.json({ + error: '文件大小超過 10MB 限制' + }, { status: 400 }) + } + + // 創建上傳目錄 + const uploadDir = join(process.cwd(), 'public', 'uploads', 'documents') + if (!existsSync(uploadDir)) { + await mkdir(uploadDir, { recursive: true }) + } + + // 生成唯一文件名 + const timestamp = Date.now() + const fileExtension = file.name.split('.').pop() + const fileName = `${awardId}_${timestamp}.${fileExtension}` + const filePath = join(uploadDir, fileName) + + // 保存文件 + const bytes = await file.arrayBuffer() + const buffer = Buffer.from(bytes) + await writeFile(filePath, buffer) + + // 返回文件信息 + const fileUrl = `/uploads/documents/${fileName}` + + return NextResponse.json({ + success: true, + url: fileUrl, + fileName: file.name, + size: file.size, + type: file.type + }) + + } catch (error) { + console.error('文件上傳錯誤:', error) + return NextResponse.json( + { error: '文件上傳失敗' }, + { status: 500 } + ) + } +} diff --git a/app/api/upload/photo/route.ts b/app/api/upload/photo/route.ts new file mode 100644 index 0000000..8f5cfbc --- /dev/null +++ b/app/api/upload/photo/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server' +import { writeFile, mkdir } from 'fs/promises' +import { join } from 'path' +import { existsSync } from 'fs' + +export async function POST(request: NextRequest) { + try { + const data = await request.formData() + const file: File | null = data.get('file') as unknown as File + const awardId = data.get('awardId') as string + + if (!file) { + return NextResponse.json({ error: '沒有選擇照片' }, { status: 400 }) + } + + // 驗證文件類型 + const allowedTypes = [ + 'image/jpeg', + 'image/jpg', + 'image/png', + 'image/gif', + 'image/webp' + ] + + if (!allowedTypes.includes(file.type)) { + return NextResponse.json({ + error: '不支持的照片格式,請上傳 JPG、PNG、GIF、WEBP 格式的照片' + }, { status: 400 }) + } + + // 驗證文件大小 (5MB) + const maxSize = 5 * 1024 * 1024 + if (file.size > maxSize) { + return NextResponse.json({ + error: '照片大小超過 5MB 限制' + }, { status: 400 }) + } + + // 創建上傳目錄 + const uploadDir = join(process.cwd(), 'public', 'uploads', 'photos') + if (!existsSync(uploadDir)) { + await mkdir(uploadDir, { recursive: true }) + } + + // 生成唯一文件名 + const timestamp = Date.now() + const fileExtension = file.name.split('.').pop() + const fileName = `${awardId}_${timestamp}.${fileExtension}` + const filePath = join(uploadDir, fileName) + + // 保存文件 + const bytes = await file.arrayBuffer() + const buffer = Buffer.from(bytes) + await writeFile(filePath, buffer) + + // 返回文件信息 + const fileUrl = `/uploads/photos/${fileName}` + + return NextResponse.json({ + success: true, + url: fileUrl, + fileName: file.name, + size: file.size, + type: file.type + }) + + } catch (error) { + console.error('照片上傳錯誤:', error) + return NextResponse.json( + { error: '照片上傳失敗' }, + { status: 500 } + ) + } +} diff --git a/components/admin/competition-management.tsx b/components/admin/competition-management.tsx index ae8735b..be5acea 100644 --- a/components/admin/competition-management.tsx +++ b/components/admin/competition-management.tsx @@ -792,6 +792,304 @@ export function CompetitionManagement() { photos: [] as { id: string; name: string; url: string; caption: string; uploadDate: string; size: string }[], }) + // 動態獎項類型狀態 + const [competitionAwardTypes, setCompetitionAwardTypes] = useState([]) + const [loadingAwardTypes, setLoadingAwardTypes] = useState(false) + + // 動態參賽者狀態 + const [competitionApps, setCompetitionApps] = useState([]) + const [competitionTeams, setCompetitionTeams] = useState([]) + const [competitionJudges, setCompetitionJudges] = useState([]) + const [loadingParticipants, setLoadingParticipants] = useState(false) + + // 文件上傳狀態 + const [uploadingFiles, setUploadingFiles] = useState(false) + const [uploadProgress, setUploadProgress] = useState<{[key: string]: number}>({}) + + // 照片上傳狀態 + const [uploadingPhotos, setUploadingPhotos] = useState(false) + const [photoUploadProgress, setPhotoUploadProgress] = useState<{[key: string]: number}>({}) + + // 日期格式化輔助函式 + const formatDate = (dateString: string | Date): string => { + if (!dateString) return '' + + const date = new Date(dateString) + if (isNaN(date.getTime())) return '' + + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + + return `${year}/${month}/${day}` + } + + // 文件上傳處理函數 + const handleFileUpload = async (files: FileList) => { + const allowedTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'] + const maxSize = 10 * 1024 * 1024 // 10MB + + setUploadingFiles(true) + const newDocuments = [...newAward.documents] + + for (let i = 0; i < files.length; i++) { + const file = files[i] + + // 驗證文件類型 + if (!allowedTypes.includes(file.type)) { + alert(`文件 ${file.name} 格式不支援,請上傳 PDF、DOC、DOCX、PPTX 格式的文件`) + continue + } + + // 驗證文件大小 + if (file.size > maxSize) { + alert(`文件 ${file.name} 超過 10MB 限制`) + continue + } + + const fileId = `doc_${Date.now()}_${i}` + setUploadProgress(prev => ({ ...prev, [fileId]: 0 })) + + try { + // 創建 FormData + const formData = new FormData() + formData.append('file', file) + formData.append('awardId', newAward.id || 'temp') + + // 上傳文件 + const response = await fetch('/api/upload/document', { + method: 'POST', + body: formData, + }) + + if (!response.ok) { + throw new Error('文件上傳失敗') + } + + const result = await response.json() + + // 添加文件到列表 + const getFileType = (mimeType: string) => { + const typeMap: {[key: string]: string} = { + 'application/pdf': 'PDF', + 'application/msword': 'DOC', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'DOCX', + 'application/vnd.ms-powerpoint': 'PPT', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PPTX' + } + return typeMap[mimeType] || file.type.split('/')[1].toUpperCase() + } + + const newDoc = { + id: fileId, + name: file.name, + type: getFileType(file.type), + size: formatFileSize(file.size), + uploadDate: new Date().toLocaleDateString('zh-TW'), + url: result.url, + originalName: file.name + } + + newDocuments.push(newDoc) + setUploadProgress(prev => ({ ...prev, [fileId]: 100 })) + + } catch (error) { + console.error('文件上傳錯誤:', error) + alert(`文件 ${file.name} 上傳失敗`) + setUploadProgress(prev => ({ ...prev, [fileId]: 0 })) + } + } + + setNewAward({ ...newAward, documents: newDocuments }) + setUploadingFiles(false) + setUploadProgress({}) + } + + // 格式化文件大小 + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } + + // 刪除文件 + const handleRemoveDocument = (docId: string) => { + setNewAward({ + ...newAward, + documents: newAward.documents.filter(doc => doc.id !== docId) + }) + } + + // 照片上傳處理函數 + const handlePhotoUpload = async (files: FileList) => { + const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'] + const maxSize = 5 * 1024 * 1024 // 5MB + + setUploadingPhotos(true) + const newPhotos = [...newAward.photos] + + for (let i = 0; i < files.length; i++) { + const file = files[i] + + // 驗證文件類型 + if (!allowedTypes.includes(file.type)) { + alert(`照片 ${file.name} 格式不支援,請上傳 JPG、PNG、GIF、WEBP 格式的照片`) + continue + } + + // 驗證文件大小 + if (file.size > maxSize) { + alert(`照片 ${file.name} 超過 5MB 限制`) + continue + } + + const photoId = `photo_${Date.now()}_${i}` + setPhotoUploadProgress(prev => ({ ...prev, [photoId]: 0 })) + + try { + // 創建 FormData + const formData = new FormData() + formData.append('file', file) + formData.append('awardId', newAward.id || 'temp') + + // 上傳照片 + const response = await fetch('/api/upload/photo', { + method: 'POST', + body: formData, + }) + + if (!response.ok) { + throw new Error('照片上傳失敗') + } + + const result = await response.json() + + // 添加照片到列表 + const newPhoto = { + id: photoId, + name: file.name, + url: result.url, + size: formatFileSize(file.size), + uploadDate: new Date().toLocaleDateString('zh-TW'), + caption: '', + type: file.type.split('/')[1].toUpperCase(), + originalName: file.name + } + + newPhotos.push(newPhoto) + setPhotoUploadProgress(prev => ({ ...prev, [photoId]: 100 })) + + } catch (error) { + console.error('照片上傳錯誤:', error) + alert(`照片 ${file.name} 上傳失敗`) + setPhotoUploadProgress(prev => ({ ...prev, [photoId]: 0 })) + } + } + + setNewAward({ ...newAward, photos: newPhotos }) + setUploadingPhotos(false) + setPhotoUploadProgress({}) + } + + // 刪除照片 + const handleRemovePhoto = (photoId: string) => { + setNewAward({ + ...newAward, + photos: newAward.photos.filter(photo => photo.id !== photoId) + }) + } + + // 載入競賽獎項類型 + const loadCompetitionAwardTypes = async (competitionId: string) => { + if (!competitionId) { + setCompetitionAwardTypes([]) + return + } + + setLoadingAwardTypes(true) + try { + const response = await fetch(`/api/admin/competitions/${competitionId}/award-types`) + const data = await response.json() + + if (data.success) { + setCompetitionAwardTypes(data.data) + // 如果有獎項類型,預設選擇第一個 + if (data.data.length > 0) { + setNewAward(prev => ({ + ...prev, + awardType: 'custom', + customAwardTypeId: data.data[0].id + })) + } + } else { + console.error('載入獎項類型失敗:', data.message) + setCompetitionAwardTypes([]) + } + } catch (error) { + console.error('載入獎項類型失敗:', error) + setCompetitionAwardTypes([]) + } finally { + setLoadingAwardTypes(false) + } + } + + // 載入競賽參賽者和評審 + const loadCompetitionParticipants = async (competitionId: string) => { + if (!competitionId) { + setCompetitionApps([]) + setCompetitionTeams([]) + setCompetitionJudges([]) + return + } + + setLoadingParticipants(true) + try { + // 並行載入參賽應用、參賽團隊和評審 + const [appsResponse, teamsResponse, judgesResponse] = await Promise.all([ + fetch(`/api/admin/competitions/${competitionId}/apps`), + fetch(`/api/admin/competitions/${competitionId}/teams`), + fetch(`/api/admin/competitions/${competitionId}/judges`) + ]) + + const [appsData, teamsData, judgesData] = await Promise.all([ + appsResponse.json(), + teamsResponse.json(), + judgesResponse.json() + ]) + + if (appsData.success) { + setCompetitionApps(appsData.data) + } else { + console.error('載入參賽應用失敗:', appsData.message) + setCompetitionApps([]) + } + + if (teamsData.success) { + console.log('✅ 載入參賽團隊成功:', teamsData.data) + setCompetitionTeams(teamsData.data) + } else { + console.error('❌ 載入參賽團隊失敗:', teamsData.message) + setCompetitionTeams([]) + } + + if (judgesData.success) { + setCompetitionJudges(judgesData.data) + } else { + console.error('載入評審失敗:', judgesData.message) + setCompetitionJudges([]) + } + } catch (error) { + console.error('載入參賽者失敗:', error) + setCompetitionApps([]) + setCompetitionTeams([]) + setCompetitionJudges([]) + } finally { + setLoadingParticipants(false) + } + } + // Team form states const [newTeam, setNewTeam] = useState({ name: "", @@ -1507,30 +1805,63 @@ export function CompetitionManagement() { } setIsLoading(true) - await new Promise((resolve) => setTimeout(resolve, 1000)) + try { + // 獲取競賽信息 const competition = competitions.find((c) => c.id === newAward.competitionId) + if (!competition) { + setError("找不到選定的競賽") + return + } + + // 根據競賽類型自動設置 participantType + let actualParticipantType = newAward.participantType + if (competition.type === "team") { + actualParticipantType = "team" + } else if (competition.type === "individual") { + actualParticipantType = "individual" + } + // 如果是 mixed 類型,保持用戶選擇的 participantType + + // 獲取參賽者信息 let participant: any = null let participantName = "" let creatorName = "" - // Get participant based on type - if (newAward.participantType === "individual") { - // 示例個人應用數據 - const mockIndividualApps = [ - { id: "app1", name: "智能客服系統", creator: "張小明", department: "ITBU" }, - { id: "app2", name: "數據分析平台", creator: "李美華", department: "研發部" }, - ] - participant = mockIndividualApps.find((a) => a.id === newAward.participantId) - participantName = participant?.name || "" - creatorName = participant?.creator || "" - } else if (newAward.participantType === "team") { - participant = (dbTeams.length > 0 ? dbTeams : teams).find((t) => t.id === newAward.participantId) - participantName = participant?.name || "" - creatorName = participant?.leader || "" - } + console.log('🔍 查找參賽者調試信息:') + console.log('competition.type:', competition.type) + console.log('original participantType:', newAward.participantType) + console.log('actual participantType:', actualParticipantType) + console.log('participantId:', newAward.participantId) + console.log('competitionApps:', competitionApps) + console.log('competitionTeams:', competitionTeams) + + if (actualParticipantType === "individual") { + participant = competitionApps.find((app) => app.id === newAward.participantId) + participantName = participant?.name || "" + creatorName = participant?.creator_name || participant?.creator || "" + console.log('找到個人參賽者:', participant) + } else if (actualParticipantType === "team") { + participant = competitionTeams.find((team) => team.id === newAward.participantId) + participantName = participant?.name || "" + creatorName = participant?.leader_name || participant?.leader || "" + console.log('找到團隊參賽者:', participant) + + // 團體賽需要關聯到對應的應用 + if (participant && participant.app_id) { + console.log('團隊關聯的應用 ID:', participant.app_id) + console.log('團隊關聯的應用名稱:', participant.app_name) + } + } + + if (!participant) { + console.error('❌ 找不到參賽者') + console.log('competitionApps 長度:', competitionApps.length) + console.log('competitionTeams 長度:', competitionTeams.length) + setError(`找不到選定的參賽者 (${newAward.participantType}: ${newAward.participantId})`) + return + } - if (competition && participant) { // 根據獎項類型設定圖標 let icon = "🏆" switch (newAward.awardType) { @@ -1543,33 +1874,78 @@ export function CompetitionManagement() { default: icon = "🏆"; break; } - const award = { - id: `award_${Date.now()}`, - competitionId: newAward.competitionId, - appId: newAward.participantType === "individual" ? newAward.participantId : undefined, - teamId: newAward.participantType === "team" ? newAward.participantId : undefined, - appName: newAward.participantType === "individual" ? participantName : undefined, + // 準備獎項數據 + const awardData = { + competition_id: newAward.competitionId, + app_id: actualParticipantType === "individual" ? newAward.participantId : (participant?.app_id || null), + team_id: actualParticipantType === "team" ? newAward.participantId : null, + app_name: actualParticipantType === "individual" ? participantName : (participant?.app_name || null), + team_name: actualParticipantType === "team" ? participantName : null, creator: creatorName, - awardType: newAward.awardType, - awardName: newAward.awardName, + award_type: newAward.awardType, + award_name: newAward.awardName, + custom_award_type_id: newAward.customAwardTypeId || null, + score: newAward.score, + rank: newAward.rank, + category: newAward.category, + description: newAward.description, + judge_comments: newAward.judgeComments, + application_links: newAward.applicationLinks, + documents: newAward.documents, + photos: newAward.photos, + year: new Date().getFullYear(), + month: new Date().getMonth() + 1, + icon: icon, + competition_type: competition.type, + participant_type: actualParticipantType, + } + + console.log('📝 準備獎項數據:', awardData) + + // 調用 API 創建獎項 + const response = await fetch('/api/admin/awards', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(awardData), + }) + + const result = await response.json() + + if (!response.ok) { + throw new Error(result.message || '創建獎項失敗') + } + + // 創建成功,添加到本地列表 + const newAwardItem = { + id: result.data.id, + competitionId: newAward.competitionId, + competitionName: competition.name, + participantId: newAward.participantId, + participantType: newAward.participantType, + participantName: participantName, + creatorName: creatorName, + awardType: newAward.awardType, + awardName: newAward.awardName, + customAwardTypeId: newAward.customAwardTypeId, score: newAward.score, - year: competition.year, - month: competition.month, - icon, rank: newAward.rank, category: newAward.category, - competitionType: newAward.participantType, description: newAward.description, judgeComments: newAward.judgeComments, applicationLinks: newAward.applicationLinks, documents: newAward.documents, photos: newAward.photos, + year: new Date().getFullYear(), + month: new Date().getMonth() + 1, + icon: icon, + createdAt: new Date().toISOString(), } - addAward(award) - } + addAward(newAwardItem) - setShowCreateAward(false) + // 重置表單 setNewAward({ competitionId: "", participantId: "", @@ -1590,14 +1966,42 @@ export function CompetitionManagement() { judgeComments: "", photos: [], }) - setSuccess(selectedAward ? "獎項更新成功!" : "獎項創建成功!") - setIsLoading(false) + + setShowCreateAward(false) + setSuccess("獎項創建成功!") setTimeout(() => setSuccess(""), 3000) + + } catch (error) { + console.error('創建獎項失敗:', error) + setError(error instanceof Error ? error.message : '創建獎項失敗') + } finally { + setIsLoading(false) + } } - const handleViewAward = (award: any) => { + const handleViewAward = async (award: any) => { setSelectedAward(award) setShowAwardDetail(true) + + // 載入該競賽的評審團信息 + if (award.competitionId) { + try { + console.log('🔍 載入競賽評審團:', award.competitionId); + const response = await fetch(`/api/admin/competitions/${award.competitionId}/judges`); + const data = await response.json(); + + if (data.success) { + console.log('✅ 獲取到評審團:', data.data.length, '位'); + setCompetitionJudges(data.data); + } else { + console.error('❌ 獲取評審團失敗:', data.message); + setCompetitionJudges([]); + } + } catch (error) { + console.error('❌ 載入評審團失敗:', error); + setCompetitionJudges([]); + } + } } const handleEditAward = (award: any) => { @@ -2215,7 +2619,7 @@ export function CompetitionManagement() { 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 `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}` } return `${formatDate(start)} ~ ${formatDate(end)}` @@ -2352,7 +2756,7 @@ export function CompetitionManagement() { // 奖项类型筛选 if (awardTypeFilter !== "all") { if (awardTypeFilter === "ranking") { - filteredAwards = filteredAwards.filter((award) => award.rank > 0 && award.rank <= 3) + filteredAwards = filteredAwards.filter((award) => parseInt(award.rank) > 0 && parseInt(award.rank) <= 3) } else if (awardTypeFilter === "popular") { filteredAwards = filteredAwards.filter((award) => award.awardType === "popular") } else { @@ -2369,10 +2773,12 @@ export function CompetitionManagement() { // 按年份、月份、排名排序 if (a.year !== b.year) return b.year - a.year if (a.month !== b.month) return b.month - a.month - if (a.rank !== b.rank) { - if (a.rank === 0) return 1 - if (b.rank === 0) return -1 - return a.rank - b.rank + const aRank = parseInt(a.rank) || 0 + const bRank = parseInt(b.rank) || 0 + if (aRank !== bRank) { + if (aRank === 0) return 1 + if (bRank === 0) return -1 + return aRank - bRank } return 0 }) @@ -3481,7 +3887,7 @@ export function CompetitionManagement() {
- {getFilteredAwards().filter((a) => a.rank > 0 && a.rank <= 3).length} + {getFilteredAwards().filter((a) => parseInt(a.rank) > 0 && parseInt(a.rank) <= 3).length}
前三名獎項
@@ -3579,7 +3985,7 @@ export function CompetitionManagement() { return (
- {paginatedAwards.map((award) => ( + {paginatedAwards.map((award: any) => (
{award.icon}
@@ -3589,41 +3995,43 @@ export function CompetitionManagement() { - {award.awardName} + {award.award_name} -

{award.appName || "團隊作品"}

+

+ {award.team_name || award.app_name || "作品"} +

by {award.creator}

{/* 評分顯示 */} - {award.score > 0 && ( + {award.score && parseFloat(award.score) > 0 && (
- {award.score.toFixed(1)} + {parseFloat(award.score).toFixed(1)}
評審評分
)} {/* 獎項排名 */} - {award.rank > 0 && ( + {award.rank && parseInt(award.rank) > 0 && ( - 第 {award.rank} 名 + 第 {parseInt(award.rank)} 名 )}
@@ -3631,60 +4039,136 @@ export function CompetitionManagement() { {/* 競賽資訊 */}
- {award.year}年{award.month}月 + {award.year && award.month ? `${award.year}年${award.month}月` : ''} - {getCompetitionTypeText(award.competitionType)} + {getCompetitionTypeText(award.competition_type)}
{/* 應用連結摘要 */} - {(award as any).applicationLinks && ( + {award.application_links && (

應用連結

-
- {(award as any).applicationLinks.production && ( +
+ {(() => { + // 解析 application_links,可能是字符串或對象 + let links = award.application_links; + if (typeof links === 'string') { + try { + links = JSON.parse(links); + } catch (e) { + links = null; + } + } + + const linkItems = []; + + if (links && links.production) { + linkItems.push( +
- )} - {(award as any).applicationLinks.demo && ( + + 生產環境 + +
+ ); + } + + if (links && links.demo) { + linkItems.push( +
- )} - {(award as any).applicationLinks.github && ( + + 演示版本 + +
+ ); + } + + if (links && links.github) { + linkItems.push( + + ); + } + + if (linkItems.length === 0) { + return 無連結; + } + + return linkItems; + })()}
)} {/* 相關文檔摘要 */} - {(award as any).documents && (award as any).documents.length > 0 && ( + {award.documents && award.documents.length > 0 && (

相關文檔

- {(award as any).documents.length} 個文檔 + {award.documents.length} 個文檔 - {(award as any).documents.map((doc: any) => doc.type).join(", ")} + {award.documents.map((doc: any) => doc.type).join(", ")}
)} {/* 得獎照片摘要 */} - {(award as any).photos && (award as any).photos.length > 0 && ( + {award.photos && award.photos.length > 0 && (

得獎照片

- {(award as any).photos.length} 張照片 + {award.photos.length} 張照片
- {(award as any).photos.slice(0, 3).map((photo: any, index: number) => ( -
+ {award.photos.slice(0, 3).map((photo: any, index: number) => ( +
+ {photo.url ? ( + {`照片 { + e.currentTarget.style.display = 'none'; + e.currentTarget.nextElementSibling?.classList.remove('hidden'); + }} + /> + ) : null} +
+ 📷 +
+
))} - {(award as any).photos.length > 3 && ( - +{(award as any).photos.length - 3} + {award.photos.length > 3 && ( + +{award.photos.length - 3} )}
@@ -3692,9 +4176,9 @@ export function CompetitionManagement() { )} {/* 獎項描述 */} - {(award as any).description && ( + {award.description && (

- {(award as any).description} + {award.description}

)} @@ -7266,7 +7750,32 @@ export function CompetitionManagement() { setNewAward({ ...newAward, awardType: value })} + value={newAward.customAwardTypeId || newAward.awardType} + onValueChange={(value: any) => { + // 檢查是否為自定義獎項類型 + const customAwardType = competitionAwardTypes.find(type => type.id === value) + if (customAwardType) { + setNewAward({ + ...newAward, + awardType: 'custom', + customAwardTypeId: value, + awardName: customAwardType.name + }) + } else { + setNewAward({ ...newAward, awardType: value, customAwardTypeId: "" }) + } + }} + disabled={loadingAwardTypes} > - + + {competitionAwardTypes.length > 0 ? ( + // 顯示競賽自定義的獎項類型 + competitionAwardTypes.map((awardType) => ( + +
+ {awardType.icon} + {awardType.name} +
+
+ )) + ) : ( + // 如果沒有自定義獎項類型,顯示預設選項 + <> 🥇 金獎 🥈 銀獎 🥉 銅獎 @@ -7318,8 +7854,15 @@ export function CompetitionManagement() { 💡 創新獎 ⚙️ 技術獎 🏆 自定義獎項 + + )}
+ {competitionAwardTypes.length > 0 && ( +

+ 此競賽有 {competitionAwardTypes.length} 個自定義獎項類型 +

+ )}
{/* 獎項類別 */} @@ -7412,8 +7955,16 @@ export function CompetitionManagement() {
競賽名稱:{selectedCompetition.name}
競賽類型:{selectedCompetition.type === "individual" ? "個人賽" : selectedCompetition.type === "team" ? "團體賽" : "混合賽"}
-
競賽期間:{selectedCompetition.startDate} ~ {selectedCompetition.endDate}
-
評審團:{judges.filter(j => selectedCompetition.judges?.includes(j.id)).length} 位評審
+
競賽期間: + {formatDate(selectedCompetition.startDate || selectedCompetition.start_date)} ~ {formatDate(selectedCompetition.endDate || selectedCompetition.end_date)} +
+
評審團: + {loadingParticipants ? ( + 載入中... + ) : ( + `${competitionJudges.length} 位評審` + )} +
) @@ -7451,53 +8002,137 @@ export function CompetitionManagement() { )} + {/* 根據競賽類型自動設定參賽者類型 */} + {selectedCompetition.type === "individual" && newAward.participantType !== "individual" && ( +
+
+

+ 個人競賽:此競賽僅限個人參賽,將顯示個人參賽應用清單 +

+
+
+ )} + + {selectedCompetition.type === "team" && newAward.participantType !== "team" && ( +
+
+

+ 團體競賽:此競賽僅限團隊參賽,將顯示團隊參賽清單 +

+
+
+ )} + {/* 參賽者選擇 */}
{/* 參賽者資訊預覽 */} {newAward.participantId && (() => { - if (newAward.participantType === "individual") { - const app = [ - { id: "app1", name: "智能客服系統", creator: "張小明", department: "ITBU", description: "基於AI的智能客服解決方案" }, - { id: "app2", name: "數據分析平台", creator: "李美華", department: "研發部", description: "企業級數據分析和視覺化平台" }, - ].find(a => a.id === newAward.participantId) + // 根據競賽類型決定顯示的參賽者資訊 + const shouldShowIndividual = selectedCompetition.type === "individual" || + (selectedCompetition.type === "mixed" && newAward.participantType === "individual"); + const shouldShowTeam = selectedCompetition.type === "team" || + (selectedCompetition.type === "mixed" && newAward.participantType === "team"); + + if (shouldShowIndividual) { + const app = competitionApps.find(a => a.id === newAward.participantId) if (!app) return null @@ -7506,14 +8141,14 @@ export function CompetitionManagement() {

個人參賽者資訊

應用名稱:{app.name}
-
創作者:{app.creator}
+
創作者:{app.creator_name || app.creator}
所屬部門:{app.department}
-
應用描述:{app.description}
+
應用描述:{app.description || '暫無描述'}
) - } else { - const team = (dbTeams.length > 0 ? dbTeams : teams).find(t => t.id === newAward.participantId) + } else if (shouldShowTeam) { + const team = competitionTeams.find(t => t.id === newAward.participantId) if (!team) return null return ( @@ -7521,22 +8156,31 @@ export function CompetitionManagement() {

團隊參賽者資訊

團隊名稱:{team.name}
-
隊長:{team.leader}
+
隊長:{team.leader_name || team.leader}
所屬部門:{team.department}
-
團隊人數:{team.memberCount}人
-
提交應用:{team.submittedAppCount}個
-
聯絡信箱:{team.contactEmail}
+
團隊人數:{team.actual_member_count || team.member_count || team.memberCount || 0}人
+
聯絡信箱:{team.contact_email || team.contactEmail || '未提供'}
) + } else { + return null } })()} {/* 評審團資訊 */}
-

競賽評審團

+

+ 競賽評審團 ({competitionJudges.length} 位) +

+ {loadingParticipants ? ( +
+
+ 載入評審資訊中... +
+ ) : competitionJudges.length > 0 ? (
- {(dbJudges.length > 0 ? dbJudges : judges).filter(judge => selectedCompetition.judges?.includes(judge.id)).map((judge) => ( + {competitionJudges.map((judge) => (
@@ -7550,6 +8194,9 @@ export function CompetitionManagement() {
))}
+ ) : ( +

暫無評審資訊

+ )}
) @@ -7562,10 +8209,10 @@ export function CompetitionManagement() { {/* 應用連結 */} - + +
+ {/* 網站預覽區塊 */}
-
- {/* 正式應用 */}
+ {/* 網站預覽內容 */} + {newAward.applicationLinks.production && ( +
+
+

網站預覽

+ +
+
+
+