完成評審評分機制
This commit is contained in:
@@ -42,67 +42,164 @@ export default function JudgeScoringPage() {
|
||||
const [error, setError] = useState("")
|
||||
const [success, setSuccess] = useState("")
|
||||
const [showAccessCode, setShowAccessCode] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [competitionRules, setCompetitionRules] = useState<any[]>([])
|
||||
|
||||
// Judge data - empty for production
|
||||
const mockJudges: Judge[] = []
|
||||
|
||||
// Scoring items - empty for production
|
||||
const mockScoringItems: ScoringItem[] = []
|
||||
|
||||
const handleLogin = () => {
|
||||
const handleLogin = async () => {
|
||||
setError("")
|
||||
setIsLoading(true)
|
||||
|
||||
if (!judgeId.trim() || !accessCode.trim()) {
|
||||
setError("請填寫評審ID和存取碼")
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (accessCode !== "judge2024") {
|
||||
setError("存取碼錯誤")
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const judge = mockJudges.find(j => j.id === judgeId)
|
||||
if (!judge) {
|
||||
setError("評審ID不存在")
|
||||
return
|
||||
try {
|
||||
// 獲取評審的評分任務
|
||||
const response = await fetch(`/api/judge/scoring-tasks?judgeId=${judgeId}`)
|
||||
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)
|
||||
}
|
||||
|
||||
setCurrentJudge(judge)
|
||||
setScoringItems(mockScoringItems)
|
||||
setIsLoggedIn(true)
|
||||
setSuccess("登入成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
|
||||
const handleStartScoring = (item: ScoringItem) => {
|
||||
const loadCompetitionRules = async () => {
|
||||
try {
|
||||
// 使用正確的競賽ID
|
||||
const response = await fetch('/api/competitions/be47d842-91f1-11f0-8595-bd825523ae01/rules')
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setCompetitionRules(data.data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('載入競賽規則失敗:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartScoring = async (item: ScoringItem) => {
|
||||
setSelectedItem(item)
|
||||
setScores({})
|
||||
setComments("")
|
||||
|
||||
// 如果是重新評分,嘗試載入現有的評分數據
|
||||
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) return
|
||||
if (!selectedItem || !currentJudge) return
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
// 模擬提交評分
|
||||
setTimeout(() => {
|
||||
setScoringItems(prev => prev.map(item =>
|
||||
item.id === selectedItem.id
|
||||
? { ...item, status: "completed", score: Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length, submittedAt: new Date().toISOString() }
|
||||
: item
|
||||
))
|
||||
try {
|
||||
// 計算總分 (1-10分制,轉換為100分制)
|
||||
const totalScore = (Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length) * 10
|
||||
|
||||
setShowScoringDialog(false)
|
||||
setSelectedItem(null)
|
||||
setScores({})
|
||||
setComments("")
|
||||
// 提交評分到 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: 'be47d842-91f1-11f0-8595-bd825523ae01', // 正確的競賽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)
|
||||
setSuccess("評分提交成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const getProgress = () => {
|
||||
@@ -111,6 +208,23 @@ export default function JudgeScoringPage() {
|
||||
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) {
|
||||
@@ -170,9 +284,19 @@ export default function JudgeScoringPage() {
|
||||
onClick={handleLogin}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<LogIn className="w-4 h-4 mr-2" />
|
||||
登入評分系統
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
登入中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LogIn className="w-4 h-4 mr-2" />
|
||||
登入評分系統
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="text-center text-sm text-gray-500">
|
||||
@@ -268,7 +392,7 @@ export default function JudgeScoringPage() {
|
||||
<User className="w-4 h-4 text-green-600" />
|
||||
</div>
|
||||
)}
|
||||
<span className="font-medium">{item.name}</span>
|
||||
<span className="font-medium">{item.display_name || item.name}</span>
|
||||
<Badge variant="outline">
|
||||
{item.type === "individual" ? "個人" : "團隊"}
|
||||
</Badge>
|
||||
@@ -277,10 +401,21 @@ export default function JudgeScoringPage() {
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{item.status === "completed" ? (
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-green-600">{item.score}</div>
|
||||
<div className="text-xs text-gray-500">/ 10</div>
|
||||
<div className="text-xs text-gray-500">{item.submittedAt}</div>
|
||||
<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
|
||||
@@ -313,15 +448,18 @@ export default function JudgeScoringPage() {
|
||||
{/* 評分項目 */}
|
||||
<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) => (
|
||||
]).map((criterion, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<Label>{criterion.name}</Label>
|
||||
<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) => (
|
||||
@@ -339,19 +477,29 @@ export default function JudgeScoringPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!scores[criterion.name] && (
|
||||
<p className="text-xs text-red-500">請為此項目打分</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 評審意見 */}
|
||||
<div className="space-y-2">
|
||||
<Label>評審意見</Label>
|
||||
<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>
|
||||
|
||||
{/* 總分顯示 */}
|
||||
@@ -360,9 +508,9 @@ export default function JudgeScoringPage() {
|
||||
<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)
|
||||
? Math.round(Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length * 10)
|
||||
: 0
|
||||
} / 10
|
||||
} / 100
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -377,7 +525,7 @@ export default function JudgeScoringPage() {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmitScore}
|
||||
disabled={isSubmitting || Object.keys(scores).length < 5 || !comments.trim()}
|
||||
disabled={isSubmitting || !isFormValid()}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
|
Reference in New Issue
Block a user