"use client" import { useState, useEffect } from "react" import { useCompetition } from "@/contexts/competition-context" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { Badge } from "@/components/ui/badge" import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Alert, AlertDescription } from "@/components/ui/alert" import { Progress } from "@/components/ui/progress" import { ScoringLinkDialog } from "./scoring-link-dialog" import { JudgeListDialog } from "./judge-list-dialog" import { Trophy, Plus, Edit, CheckCircle, AlertTriangle, ClipboardList, User, Users, Search, Loader2, BarChart3, ChevronLeft, ChevronRight, Link } from "lucide-react" interface ScoringRecord { id: string judgeId: string judgeName: string participantId: string participantName: string participantType: "individual" | "team" teamName?: string scores: Record totalScore: number comments: string submittedAt?: string status: "completed" | "pending" | "draft" } const mockIndividualApps: any[] = [] const initialTeams: any[] = [] export function ScoringManagement() { const { competitions, judges, judgeScores, submitJudgeScore } = useCompetition() // 狀態定義必須在使用之前 const [selectedCompetition, setSelectedCompetition] = useState(null) const [scoringRecords, setScoringRecords] = useState([]) const [showManualScoring, setShowManualScoring] = useState(false) const [showEditScoring, setShowEditScoring] = useState(false) const [selectedRecord, setSelectedRecord] = useState(null) const [manualScoring, setManualScoring] = useState({ judgeId: "", participantId: "", scores: {} as Record, comments: "" }) const [statusFilter, setStatusFilter] = useState<"all" | "completed" | "pending">("all") const [searchQuery, setSearchQuery] = useState("") const [isLoading, setIsLoading] = useState(false) const [success, setSuccess] = useState("") const [error, setError] = useState("") const [showScoringLink, setShowScoringLink] = useState(false) const [showJudgeList, setShowJudgeList] = useState(false) // 新增狀態:從後端獲取的評審和參賽者數據 const [competitionJudges, setCompetitionJudges] = useState([]) const [competitionParticipants, setCompetitionParticipants] = useState([]) const [isLoadingData, setIsLoadingData] = useState(false) const [isInitialLoading, setIsInitialLoading] = useState(true) // 評分完成度匯總狀態 const [scoringSummary, setScoringSummary] = useState(null) const [isLoadingSummary, setIsLoadingSummary] = useState(false) // 評分進度狀態(用於確保APP數量一致性) const [scoringProgress, setScoringProgress] = useState(null) // APP詳細評分狀態 const [selectedApp, setSelectedApp] = useState(null) const [appScoringDetails, setAppScoringDetails] = useState(null) const [isLoadingAppDetails, setIsLoadingAppDetails] = useState(false) const [showAppDetails, setShowAppDetails] = useState(false) // 競賽規則狀態 const [competitionRules, setCompetitionRules] = useState([]) const [isLoadingRules, setIsLoadingRules] = useState(false) // 調試:檢查競賽數據 console.log('📋 競賽數據:', competitions) console.log('👨‍⚖️ 評審數據:', judges) console.log('📊 競賽數量:', competitions?.length || 0) // 檢查初始載入狀態 useEffect(() => { if (competitions && competitions.length > 0) { console.log('✅ 競賽數據已載入,關閉初始載入狀態') setIsInitialLoading(false) // 自動選擇第一個競賽(如果沒有選中的話) if (!selectedCompetition) { console.log('🎯 自動選擇第一個競賽:', competitions[0].name) setSelectedCompetition(competitions[0]) } } }, [competitions, selectedCompetition]) useEffect(() => { if (selectedCompetition) { loadScoringData() loadCompetitionData() } }, [selectedCompetition]) const loadScoringData = async () => { if (!selectedCompetition) return setIsLoading(true) try { // 從後端API獲取評分數據 const response = await fetch(`/api/admin/scoring?competitionId=${selectedCompetition.id}`) const data = await response.json() if (data.success) { // 轉換API數據格式為前端組件格式 const records: ScoringRecord[] = data.data.scores.map((score: any) => { // 解析 score_details 字符串為動態評分對象 let dynamicScores: Record = {}; if (score.score_details) { // 處理兩種格式:aa:4,bb:7 或 aa:4:50.00|bb:7:50.00 const details = score.score_details.includes('|') ? score.score_details.split('|') : score.score_details.split(','); details.forEach((detail: string) => { const parts = detail.split(':'); if (parts.length >= 2) { const ruleName = parts[0]; const scoreValue = parts[1]; if (ruleName && scoreValue) { dynamicScores[ruleName] = parseInt(scoreValue); } } }); } // 如果沒有動態評分,使用預設字段 if (Object.keys(dynamicScores).length === 0) { dynamicScores = { innovation: score.innovation_score || 0, technical: score.technical_score || 0, usability: score.usability_score || 0, presentation: score.presentation_score || 0, impact: score.impact_score || 0 }; } return { id: score.id, judgeId: score.judge_id, judgeName: score.judge_name, participantId: score.app_id, participantName: score.app_name, participantType: score.participant_type === 'app' ? 'individual' : 'team', scores: dynamicScores, totalScore: score.total_score, comments: score.comments || '', submittedAt: score.submitted_at, status: score.total_score > 0 ? 'completed' : 'pending' }; }) setScoringRecords(records) } else { setError('載入評分數據失敗') } } catch (err) { console.error('載入評分數據失敗:', err) setError('載入評分數據失敗') } finally { setIsLoading(false) } } const calculateTotalScore = (scores: Record, rules: any[]): number => { if (rules.length === 0) { const values = Object.values(scores) return values.length > 0 ? values.reduce((a, b) => a + b, 0) : 0 } let totalScore = 0 let totalWeight = 0 rules.forEach((rule: any) => { const score = scores[rule.name] || 0 const weight = parseFloat(rule.weight) || 1 totalScore += score * weight totalWeight += weight }) return totalWeight > 0 ? totalScore / totalWeight : 0 } // 生成所有評審和APP的組合 const generateAllScoringCombinations = () => { if (!competitionJudges.length || !competitionParticipants.length) { return [] } const combinations: ScoringRecord[] = [] // 為每個評審和每個參賽者創建組合 competitionJudges.forEach(judge => { competitionParticipants.forEach(participant => { // 檢查是否已有評分記錄 const existingRecord = scoringRecords.find(record => record.judgeId === judge.id && record.participantId === participant.id ) if (existingRecord) { // 使用現有記錄 combinations.push(existingRecord) } else { // 創建新的待評分記錄 combinations.push({ id: `pending_${judge.id}_${participant.id}`, judgeId: judge.id, judgeName: judge.name, participantId: participant.id, participantName: participant.displayName || participant.name, participantType: participant.type as "individual" | "team", teamName: participant.teamName, scores: {}, totalScore: 0, comments: "", status: "pending" }) } }) }) return combinations } const getFilteredRecords = () => { // 使用生成的組合而不是僅有的評分記錄 const allCombinations = generateAllScoringCombinations() let filtered = [...allCombinations] if (statusFilter !== "all") { filtered = filtered.filter(record => record.status === statusFilter) } if (searchQuery.trim()) { const query = searchQuery.toLowerCase().trim() filtered = filtered.filter(record => record.judgeName.toLowerCase().includes(query) || record.participantName.toLowerCase().includes(query) ) } return filtered } const handleManualScoring = () => { // 根據競賽規則初始化評分項目 const initialScores: Record = {} if (competitionRules && competitionRules.length > 0) { competitionRules.forEach((rule: any) => { initialScores[rule.name] = 0 }) } else { // 預設評分項目 initialScores.innovation = 0 initialScores.technical = 0 initialScores.usability = 0 initialScores.presentation = 0 initialScores.impact = 0 } setManualScoring({ judgeId: "", participantId: "", scores: initialScores, comments: "" }) setShowManualScoring(true) } const handleEditScoring = (record: ScoringRecord) => { setSelectedRecord(record) // 根據競賽規則初始化評分項目 const initialScores: Record = {} // 直接使用記錄中的評分數據,不依賴競賽規則 Object.keys(record.scores).forEach(key => { initialScores[key] = record.scores[key] || 0; }); // 如果記錄中沒有評分數據,則使用競賽規則 if (Object.keys(initialScores).length === 0) { if (competitionRules && competitionRules.length > 0) { competitionRules.forEach((rule: any) => { initialScores[rule.name] = 0; }); } else { // 預設評分項目 initialScores.innovation = 0; initialScores.technical = 0; initialScores.usability = 0; initialScores.presentation = 0; initialScores.impact = 0; } } setManualScoring({ judgeId: record.judgeId, participantId: record.participantId, scores: initialScores, comments: record.comments || '', }) // 如果是待評分項目,顯示手動評分對話框;如果是已完成項目,顯示編輯對話框 if (record.status === "pending") { setShowManualScoring(true) } else { setShowEditScoring(true) } } const handleSubmitScore = async () => { setError("") if (!manualScoring.judgeId || !manualScoring.participantId) { setError("請選擇評審和參賽項目") return } // 檢查所有評分項目是否都已評分 const defaultRules = [ { name: "創新性" }, { name: "技術性" }, { name: "實用性" }, { name: "展示效果" }, { name: "影響力" } ] const rules = competitionRules.length > 0 ? competitionRules : defaultRules const hasAllScores = rules.every((rule: any) => manualScoring.scores[rule.name] && manualScoring.scores[rule.name] > 0 ) if (!hasAllScores) { setError("請為所有評分項目打分") return } if (!manualScoring.comments.trim()) { setError("請填寫評審意見") return } setIsLoading(true) try { // 轉換評分格式以符合API要求 - 使用動態規則 const apiScores: Record = {} rules.forEach((rule: any) => { apiScores[rule.name] = manualScoring.scores[rule.name] || 0 }) // 根據參賽者類型確定participantType const selectedParticipant = competitionParticipants.find(p => p.id === manualScoring.participantId) console.log('🔍 選中的參賽者:', selectedParticipant); // 由於所有參賽者都是團隊的 app,所以 participantType 應該是 'app' const participantType = 'app' const requestData = { judgeId: manualScoring.judgeId, participantId: manualScoring.participantId, participantType: participantType, scores: apiScores, comments: manualScoring.comments.trim(), competitionId: selectedCompetition?.id, isEdit: showEditScoring, // 標識是否為編輯模式 recordId: selectedRecord?.id // 編輯時的記錄ID } console.log('🔍 提交評分請求數據:', requestData); const response = await fetch('/api/admin/scoring', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestData) }) const data = await response.json() console.log('🔍 API 回應:', data); if (data.success) { setSuccess(showEditScoring ? "評分更新成功!" : "評分提交成功!") // 更新本地評分記錄 const newRecord: ScoringRecord = { id: data.data?.id || `new_${Date.now()}`, judgeId: manualScoring.judgeId, judgeName: competitionJudges.find(j => j.id === manualScoring.judgeId)?.name || '未知評審', participantId: manualScoring.participantId, participantName: competitionParticipants.find(p => p.id === manualScoring.participantId)?.displayName || competitionParticipants.find(p => p.id === manualScoring.participantId)?.name || '未知參賽者', participantType: competitionParticipants.find(p => p.id === manualScoring.participantId)?.type as "individual" | "team" || "individual", scores: apiScores, totalScore: calculateTotalScore(apiScores, rules), comments: manualScoring.comments.trim(), status: "completed", submittedAt: new Date().toISOString() } // 更新本地狀態 setScoringRecords(prev => { const existingIndex = prev.findIndex(r => r.judgeId === newRecord.judgeId && r.participantId === newRecord.participantId) if (existingIndex >= 0) { // 更新現有記錄 const updated = [...prev] updated[existingIndex] = newRecord return updated } else { // 添加新記錄 return [...prev, newRecord] } }) setShowManualScoring(false) setShowEditScoring(false) setSelectedRecord(null) } else { setError(data.message || "評分提交失敗") } } catch (err) { console.error('評分提交失敗:', err) setError("評分提交失敗,請重試") } finally { setIsLoading(false) setTimeout(() => setSuccess(""), 3000) } } const getStatusBadge = (status: string) => { switch (status) { case "completed": return 已完成 case "pending": return 待評分 default: return {status} } } const [scoringStats, setScoringStats] = useState({ totalScores: 0, completedScores: 0, pendingScores: 0, completionRate: 0, totalParticipants: 0 }) const loadScoringStats = async () => { if (!selectedCompetition) return try { const response = await fetch(`/api/admin/scoring/stats?competitionId=${selectedCompetition.id}`) const data = await response.json() if (data.success) { setScoringStats(data.data) } } catch (err) { console.error('載入評分統計失敗:', err) } } // 當選擇競賽時載入統計數據 useEffect(() => { if (selectedCompetition) { loadScoringStats() loadCompetitionData() loadScoringSummary() } }, [selectedCompetition]) // 載入競賽相關數據(評審和參賽者) const loadCompetitionData = async () => { if (!selectedCompetition) return console.log('🔍 開始載入競賽數據,競賽ID:', selectedCompetition.id) setIsLoadingData(true) setError("") try { // 載入競賽評審 console.log('📋 載入競賽評審...') const judgesResponse = await fetch(`/api/competitions/${selectedCompetition.id}/judges`) const judgesData = await judgesResponse.json() console.log('評審API回應:', judgesData) if (judgesData.success && judgesData.data && judgesData.data.judges) { setCompetitionJudges(judgesData.data.judges) console.log('✅ 評審數據載入成功:', judgesData.data.judges.length, '個評審') } else { console.error('❌ 評審數據載入失敗:', judgesData.message || 'API回應格式錯誤') setCompetitionJudges([]) } // 使用統一的評分進度API來獲取準確的參賽者數量 console.log('📱 載入競賽參賽者(使用統一計算邏輯)...') const scoringProgressResponse = await fetch(`/api/competitions/scoring-progress?competitionId=${selectedCompetition.id}`) const scoringProgressData = await scoringProgressResponse.json() console.log('評分進度API回應:', scoringProgressData) let participants = [] if (scoringProgressData.success && scoringProgressData.data) { // 保存評分進度數據 setScoringProgress(scoringProgressData.data) // 獲取競賽的實際參賽APP(使用與評分進度相同的邏輯) const appsResponse = await fetch(`/api/competitions/${selectedCompetition.id}/apps`) const appsData = await appsResponse.json() console.log('應用API回應:', appsData) if (appsData.success && appsData.data && appsData.data.apps) { // 直接使用API返回的APP數據,確保數量與評分進度一致 participants = appsData.data.apps.map((app: any) => ({ id: app.id, name: app.name, type: selectedCompetition.type === 'team' ? 'team' : 'individual', teamName: app.teamName || (selectedCompetition.type === 'team' ? '未知團隊' : null), displayName: app.name, creator: app.creator || '未知作者', teamId: app.teamId || null })) console.log(`✅ ${selectedCompetition.type === 'team' ? '團隊' : '個人'}競賽APP數據載入成功:`, participants.length, '個APP') } else { console.error('❌ 應用數據載入失敗:', appsData.message || 'API回應格式錯誤') } } else { console.error('❌ 評分進度數據載入失敗:', scoringProgressData.message || 'API回應格式錯誤') } setCompetitionParticipants(participants) console.log('✅ 參賽者數據載入完成:', participants.length, '個參賽者') console.log('🔍 參賽者詳細數據:', participants) // 載入競賽規則 console.log('📋 載入競賽規則...') const rulesResponse = await fetch(`/api/competitions/${selectedCompetition.id}/rules`) const rulesData = await rulesResponse.json() if (rulesData.success && rulesData.data) { setCompetitionRules(rulesData.data) console.log('✅ 競賽規則載入成功:', rulesData.data.length, '個規則') } else { console.error('❌ 競賽規則載入失敗:', rulesData.message || 'API回應格式錯誤') setCompetitionRules([]) } // 如果沒有載入到任何數據,顯示警告 if (participants.length === 0) { console.warn('⚠️ 沒有載入到任何參賽者數據') setError('該競賽暫無參賽者數據,請檢查競賽設置') } } catch (err) { console.error('❌ 載入競賽數據失敗:', err) setError('載入競賽數據失敗: ' + (err instanceof Error ? err.message : '未知錯誤')) // 設置空數組以避免undefined錯誤 setCompetitionJudges([]) setCompetitionParticipants([]) } finally { setIsLoadingData(false) } } // 載入評分完成度匯總 const loadScoringSummary = async () => { if (!selectedCompetition) return setIsLoadingSummary(true) try { const response = await fetch(`/api/admin/scoring/summary?competitionId=${selectedCompetition.id}`) const data = await response.json() if (data.success) { setScoringSummary(data.data) console.log('✅ 評分完成度匯總載入成功:', data.data) } else { console.log('❌ 評分完成度匯總載入失敗:', data) setScoringSummary(null) } } catch (error) { console.error('載入評分完成度匯總失敗:', error) setScoringSummary(null) } finally { setIsLoadingSummary(false) } } // 載入APP詳細評分信息 const loadAppScoringDetails = async (app: any) => { if (!selectedCompetition?.id) return setSelectedApp(app) setIsLoadingAppDetails(true) setShowAppDetails(true) try { // 獲取該APP的所有評分記錄 const response = await fetch(`/api/admin/scoring?competitionId=${selectedCompetition.id}&appId=${app.id}`) const data = await response.json() if (data.success) { setAppScoringDetails({ app: app, scores: data.data || [], judges: competitionJudges, totalJudges: competitionJudges.length, scoredJudges: data.data?.length || 0 }) } else { console.error('載入APP評分詳情失敗:', data.message) setError(data.message || '載入APP評分詳情失敗') } } catch (error) { console.error('載入APP評分詳情失敗:', error) setError('載入APP評分詳情失敗') } finally { setIsLoadingAppDetails(false) } } // 關閉APP詳細信息 const closeAppDetails = () => { setShowAppDetails(false) setSelectedApp(null) setAppScoringDetails(null) } // 計算基於所有組合的統計數據 const calculateProgressStats = () => { const allCombinations = generateAllScoringCombinations() const total = allCombinations.length const completed = allCombinations.filter(record => record.status === "completed").length const pending = allCombinations.filter(record => record.status === "pending").length const percentage = total > 0 ? Math.round((completed / total) * 100) : 0 return { total, completed, pending, percentage } } const progress = calculateProgressStats() // 顯示初始載入狀態 if (isInitialLoading) { return (

載入競賽數據中...

請稍候,正在從服務器獲取數據

) } return (
{success && ( {success} )} {error && ( {error} )} 選擇競賽 選擇要管理的競賽評分 {selectedCompetition && ( <> {selectedCompetition.name} - 評分概覽 {selectedCompetition.type === "individual" ? "個人賽" : selectedCompetition.type === "team" ? "團體賽" : "混合賽"} 查看當前競賽的評分進度和詳情

{competitionJudges.length}

評審總數

{scoringProgress?.appCount || competitionParticipants.length}

參賽APP數

{progress.completed}

已完成評分

{progress.percentage}%

總完成率

評分進度 {progress.completed} / {progress.total}
評分管理 管理競賽評分,查看APP評分詳情和完成度狀況
狀態:
setSearchQuery(e.target.value)} className="pl-10" />
{(() => { // 按評審分組 (使用 judgeId 避免重名問題) const groupedByJudge = getFilteredRecords().reduce((groups, record) => { const judgeId = record.judgeId if (!groups[judgeId]) { groups[judgeId] = [] } groups[judgeId].push(record) return groups }, {} as Record) return Object.entries(groupedByJudge).map(([judgeId, records]) => { const completedCount = records.filter(r => r.status === "completed").length const totalCount = records.length const progressPercentage = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0 // 從第一條記錄獲取評審名稱 (因為同一個 judgeId 的記錄都有相同的 judgeName) const judgeName = records[0]?.judgeName || '未知評審' return (
{judgeName.charAt(0)}

{judgeName}

評分進度:{completedCount} / {totalCount} 項

{progressPercentage}%
完成度
{progressPercentage}%
{/* 左滑動箭頭 */} {records.length > 4 && ( )} {/* 右滑動箭頭 */} {records.length > 4 && ( )}
{records.map((record) => (
{/* 項目標題和類型 */}
{record.participantType === "individual" ? ( ) : ( )}
{record.participantType === "team" && record.teamName ? `${record.teamName} - ${record.participantName}` : record.participantName }
{record.participantType === "individual" ? "個人" : "團隊"}
{/* 評分狀態 */}
{Math.round(record.totalScore)} / 100
{getStatusBadge(record.status)} {record.submittedAt && ( {new Date(record.submittedAt).toLocaleDateString()} )}
{/* 操作按鈕 */}
))}
) }) })()}
)} { if (!open) { setShowManualScoring(false) setShowEditScoring(false) setSelectedRecord(null) setManualScoring({ judgeId: "", participantId: "", scores: {} as Record, comments: "" }) } }}> {showEditScoring ? "編輯評分" : "手動輸入評分"} {showEditScoring ? "修改現有評分記錄" : "為參賽者手動輸入評分"}
{/* 動態評分項目 */}

評分項目

{(() => { const defaultRules = [ { name: "創新性", description: "創新程度和獨特性", weight: 25 }, { name: "技術性", description: "技術實現的複雜度和品質", weight: 30 }, { name: "實用性", description: "實際應用價值和用戶體驗", weight: 20 }, { name: "展示效果", description: "展示的清晰度和吸引力", weight: 15 }, { name: "影響力", description: "對行業或社會的潛在影響", weight: 10 } ] const rules = competitionRules.length > 0 ? competitionRules : defaultRules return rules.map((rule: any, index: number) => (

{rule.description}

{rule.weight && (

權重:{rule.weight}%

)}
{manualScoring.scores[rule.name] || 0} / 10
{/* 評分按鈕 */}
{Array.from({ length: 10 }, (_, i) => i + 1).map((score) => ( ))}
)) })()}
{/* 總分顯示 */}
總分

根據權重計算的綜合評分

{Math.round(calculateTotalScore(manualScoring.scores, competitionRules) * 10)} / 100