Files
hr-assessment-system/app/tests/combined/page.tsx

692 lines
25 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, useEffect } from "react"
import { TestLayout } from "@/components/test-layout"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Label } from "@/components/ui/label"
import { Progress } from "@/components/ui/progress"
import { useRouter } from "next/navigation"
import { calculateCombinedScore } from "@/lib/utils/score-calculator"
import { useAuth } from "@/lib/hooks/use-auth"
import { TimeWarningModal } from "@/components/time-warning-modal"
interface LogicQuestion {
id: number
question: string
option_a: string
option_b: string
option_c: string
option_d: string
option_e: string
correct_answer: 'A' | 'B' | 'C' | 'D' | 'E'
explanation?: string
created_at: string
}
interface CreativeQuestion {
id: number
statement: string
category: 'innovation' | 'imagination' | 'flexibility' | 'originality'
is_reverse: boolean
created_at: string
}
type TestPhase = "logic" | "creative" | "completed"
export default function CombinedTestPage() {
const router = useRouter()
const { user } = useAuth()
const [phase, setPhase] = useState<TestPhase>("logic")
const [currentQuestion, setCurrentQuestion] = useState(0)
const [logicAnswers, setLogicAnswers] = useState<Record<number, string>>({})
const [creativeAnswers, setCreativeAnswers] = useState<Record<number, number>>({})
const [timeRemaining, setTimeRemaining] = useState(30 * 60) // 30 minutes total
const [hasShownWarning, setHasShownWarning] = useState(false)
const [logicQuestions, setLogicQuestions] = useState<LogicQuestion[]>([])
const [creativeQuestions, setCreativeQuestions] = useState<CreativeQuestion[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isSubmitting, setIsSubmitting] = useState(false)
const [hasTimedOut, setHasTimedOut] = useState(false) // 防止重複提交
const [timeoutSubmitted, setTimeoutSubmitted] = useState(false) // 確保只提交一次
// 彈窗狀態
const [showWarningModal, setShowWarningModal] = useState(false)
const [showTimeoutModal, setShowTimeoutModal] = useState(false)
const [showSuccessModal, setShowSuccessModal] = useState(false)
const [modalMessage, setModalMessage] = useState('')
// Load questions from database
useEffect(() => {
const loadQuestions = async () => {
try {
// 清除之前的提交標記
localStorage.removeItem('combinedTestTimeoutSubmitted')
// Load logic questions
const logicResponse = await fetch('/api/logic-questions')
const logicData = await logicResponse.json()
// Load creative questions
const creativeResponse = await fetch('/api/creative-questions')
const creativeData = await creativeResponse.json()
if (logicData.success && creativeData.success) {
setLogicQuestions(logicData.questions)
setCreativeQuestions(creativeData.questions)
} else {
console.error('Failed to load questions:', logicData.error || creativeData.error)
}
} catch (error) {
console.error('Error loading questions:', error)
} finally {
setIsLoading(false)
}
}
loadQuestions()
}, [])
// Timer effect
useEffect(() => {
if (logicQuestions.length === 0 && creativeQuestions.length === 0) return
const timer = setInterval(() => {
setTimeRemaining((prev) => {
const newTime = prev - 1
// 檢查是否剩餘5分鐘300秒且尚未顯示警告
if (newTime <= 300 && !hasShownWarning) {
setHasShownWarning(true)
setModalMessage('距離測試結束還有5分鐘請盡快完成剩餘題目')
setShowWarningModal(true)
}
// 時間到,強制提交
if (newTime <= 0 && !hasTimedOut && !timeoutSubmitted) {
// 檢查 localStorage 是否已經提交過
const alreadySubmitted = localStorage.getItem('combinedTestTimeoutSubmitted')
if (alreadySubmitted) {
console.log('⏰ 已經提交過時間到結果,跳過重複提交')
setHasTimedOut(true)
setTimeoutSubmitted(true)
return 0
}
console.log('⏰ 時間到!強制提交測驗...')
localStorage.setItem('combinedTestTimeoutSubmitted', 'true')
setHasTimedOut(true) // 防止重複提交
setTimeoutSubmitted(true) // 確保只提交一次
handleTimeoutSubmit()
return 0
}
return newTime
})
}, 1000)
return () => clearInterval(timer)
}, [logicQuestions, creativeQuestions, hasShownWarning, hasTimedOut, timeoutSubmitted])
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
}
const getCurrentQuestions = () => {
return phase === "logic" ? logicQuestions : creativeQuestions
}
const getTotalQuestions = () => {
return logicQuestions.length + creativeQuestions.length
}
const getOverallProgress = () => {
const logicCompleted = phase === "logic" ? currentQuestion : logicQuestions.length
const creativeCompleted = phase === "creative" ? currentQuestion : 0
return logicCompleted + creativeCompleted
}
const handleAnswerChange = (value: string) => {
if (phase === "logic") {
setLogicAnswers((prev) => ({
...prev,
[currentQuestion]: value,
}))
} else {
setCreativeAnswers((prev) => ({
...prev,
[currentQuestion]: Number.parseInt(value),
}))
}
}
const handleNext = () => {
const currentQuestions = getCurrentQuestions()
if (currentQuestion < currentQuestions.length - 1) {
setCurrentQuestion((prev) => prev + 1)
} else if (phase === "logic") {
// Switch to creative phase
setPhase("creative")
setCurrentQuestion(0)
} else {
// Complete the test
handleSubmit()
}
}
const handlePrevious = () => {
if (currentQuestion > 0) {
setCurrentQuestion((prev) => prev - 1)
} else if (phase === "creative") {
// Go back to logic phase
setPhase("logic")
setCurrentQuestion(logicQuestions.length - 1)
}
}
const handleTimeoutSubmit = async () => {
// 防止重複提交 - 多重檢查
if (isSubmitting || hasTimedOut || timeoutSubmitted) {
console.log('⏰ 已經在處理時間到提交,跳過重複請求')
return
}
// 再次檢查 localStorage
const alreadySubmitted = localStorage.getItem('combinedTestTimeoutSubmitted')
if (alreadySubmitted) {
console.log('⏰ localStorage 顯示已經提交過,跳過重複請求')
return
}
console.log('⏰ 時間到!強制提交綜合測試...')
console.log('用戶狀態:', user)
if (!user) {
console.log('❌ 用戶未登入')
setModalMessage('時間到!但用戶未登入,無法提交結果。')
setShowTimeoutModal(true)
return
}
console.log('✅ 用戶已登入用戶ID:', user.id)
setIsSubmitting(true)
setHasTimedOut(true)
setTimeoutSubmitted(true)
try {
// Calculate logic score
let logicCorrect = 0
logicQuestions.forEach((question, index) => {
if (logicAnswers[index] === question.correct_answer) {
logicCorrect++
}
})
const logicScore = Math.round((logicCorrect / logicQuestions.length) * 100)
// Calculate creativity score
let creativityTotal = 0
const processedCreativeAnswers: Record<number, number> = {}
creativeQuestions.forEach((question, index) => {
const answer = creativeAnswers[index] || 1
const processedScore = question.is_reverse ? 6 - answer : answer
creativityTotal += processedScore
processedCreativeAnswers[index] = processedScore
})
const creativityMaxScore = creativeQuestions.length * 5
const creativityScore = Math.round((creativityTotal / creativityMaxScore) * 100)
// Calculate combined score
const combinedResult = calculateCombinedScore(logicScore, creativityScore)
// Store results in localStorage with timeout flag
const results = {
type: "combined",
logicScore,
creativityScore,
overallScore: combinedResult.overallScore,
level: combinedResult.level,
description: combinedResult.description,
breakdown: combinedResult.breakdown,
logicAnswers,
creativeAnswers: processedCreativeAnswers,
logicCorrect,
creativityTotal,
creativityMaxScore,
completedAt: new Date().toISOString(),
isTimeout: true // 標記為時間到強制提交
}
localStorage.setItem("combinedTestResults", JSON.stringify(results))
console.log('✅ 強制提交結果已儲存到 localStorage')
// Upload to database with timeout flag
console.log('🔄 開始上傳強制提交結果到資料庫...')
const uploadData = {
userId: user.id,
logicScore,
creativityScore,
overallScore: combinedResult.overallScore,
level: combinedResult.level,
description: combinedResult.description,
logicBreakdown: {
correct: logicCorrect,
total: logicQuestions.length,
answers: logicAnswers
},
creativityBreakdown: {
total: creativityTotal,
maxScore: creativityMaxScore,
answers: processedCreativeAnswers
},
balanceScore: combinedResult.breakdown.balance,
completedAt: new Date().toISOString(),
isTimeout: true // 標記為時間到強制提交
}
console.log('強制提交上傳數據:', uploadData)
const uploadResponse = await fetch('/api/test-results/combined', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(uploadData)
})
console.log('📡 強制提交 API 響應狀態:', uploadResponse.status)
const uploadResult = await uploadResponse.json()
console.log('📡 強制提交 API 響應內容:', uploadResult)
if (uploadResult.success) {
console.log('✅ 強制提交結果已上傳到資料庫')
console.log('測試結果ID:', uploadResult.data.testResult.id)
} else {
console.error('❌ 強制提交上傳到資料庫失敗:', uploadResult.error)
}
// 顯示時間到提示
setModalMessage('測試時間已到!系統已自動提交您的答案。')
setShowTimeoutModal(true)
// 延遲跳轉,讓用戶看到提示
setTimeout(() => {
router.push("/results/combined")
}, 3000)
} catch (error) {
console.error('❌ 強制提交測驗失敗:', error)
setModalMessage('時間到!但提交失敗,請聯繫管理員。')
setShowTimeoutModal(true)
} finally {
setIsSubmitting(false)
}
}
const handleSubmit = async () => {
console.log('🔍 開始提交綜合測試...')
console.log('用戶狀態:', user)
if (!user) {
console.log('❌ 用戶未登入')
alert('請先登入')
return
}
console.log('✅ 用戶已登入用戶ID:', user.id)
setIsSubmitting(true)
try {
// Calculate logic score
let logicCorrect = 0
logicQuestions.forEach((question, index) => {
if (logicAnswers[index] === question.correct_answer) {
logicCorrect++
}
})
const logicScore = Math.round((logicCorrect / logicQuestions.length) * 100)
// Calculate creativity score
let creativityTotal = 0
const processedCreativeAnswers: Record<number, number> = {}
creativeQuestions.forEach((question, index) => {
const answer = creativeAnswers[index] || 1
const processedScore = question.is_reverse ? 6 - answer : answer
creativityTotal += processedScore
processedCreativeAnswers[index] = processedScore
})
const creativityMaxScore = creativeQuestions.length * 5
const creativityScore = Math.round((creativityTotal / creativityMaxScore) * 100)
// Calculate combined score
const combinedResult = calculateCombinedScore(logicScore, creativityScore)
// Store results in localStorage (for backward compatibility)
const results = {
type: "combined",
logicScore,
creativityScore,
overallScore: combinedResult.overallScore,
level: combinedResult.level,
description: combinedResult.description,
breakdown: combinedResult.breakdown,
logicAnswers,
creativeAnswers: processedCreativeAnswers,
logicCorrect,
creativityTotal,
creativityMaxScore,
completedAt: new Date().toISOString(),
isTimeout: false // 標記為正常提交
}
localStorage.setItem("combinedTestResults", JSON.stringify(results))
console.log('✅ 結果已儲存到 localStorage')
// Upload to database
console.log('🔄 開始上傳到資料庫...')
const uploadData = {
userId: user.id,
logicScore,
creativityScore,
overallScore: combinedResult.overallScore,
level: combinedResult.level,
description: combinedResult.description,
logicBreakdown: {
correct: logicCorrect,
total: logicQuestions.length,
answers: logicAnswers
},
creativityBreakdown: {
total: creativityTotal,
maxScore: creativityMaxScore,
answers: processedCreativeAnswers
},
balanceScore: combinedResult.breakdown.balance,
completedAt: new Date().toISOString(),
isTimeout: false // 標記為正常提交
}
console.log('上傳數據:', uploadData)
const uploadResponse = await fetch('/api/test-results/combined', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(uploadData)
})
console.log('📡 API 響應狀態:', uploadResponse.status)
const uploadResult = await uploadResponse.json()
console.log('📡 API 響應內容:', uploadResult)
if (uploadResult.success) {
console.log('✅ 綜合測試結果已上傳到資料庫')
console.log('測試結果ID:', uploadResult.data.testResult.id)
} else {
console.error('❌ 上傳到資料庫失敗:', uploadResult.error)
// 即使上傳失敗,也繼續顯示結果
}
router.push("/results/combined")
} catch (error) {
console.error('❌ 提交測驗失敗:', error)
alert('提交測驗失敗,請重試')
} finally {
setIsSubmitting(false)
}
}
const currentQuestions = getCurrentQuestions()
const currentQ = currentQuestions[currentQuestion]
const isLastQuestion = phase === "creative" && currentQuestion === creativeQuestions.length - 1
const hasAnswer =
phase === "logic" ? logicAnswers[currentQuestion] !== undefined : creativeAnswers[currentQuestion] !== undefined
const getPhaseTitle = () => {
if (phase === "logic") return "第一部分:邏輯思維測試"
return "第二部分:創意能力測試"
}
const getQuestionNumber = () => {
if (phase === "logic") return currentQuestion + 1
return logicQuestions.length + currentQuestion + 1
}
if (isLoading) {
return (
<TestLayout
title="綜合能力測試"
currentQuestion={0}
totalQuestions={0}
timeRemaining="00:00"
onBack={() => router.push("/")}
>
<div className="max-w-4xl mx-auto text-center">
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-muted-foreground">...</p>
</div>
</TestLayout>
)
}
if (logicQuestions.length === 0 || creativeQuestions.length === 0) {
return (
<TestLayout
title="綜合能力測試"
currentQuestion={0}
totalQuestions={0}
timeRemaining="00:00"
onBack={() => router.push("/")}
>
<div className="max-w-4xl mx-auto text-center">
<p className="text-muted-foreground mb-4"></p>
<Button onClick={() => window.location.reload()}></Button>
</div>
</TestLayout>
)
}
return (
<TestLayout
title="綜合能力測試"
currentQuestion={getQuestionNumber()}
totalQuestions={getTotalQuestions()}
timeRemaining={formatTime(timeRemaining)}
onBack={() => router.push("/")}
>
<div className="max-w-4xl mx-auto">
{/* Phase Indicator */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-foreground">{getPhaseTitle()}</h2>
<div className="text-sm text-muted-foreground">
{phase === "logic"
? `${currentQuestion + 1}/${logicQuestions.length}`
: `${currentQuestion + 1}/${creativeQuestions.length}`}
</div>
</div>
<Progress value={(getOverallProgress() / getTotalQuestions()) * 100} className="h-2" />
</div>
<Card className="mb-8">
<CardHeader>
<CardTitle className="text-xl text-balance">
{phase === "logic" ? (currentQ as LogicQuestion).question : (currentQ as CreativeQuestion).statement}
</CardTitle>
{phase === "logic" && (
<p className="text-sm text-muted-foreground mt-2">
</p>
)}
{phase === "creative" && (
<p className="text-sm text-muted-foreground">5=1=</p>
)}
</CardHeader>
<CardContent>
<RadioGroup
value={
phase === "logic"
? logicAnswers[currentQuestion] || ""
: creativeAnswers[currentQuestion]?.toString() || ""
}
onValueChange={handleAnswerChange}
className="space-y-4"
>
{phase === "logic"
? // Logic question options
[
{ value: 'A', text: (currentQ as LogicQuestion).option_a },
{ value: 'B', text: (currentQ as LogicQuestion).option_b },
{ value: 'C', text: (currentQ as LogicQuestion).option_c },
{ value: 'D', text: (currentQ as LogicQuestion).option_d },
{ value: 'E', text: (currentQ as LogicQuestion).option_e }
].map((option, index) => (
<div
key={index}
className="flex items-center space-x-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors"
>
<RadioGroupItem value={option.value} id={`option-${index}`} />
<Label htmlFor={`option-${index}`} className="flex-1 cursor-pointer text-base leading-relaxed">
{option.value}. {option.text}
</Label>
</div>
))
: // Creative question options
[
{ value: "5", label: "我最符合", color: "text-green-600", bgColor: "bg-green-50" },
{ value: "4", label: "比較符合", color: "text-green-500", bgColor: "bg-green-50" },
{ value: "3", label: "一般", color: "text-yellow-500", bgColor: "bg-yellow-50" },
{ value: "2", label: "不太符合", color: "text-orange-500", bgColor: "bg-orange-50" },
{ value: "1", label: "與我不符", color: "text-red-500", bgColor: "bg-red-50" },
].map((option) => (
<div
key={option.value}
className={`flex items-center space-x-4 p-4 rounded-lg border ${option.bgColor} hover:bg-muted/50 transition-colors`}
>
<RadioGroupItem value={option.value} id={`option-${option.value}`} />
<Label
htmlFor={`option-${option.value}`}
className={`flex-1 cursor-pointer text-base font-medium ${option.color}`}
>
{option.label}
</Label>
</div>
))}
</RadioGroup>
</CardContent>
</Card>
{/* Navigation */}
<div className="flex justify-between items-center">
<Button variant="outline" onClick={handlePrevious} disabled={phase === "logic" && currentQuestion === 0}>
</Button>
<div className="flex gap-2 max-w-md overflow-x-auto">
{/* Logic questions indicators */}
{logicQuestions.map((_, index) => (
<button
key={`logic-${index}`}
onClick={() => {
if (phase === "logic" || phase === "creative") {
setPhase("logic")
setCurrentQuestion(index)
}
}}
className={`w-8 h-8 rounded-full text-sm font-medium transition-colors flex-shrink-0 ${
phase === "logic" && index === currentQuestion
? "bg-primary text-primary-foreground"
: logicAnswers[index] !== undefined
? "bg-primary/70 text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-muted/80"
}`}
>
{index + 1}
</button>
))}
{/* Separator */}
<div className="w-px h-8 bg-border mx-2"></div>
{/* Creative questions indicators */}
{creativeQuestions.map((_, index) => (
<button
key={`creative-${index}`}
onClick={() => {
if (phase === "creative") {
setCurrentQuestion(index)
}
}}
className={`w-8 h-8 rounded-full text-sm font-medium transition-colors flex-shrink-0 ${
phase === "creative" && index === currentQuestion
? "bg-accent text-accent-foreground"
: creativeAnswers[index] !== undefined
? "bg-accent/70 text-accent-foreground"
: phase === "creative"
? "bg-muted text-muted-foreground hover:bg-muted/80"
: "bg-muted/50 text-muted-foreground/50"
}`}
disabled={phase === "logic"}
>
{logicQuestions.length + index + 1}
</button>
))}
</div>
{isLastQuestion ? (
<Button onClick={handleSubmit} disabled={!hasAnswer || isSubmitting} className="bg-green-600 hover:bg-green-700">
{isSubmitting ? '提交中...' : '提交測試'}
</Button>
) : (
<Button onClick={handleNext} disabled={!hasAnswer}>
{phase === "logic" && currentQuestion === logicQuestions.length - 1 ? "進入第二部分" : "下一題"}
</Button>
)}
</div>
{/* Progress Summary */}
<div className="mt-8 text-center text-sm text-muted-foreground">
{getOverallProgress()} / {getTotalQuestions()}
<br />
{phase === "logic" ? "邏輯思維測試" : "創意能力測試"} (
{Object.keys(phase === "logic" ? logicAnswers : creativeAnswers).length} / {currentQuestions.length} )
</div>
</div>
{/* 時間警告彈窗 */}
<TimeWarningModal
isOpen={showWarningModal}
onClose={() => setShowWarningModal(false)}
type="warning"
title="⚠️ 時間提醒"
message={modalMessage}
showCountdown={false}
/>
{/* 時間到彈窗 */}
<TimeWarningModal
isOpen={showTimeoutModal}
onClose={() => setShowTimeoutModal(false)}
type="timeout"
title="⏰ 時間到"
message={modalMessage}
showCountdown={true}
countdownSeconds={3}
/>
{/* 成功提交彈窗 */}
<TimeWarningModal
isOpen={showSuccessModal}
onClose={() => setShowSuccessModal(false)}
type="success"
title="✅ 提交成功"
message={modalMessage}
showCountdown={false}
/>
</TestLayout>
)
}