Files
ai-showcase-platform/app/judge-scoring/page.tsx

540 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState } from "react"
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 { Alert, AlertDescription } from "@/components/ui/alert"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Textarea } from "@/components/ui/textarea"
import { AlertTriangle, CheckCircle, User, Trophy, LogIn, Loader2, Eye, EyeOff, Lock } from "lucide-react"
interface Judge {
id: string
name: string
specialty: string
}
interface ScoringItem {
id: string
name: string
type: "individual" | "team"
status: "pending" | "completed"
score?: number
submittedAt?: string
}
export default function JudgeScoringPage() {
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [judgeId, setJudgeId] = useState("")
const [accessCode, setAccessCode] = useState("")
const [currentJudge, setCurrentJudge] = useState<Judge | null>(null)
const [scoringItems, setScoringItems] = useState<ScoringItem[]>([])
const [selectedItem, setSelectedItem] = useState<ScoringItem | null>(null)
const [showScoringDialog, setShowScoringDialog] = useState(false)
const [scores, setScores] = useState<Record<string, number>>({})
const [comments, setComments] = useState("")
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState("")
const [success, setSuccess] = useState("")
const [showAccessCode, setShowAccessCode] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [competitionRules, setCompetitionRules] = useState<any[]>([])
const handleLogin = async () => {
setError("")
setIsLoading(true)
if (!judgeId.trim() || !accessCode.trim()) {
setError("請填寫評審ID和存取碼")
setIsLoading(false)
return
}
if (accessCode !== "judge2024") {
setError("存取碼錯誤")
setIsLoading(false)
return
}
try {
// 獲取評審的評分任務
const response = await fetch(`/api/judge/scoring-tasks?judgeId=${judgeId}&competitionId=0fffae9a-9539-11f0-b5d9-6e36c63cdb98`)
const data = await response.json()
if (data.success) {
setCurrentJudge(data.data.judge)
setScoringItems(data.data.tasks)
setIsLoggedIn(true)
setSuccess("登入成功!")
setTimeout(() => setSuccess(""), 3000)
// 載入競賽規則
await loadCompetitionRules()
} else {
setError(data.message || "登入失敗")
}
} catch (err) {
console.error('登入失敗:', err)
setError("登入失敗,請重試")
} finally {
setIsLoading(false)
}
}
const loadCompetitionRules = async () => {
try {
// 使用正確的競賽ID
const response = await fetch('/api/competitions/0fffae9a-9539-11f0-b5d9-6e36c63cdb98/rules')
const data = await response.json()
if (data.success) {
setCompetitionRules(data.data)
}
} catch (err) {
console.error('載入競賽規則失敗:', err)
}
}
const handleStartScoring = async (item: ScoringItem) => {
setSelectedItem(item)
// 如果是重新評分,嘗試載入現有的評分數據
if (item.status === "completed") {
try {
// 這裡可以添加載入現有評分數據的邏輯
// 暫時使用默認值
const initialScores: Record<string, number> = {}
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
}
setScores(initialScores)
setComments("")
} catch (err) {
console.error('載入現有評分數據失敗:', err)
}
} else {
// 新評分初始化為0
const initialScores: Record<string, number> = {}
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
}
setScores(initialScores)
setComments("")
}
setShowScoringDialog(true)
}
const handleSubmitScore = async () => {
if (!selectedItem || !currentJudge) return
setIsSubmitting(true)
try {
// 計算總分 (1-10分制轉換為100分制)
const totalScore = (Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length) * 10
// 提交評分到 API
const response = await fetch('/api/admin/scoring', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
judgeId: currentJudge.id,
participantId: selectedItem.id,
participantType: 'app',
scores: scores,
comments: comments.trim(),
competitionId: '0fffae9a-9539-11f0-b5d9-6e36c63cdb98', // 正確的競賽ID
isEdit: selectedItem.status === "completed", // 如果是重新評分,標記為編輯模式
recordId: selectedItem.status === "completed" ? selectedItem.id : null
})
})
const data = await response.json()
if (data.success) {
// 更新本地狀態
setScoringItems(prev => prev.map(item =>
item.id === selectedItem.id
? { ...item, status: "completed", score: totalScore, submittedAt: new Date().toISOString() }
: item
))
setShowScoringDialog(false)
setSelectedItem(null)
setScores({})
setComments("")
setSuccess("評分提交成功!")
setTimeout(() => setSuccess(""), 3000)
} else {
setError(data.message || "評分提交失敗")
}
} catch (err) {
console.error('評分提交失敗:', err)
setError("評分提交失敗,請重試")
} finally {
setIsSubmitting(false)
}
}
const getProgress = () => {
const total = scoringItems.length
const completed = scoringItems.filter(item => item.status === "completed").length
return { total, completed, percentage: total > 0 ? Math.round((completed / total) * 100) : 0 }
}
const isFormValid = () => {
// 檢查所有評分項目是否都已評分
const rules = competitionRules && competitionRules.length > 0 ? competitionRules : [
{ name: "創新性" }, { name: "技術性" }, { name: "實用性" },
{ name: "展示效果" }, { name: "影響力" }
]
const allScoresFilled = rules.every((rule: any) =>
scores[rule.name] && scores[rule.name] > 0
)
// 檢查評審意見是否填寫
const commentsFilled = comments.trim().length > 0
return allScoresFilled && commentsFilled
}
const progress = getProgress()
if (!isLoggedIn) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<div className="mx-auto w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-4">
<Trophy className="w-8 h-8 text-blue-600" />
</div>
<CardTitle className="text-2xl"></CardTitle>
<CardDescription>
ID和存取碼進行登入
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{error && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="space-y-2">
<Label htmlFor="judgeId">ID</Label>
<Input
id="judgeId"
placeholder="例如j1"
value={judgeId}
onChange={(e) => setJudgeId(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="accessCode"></Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
id="accessCode"
type={showAccessCode ? "text" : "password"}
placeholder="請輸入存取碼"
value={accessCode}
onChange={(e) => setAccessCode(e.target.value)}
className="pl-10 pr-10"
/>
<button
type="button"
onClick={() => setShowAccessCode(!showAccessCode)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showAccessCode ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<Button
onClick={handleLogin}
className="w-full"
size="lg"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<LogIn className="w-4 h-4 mr-2" />
</>
)}
</Button>
</CardContent>
</Card>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50 p-4">
<div className="max-w-6xl mx-auto space-y-6">
{/* 成功訊息 */}
{success && (
<Alert className="border-green-200 bg-green-50">
<CheckCircle className="h-4 w-4 text-green-600" />
<AlertDescription className="text-green-800">{success}</AlertDescription>
</Alert>
)}
{/* 評審資訊 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Avatar className="w-12 h-12">
<AvatarFallback className="text-lg font-semibold">
{currentJudge?.name.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<h1 className="text-2xl font-bold">{currentJudge?.name}</h1>
<p className="text-gray-600">{currentJudge?.specialty}</p>
</div>
</div>
<Button
variant="outline"
onClick={() => setIsLoggedIn(false)}
>
</Button>
</div>
</CardHeader>
</Card>
{/* 評分進度 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between text-sm">
<span></span>
<span>{progress.completed} / {progress.total}</span>
</div>
<Progress value={progress.percentage} className="h-2" />
<div className="text-center">
<span className="text-2xl font-bold text-blue-600">{progress.percentage}%</span>
</div>
</div>
</CardContent>
</Card>
{/* 評分項目列表 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{scoringItems.map((item) => (
<div
key={item.id}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
{item.type === "individual" ? (
<User className="w-4 h-4 text-blue-600" />
) : (
<div className="flex space-x-1">
<User className="w-4 h-4 text-green-600" />
<User className="w-4 h-4 text-green-600" />
</div>
)}
<span className="font-medium">{item.display_name || item.name}</span>
<Badge variant="outline">
{item.type === "individual" ? "個人" : "團隊"}
</Badge>
</div>
</div>
<div className="flex items-center space-x-4">
{item.status === "completed" ? (
<div className="flex items-center space-x-3">
<div className="text-center">
<div className="text-lg font-bold text-green-600">{item.score}</div>
<div className="text-xs text-gray-500">/ 100</div>
<div className="text-xs text-gray-500">
{item.submittedAt ? new Date(item.submittedAt).toLocaleDateString('zh-TW') : ''}
</div>
</div>
<Button
onClick={() => handleStartScoring(item)}
variant="outline"
size="sm"
>
</Button>
</div>
) : (
<Button
onClick={() => handleStartScoring(item)}
variant="outline"
size="sm"
>
</Button>
)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* 評分對話框 */}
<Dialog open={showScoringDialog} onOpenChange={setShowScoringDialog}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{selectedItem?.name}</DialogTitle>
<DialogDescription>
滿10
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* 評分項目 */}
<div className="space-y-4">
<h3 className="text-lg font-semibold"></h3>
{(competitionRules && competitionRules.length > 0 ? competitionRules : [
{ name: "創新性", description: "創新程度和獨特性" },
{ name: "技術性", description: "技術實現的複雜度和品質" },
{ name: "實用性", description: "實際應用價值和用戶體驗" },
{ name: "展示效果", description: "展示的清晰度和吸引力" },
{ name: "影響力", description: "對行業或社會的潛在影響" }
]).map((criterion, index) => (
<div key={index} className="space-y-2">
<Label className="flex items-center space-x-1">
<span>{criterion.name}</span>
<span className="text-red-500">*</span>
</Label>
<p className="text-sm text-gray-600">{criterion.description}</p>
<div className="flex space-x-2">
{Array.from({ length: 10 }, (_, i) => i + 1).map((score) => (
<button
key={score}
type="button"
onClick={() => setScores(prev => ({ ...prev, [criterion.name]: score }))}
className={`w-10 h-10 rounded border-2 font-semibold transition-all ${
scores[criterion.name] === score
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:border-blue-400'
}`}
>
{score}
</button>
))}
</div>
{!scores[criterion.name] && (
<p className="text-xs text-red-500"></p>
)}
</div>
))}
</div>
{/* 評審意見 */}
<div className="space-y-2">
<Label className="flex items-center space-x-1">
<span></span>
<span className="text-red-500">*</span>
</Label>
<Textarea
placeholder="請詳細填寫評審意見、優點分析、改進建議等..."
value={comments}
onChange={(e) => setComments(e.target.value)}
rows={4}
className={!comments.trim() ? "border-red-300" : ""}
/>
{!comments.trim() && (
<p className="text-xs text-red-500"></p>
)}
</div>
{/* 總分顯示 */}
<div className="p-4 bg-blue-50 rounded-lg">
<div className="flex justify-between items-center">
<span className="font-semibold"></span>
<span className="text-2xl font-bold text-blue-600">
{Object.values(scores).length > 0
? Math.round(Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length * 10)
: 0
} / 100
</span>
</div>
</div>
</div>
<div className="flex justify-end space-x-4 pt-6 border-t">
<Button
variant="outline"
onClick={() => setShowScoringDialog(false)}
>
</Button>
<Button
onClick={handleSubmitScore}
disabled={isSubmitting || !isFormValid()}
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
"提交評分"
)}
</Button>
</div>
</DialogContent>
</Dialog>
</div>
)
}