完成評審評分機制

This commit is contained in:
2025-09-18 18:34:31 +08:00
parent 2101767690
commit ffa1e45f63
54 changed files with 5730 additions and 709 deletions

View File

@@ -40,6 +40,7 @@ const initialTeams: any[] = []
export function ScoringManagement() {
const { competitions, judges, judgeScores, submitJudgeScore } = useCompetition()
// 狀態定義必須在使用之前
const [selectedCompetition, setSelectedCompetition] = useState<any>(null)
const [scoringRecords, setScoringRecords] = useState<ScoringRecord[]>([])
const [showManualScoring, setShowManualScoring] = useState(false)
@@ -56,10 +57,51 @@ export function ScoringManagement() {
const [showScoringLink, setShowScoringLink] = useState(false)
const [showJudgeList, setShowJudgeList] = useState(false)
// 新增狀態:從後端獲取的評審和參賽者數據
const [competitionJudges, setCompetitionJudges] = useState<any[]>([])
const [competitionParticipants, setCompetitionParticipants] = useState<any[]>([])
const [isLoadingData, setIsLoadingData] = useState(false)
const [isInitialLoading, setIsInitialLoading] = useState(true)
// 評分完成度匯總狀態
const [scoringSummary, setScoringSummary] = useState<any>(null)
const [isLoadingSummary, setIsLoadingSummary] = useState(false)
// APP詳細評分狀態
const [selectedApp, setSelectedApp] = useState<any>(null)
const [appScoringDetails, setAppScoringDetails] = useState<any>(null)
const [isLoadingAppDetails, setIsLoadingAppDetails] = useState(false)
const [showAppDetails, setShowAppDetails] = useState(false)
// 競賽規則狀態
const [competitionRules, setCompetitionRules] = useState<any[]>([])
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])
@@ -67,76 +109,80 @@ export function ScoringManagement() {
const loadScoringData = () => {
const loadScoringData = async () => {
if (!selectedCompetition) return
const participants = [
...(selectedCompetition.participatingApps || []).map((appId: string) => {
const app = mockIndividualApps.find(a => a.id === appId)
return { id: appId, name: app?.name || `應用 ${appId}`, type: "individual" as const }
}),
...(selectedCompetition.participatingTeams || []).map((teamId: string) => {
const team = initialTeams.find(t => t.id === teamId)
return { id: teamId, name: team?.name || `團隊 ${teamId}`, type: "team" as const }
})
]
const records: ScoringRecord[] = []
participants.forEach(participant => {
selectedCompetition.judges.forEach((judgeId: string) => {
const judge = judges.find(j => j.id === judgeId)
if (!judge) return
const existingScore = judgeScores.find(score =>
score.judgeId === judgeId && score.appId === participant.id
)
if (existingScore) {
records.push({
id: `${judgeId}-${participant.id}`,
judgeId, judgeName: judge.name,
participantId: participant.id, participantName: participant.name,
participantType: participant.type, scores: existingScore.scores,
totalScore: calculateTotalScore(existingScore.scores, selectedCompetition.rules || []),
comments: existingScore.comments,
submittedAt: existingScore.submittedAt || new Date().toISOString(),
status: "completed" as const,
})
} else {
// 初始化評分項目
const initialScores: Record<string, number> = {}
if (selectedCompetition.rules && selectedCompetition.rules.length > 0) {
selectedCompetition.rules.forEach((rule: any) => {
initialScores[rule.name] = 0
})
} else {
// 預設評分項目
initialScores.innovation = 0
initialScores.technical = 0
initialScores.usability = 0
initialScores.presentation = 0
initialScores.impact = 0
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<string, number> = {};
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);
}
}
});
}
records.push({
id: `${judgeId}-${participant.id}`,
judgeId, judgeName: judge.name,
participantId: participant.id, participantName: participant.name,
participantType: participant.type, scores: initialScores,
totalScore: 0, comments: "", submittedAt: "",
status: "pending" as const,
})
}
})
})
setScoringRecords(records)
// 如果沒有動態評分,使用預設字段
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<string, number>, rules: any[]): number => {
if (rules.length === 0) {
const values = Object.values(scores)
return values.length > 0 ? Math.round(values.reduce((a, b) => a + b, 0) / values.length) : 0
return values.length > 0 ? values.reduce((a, b) => a + b, 0) : 0
}
let totalScore = 0
@@ -144,12 +190,12 @@ export function ScoringManagement() {
rules.forEach((rule: any) => {
const score = scores[rule.name] || 0
const weight = rule.weight || 1
const weight = parseFloat(rule.weight) || 1
totalScore += score * weight
totalWeight += weight
})
return totalWeight > 0 ? Math.round(totalScore / totalWeight) : 0
return totalWeight > 0 ? totalScore / totalWeight : 0
}
const getFilteredRecords = () => {
@@ -170,8 +216,8 @@ export function ScoringManagement() {
const handleManualScoring = () => {
// 根據競賽規則初始化評分項目
const initialScores: Record<string, number> = {}
if (selectedCompetition?.rules && selectedCompetition.rules.length > 0) {
selectedCompetition.rules.forEach((rule: any) => {
if (competitionRules && competitionRules.length > 0) {
competitionRules.forEach((rule: any) => {
initialScores[rule.name] = 0
})
} else {
@@ -192,13 +238,39 @@ export function ScoringManagement() {
setShowManualScoring(true)
}
const handleEditScoring = (record: ScoringRecord) => {
setSelectedRecord(record)
// 根據競賽規則初始化評分項目
const initialScores: Record<string, number> = {}
// 直接使用記錄中的評分數據,不依賴競賽規則
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: { ...record.scores },
comments: record.comments,
scores: initialScores,
comments: record.comments || '',
})
setShowEditScoring(true)
}
@@ -211,12 +283,11 @@ export function ScoringManagement() {
}
// 檢查所有評分項目是否都已評分
const scoringRules = selectedCompetition?.rules || []
const defaultRules = [
{ name: "創新性" }, { name: "技術性" }, { name: "實用性" },
{ name: "展示效果" }, { name: "影響力" }
]
const rules = scoringRules.length > 0 ? scoringRules : defaultRules
const rules = competitionRules.length > 0 ? competitionRules : defaultRules
const hasAllScores = rules.every((rule: any) =>
manualScoring.scores[rule.name] && manualScoring.scores[rule.name] > 0
@@ -234,18 +305,54 @@ export function ScoringManagement() {
setIsLoading(true)
try {
await submitJudgeScore({
judgeId: manualScoring.judgeId,
appId: manualScoring.participantId,
scores: manualScoring.scores,
comments: manualScoring.comments.trim(),
// 轉換評分格式以符合API要求 - 使用動態規則
const apiScores: Record<string, number> = {}
rules.forEach((rule: any) => {
apiScores[rule.name] = manualScoring.scores[rule.name] || 0
})
setSuccess(showEditScoring ? "評分更新成功!" : "評分提交成功!")
loadScoringData()
setShowManualScoring(false)
setShowEditScoring(false)
setSelectedRecord(null)
// 根據參賽者類型確定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 ? "評分更新成功!" : "評分提交成功!")
await loadScoringData() // 重新載入數據
setShowManualScoring(false)
setShowEditScoring(false)
setSelectedRecord(null)
} else {
setError(data.message || "評分提交失敗")
}
} catch (err) {
console.error('評分提交失敗:', err)
setError("評分提交失敗,請重試")
} finally {
setIsLoading(false)
@@ -261,15 +368,244 @@ export function ScoringManagement() {
}
}
const getScoringProgress = () => {
const total = scoringRecords.length
const completed = scoringRecords.filter(r => r.status === "completed").length
const pending = scoringRecords.filter(r => r.status === "pending").length
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0
return { total, completed, pending, percentage }
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)
}
}
const progress = getScoringProgress()
// 當選擇競賽時載入統計數據
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([])
}
// 載入競賽參賽者(應用和團隊)
console.log('📱 載入競賽參賽者...')
const [appsResponse, teamsResponse] = await Promise.all([
fetch(`/api/competitions/${selectedCompetition.id}/apps`),
fetch(`/api/competitions/${selectedCompetition.id}/teams`)
])
const appsData = await appsResponse.json()
const teamsData = await teamsResponse.json()
console.log('應用API回應:', appsData)
console.log('團隊API回應:', teamsData)
const participants = []
if (appsData.success && appsData.data && appsData.data.apps) {
participants.push(...appsData.data.apps.map((app: any) => ({
id: app.id,
name: app.name,
type: 'individual',
creator: app.creator
})))
console.log('✅ 應用數據載入成功:', appsData.data.apps.length, '個應用')
} else {
console.error('❌ 應用數據載入失敗:', appsData.message || 'API回應格式錯誤')
}
if (teamsData.success && teamsData.data && teamsData.data.teams) {
// 將每個團隊的每個 app 作為獨立的參賽項目
teamsData.data.teams.forEach((team: any) => {
console.log('🔍 處理團隊:', team);
if (team.apps && team.apps.length > 0) {
team.apps.forEach((app: any) => {
console.log('🔍 處理團隊 app:', app);
participants.push({
id: app.id, // 使用 app 的 ID
name: app.name, // app 名稱
type: 'team',
teamName: team.name || '未知團隊', // 團隊名稱
displayName: `${team.name || '未知團隊'} - ${app.name}`, // 顯示名稱:團隊名稱 - app名稱
creator: team.members && team.members.find((m: any) => m.role === '隊長')?.name || '未知隊長',
teamId: team.id // 保存團隊 ID
})
})
} else {
// 如果團隊沒有 app仍然顯示團隊本身
participants.push({
id: team.id,
name: team.name,
type: 'team',
teamName: team.name || '未知團隊',
creator: team.members && team.members.find((m: any) => m.role === '隊長')?.name || '未知隊長',
teamId: team.id
})
}
})
console.log('✅ 團隊數據載入成功:', teamsData.data.teams.length, '個團隊')
} else {
console.error('❌ 團隊數據載入失敗:', teamsData.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 progress = {
total: scoringStats.totalScores,
completed: scoringStats.completedScores,
pending: scoringStats.pendingScores,
percentage: scoringStats.completionRate
}
// 顯示初始載入狀態
if (isInitialLoading) {
return (
<div className="space-y-6">
<Card>
<CardContent className="flex items-center justify-center py-12">
<div className="text-center space-y-4">
<Loader2 className="w-8 h-8 animate-spin mx-auto" />
<p className="text-lg font-medium">...</p>
<p className="text-sm text-gray-500"></p>
</div>
</CardContent>
</Card>
</div>
)
}
return (
<div className="space-y-6">
@@ -297,7 +633,9 @@ export function ScoringManagement() {
</CardHeader>
<CardContent>
<Select value={selectedCompetition?.id || ""} onValueChange={(value) => {
console.log('🎯 選擇競賽:', value)
const competition = competitions.find(c => c.id === value)
console.log('🏆 找到競賽:', competition)
setSelectedCompetition(competition)
}}>
<SelectTrigger className="w-full">
@@ -338,7 +676,29 @@ export function ScoringManagement() {
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">{progress.completed}</p>
<p className="text-2xl font-bold text-blue-600">
{scoringSummary ? scoringSummary.overallStats.totalJudges : progress.completed}
</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-green-600">
{scoringSummary ? scoringSummary.overallStats.totalApps : progress.pending}
</p>
<p className="text-sm text-gray-600">APP數</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-orange-600">
{scoringSummary ? scoringSummary.overallStats.completedScores : progress.percentage}
</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
@@ -346,24 +706,10 @@ export function ScoringManagement() {
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-orange-600">{progress.pending}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-green-600">{progress.percentage}%</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-purple-600">{progress.total}</p>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold text-purple-600">
{scoringSummary ? `${scoringSummary.overallStats.overallCompletionRate}%` : progress.total}
</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
@@ -372,42 +718,54 @@ export function ScoringManagement() {
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span></span>
<span>{progress.completed} / {progress.total}</span>
<span>
{scoringSummary ?
`${scoringSummary.overallStats.completedScores} / ${scoringSummary.overallStats.totalPossibleScores}` :
`${progress.completed} / ${progress.total}`
}
</span>
</div>
<Progress value={progress.percentage} className="h-2" />
<Progress
value={scoringSummary ? scoringSummary.overallStats.overallCompletionRate : progress.percentage}
className="h-2"
/>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="flex items-center space-x-2">
<ClipboardList className="w-5 h-5" />
<span></span>
</CardTitle>
<div className="flex space-x-2">
<Button
onClick={() => setShowScoringLink(true)}
variant="outline"
className="border-blue-200 text-blue-600 hover:bg-blue-50"
>
<Link className="w-4 h-4 mr-2" />
</Button>
<Button
onClick={() => setShowJudgeList(true)}
variant="outline"
>
<Users className="w-4 h-4 mr-2" />
</Button>
<Button onClick={handleManualScoring} variant="outline">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
<div>
<CardTitle className="flex items-center space-x-2">
<BarChart3 className="w-5 h-5" />
<span></span>
</CardTitle>
<CardDescription>APP評分詳情和完成度狀況</CardDescription>
</div>
<div className="flex space-x-2">
<Button
onClick={() => setShowScoringLink(true)}
variant="outline"
className="border-blue-200 text-blue-600 hover:bg-blue-50"
>
<Link className="w-4 h-4 mr-2" />
</Button>
<Button
onClick={() => setShowJudgeList(true)}
variant="outline"
>
<Users className="w-4 h-4 mr-2" />
</Button>
<Button onClick={handleManualScoring} variant="outline">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
@@ -443,25 +801,28 @@ export function ScoringManagement() {
<div className="space-y-6">
{(() => {
// 按評審分組
// 按評審分組 (使用 judgeId 避免重名問題)
const groupedByJudge = getFilteredRecords().reduce((groups, record) => {
const judgeName = record.judgeName
if (!groups[judgeName]) {
groups[judgeName] = []
const judgeId = record.judgeId
if (!groups[judgeId]) {
groups[judgeId] = []
}
groups[judgeName].push(record)
groups[judgeId].push(record)
return groups
}, {} as Record<string, ScoringRecord[]>)
return Object.entries(groupedByJudge).map(([judgeName, records]) => {
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 (
<Card key={judgeName} className="border-l-4 border-l-blue-500">
<Card key={judgeId} className="border-l-4 border-l-blue-500">
<CardHeader className="pb-3">
<div className="flex justify-between items-center">
<div className="flex items-center space-x-3">
@@ -513,7 +874,7 @@ export function ScoringManagement() {
{records.length > 4 && (
<button
onClick={() => {
const container = document.getElementById(`scroll-${judgeName}`)
const container = document.getElementById(`scroll-${judgeId}`)
if (container) {
container.scrollLeft -= 280 // 滑動一個卡片的寬度
}
@@ -528,7 +889,7 @@ export function ScoringManagement() {
{records.length > 4 && (
<button
onClick={() => {
const container = document.getElementById(`scroll-${judgeName}`)
const container = document.getElementById(`scroll-${judgeId}`)
if (container) {
container.scrollLeft += 280 // 滑動一個卡片的寬度
}
@@ -540,7 +901,7 @@ export function ScoringManagement() {
)}
<div
id={`scroll-${judgeName}`}
id={`scroll-${judgeId}`}
className="flex space-x-4 overflow-x-auto scrollbar-hide pb-2 scroll-smooth"
style={{
scrollbarWidth: 'none',
@@ -573,8 +934,8 @@ export function ScoringManagement() {
<div className="flex items-center justify-between">
<div className="text-center">
<div className="flex items-center space-x-1">
<span className="font-bold text-lg">{record.totalScore}</span>
<span className="text-gray-500 text-sm">/ 10</span>
<span className="font-bold text-lg">{Math.round(record.totalScore)}</span>
<span className="text-gray-500 text-sm">/ 100</span>
</div>
</div>
<div className="flex flex-col items-end space-y-1">
@@ -662,11 +1023,27 @@ export function ScoringManagement() {
<SelectValue placeholder="選擇評審" />
</SelectTrigger>
<SelectContent>
{judges.map((judge) => (
<SelectItem key={judge.id} value={judge.id}>
{judge.name}
{isLoadingData ? (
<SelectItem value="loading-judges" disabled>
<div className="flex items-center space-x-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span>...</span>
</div>
</SelectItem>
))}
) : competitionJudges.length > 0 ? (
competitionJudges.map((judge) => (
<SelectItem key={judge.id} value={judge.id}>
<div className="flex flex-col">
<span className="font-medium">{judge.name}</span>
<span className="text-xs text-gray-500">{judge.title} - {judge.department}</span>
</div>
</SelectItem>
))
) : (
<SelectItem value="no-judges" disabled>
<span className="text-gray-500"></span>
</SelectItem>
)}
</SelectContent>
</Select>
</div>
@@ -681,16 +1058,15 @@ export function ScoringManagement() {
<SelectValue placeholder="選擇參賽者" />
</SelectTrigger>
<SelectContent>
{[
...(selectedCompetition?.participatingApps || []).map((appId: string) => {
const app = mockIndividualApps.find(a => a.id === appId)
return { id: appId, name: app?.name || `應用 ${appId}`, type: "individual" }
}),
...(selectedCompetition?.participatingTeams || []).map((teamId: string) => {
const team = initialTeams.find(t => t.id === teamId)
return { id: teamId, name: team?.name || `團隊 ${teamId}`, type: "team" }
})
].map((participant) => (
{isLoadingData ? (
<SelectItem value="loading-participants" disabled>
<div className="flex items-center space-x-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span>...</span>
</div>
</SelectItem>
) : competitionParticipants.length > 0 ? (
competitionParticipants.map((participant) => (
<SelectItem key={participant.id} value={participant.id}>
<div className="flex items-center space-x-2">
{participant.type === "individual" ? (
@@ -698,13 +1074,23 @@ export function ScoringManagement() {
) : (
<Users className="w-4 h-4 text-green-600" />
)}
<span>{participant.name}</span>
<Badge variant="outline" className="text-xs">
{participant.type === "individual" ? "個人" : "團隊"}
</Badge>
<div className="flex flex-col">
<span className="font-medium">
{participant.type === "individual"
? `個人 - ${participant.name}`
: participant.displayName || `${participant.teamName} - ${participant.name}`
}
</span>
<span className="text-xs text-gray-500">{participant.creator}</span>
</div>
</div>
</SelectItem>
))}
))
) : (
<SelectItem value="no-participants" disabled>
<span className="text-gray-500"></span>
</SelectItem>
)}
</SelectContent>
</Select>
</div>
@@ -714,7 +1100,6 @@ export function ScoringManagement() {
<div className="space-y-4">
<h3 className="text-lg font-semibold"></h3>
{(() => {
const scoringRules = selectedCompetition?.rules || []
const defaultRules = [
{ name: "創新性", description: "創新程度和獨特性", weight: 25 },
{ name: "技術性", description: "技術實現的複雜度和品質", weight: 30 },
@@ -723,7 +1108,7 @@ export function ScoringManagement() {
{ name: "影響力", description: "對行業或社會的潛在影響", weight: 10 }
]
const rules = scoringRules.length > 0 ? scoringRules : defaultRules
const rules = competitionRules.length > 0 ? competitionRules : defaultRules
return rules.map((rule: any, index: number) => (
<div key={index} className="space-y-4 p-6 border rounded-lg bg-white shadow-sm">
@@ -776,9 +1161,9 @@ export function ScoringManagement() {
</div>
<div className="flex items-center space-x-3">
<span className="text-4xl font-bold text-blue-600">
{calculateTotalScore(manualScoring.scores, selectedCompetition?.rules || [])}
{Math.round(calculateTotalScore(manualScoring.scores, competitionRules) * 10)}
</span>
<span className="text-xl text-gray-500 font-medium">/ 10</span>
<span className="text-xl text-gray-500 font-medium">/ 100</span>
</div>
</div>
</div>
@@ -842,15 +1227,17 @@ export function ScoringManagement() {
<JudgeListDialog
open={showJudgeList}
onOpenChange={setShowJudgeList}
judges={selectedCompetition ?
judges
.filter(judge => selectedCompetition.judges.includes(judge.id))
.map(judge => ({
id: judge.id,
name: judge.name,
specialty: "評審專家"
})) : []
}
judges={competitionJudges.map(judge => ({
id: judge.id,
name: judge.name,
specialty: judge.specialty || "評審專家",
expertise: judge.expertise || [],
title: judge.title,
department: judge.department,
email: judge.email,
phone: judge.phone,
organization: judge.organization
}))}
/>
</div>
)