修正時間到上船數據問題
This commit is contained in:
95
DATABASE_MIGRATION_GUIDE.md
Normal file
95
DATABASE_MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# 資料庫遷移指南 - 添加 is_timeout 欄位
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
本次遷移為現有的測試結果表添加 `is_timeout` 欄位,用於標記測試是否因為時間到而強制提交。
|
||||||
|
|
||||||
|
## 受影響的表
|
||||||
|
1. `test_results` - 邏輯測試和創意測試結果表
|
||||||
|
2. `combined_test_results` - 綜合測試結果表
|
||||||
|
|
||||||
|
## 遷移內容
|
||||||
|
為每個表添加以下欄位:
|
||||||
|
```sql
|
||||||
|
is_timeout BOOLEAN DEFAULT FALSE
|
||||||
|
```
|
||||||
|
|
||||||
|
## 執行方法
|
||||||
|
|
||||||
|
### 方法一:直接執行 SQL 腳本(推薦)
|
||||||
|
1. 打開 MySQL 客戶端(如 phpMyAdmin、MySQL Workbench 或命令行)
|
||||||
|
2. 連接到 `hr_assessment` 資料庫
|
||||||
|
3. 執行以下 SQL 命令:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
USE hr_assessment;
|
||||||
|
|
||||||
|
-- 添加 test_results 表的 is_timeout 欄位
|
||||||
|
ALTER TABLE test_results
|
||||||
|
ADD COLUMN is_timeout BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- 添加 combined_test_results 表的 is_timeout 欄位
|
||||||
|
ALTER TABLE combined_test_results
|
||||||
|
ADD COLUMN is_timeout BOOLEAN DEFAULT FALSE;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法二:使用 Node.js 腳本
|
||||||
|
1. 確保已安裝 mysql2 依賴:
|
||||||
|
```bash
|
||||||
|
npm install mysql2
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 執行遷移腳本:
|
||||||
|
```bash
|
||||||
|
node scripts/add-timeout-columns.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法三:應用程式自動遷移
|
||||||
|
當應用程式重新啟動時,會自動執行遷移(如果欄位不存在)。
|
||||||
|
|
||||||
|
## 驗證遷移
|
||||||
|
執行以下查詢來驗證欄位是否成功添加:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 檢查 test_results 表結構
|
||||||
|
DESCRIBE test_results;
|
||||||
|
|
||||||
|
-- 檢查 combined_test_results 表結構
|
||||||
|
DESCRIBE combined_test_results;
|
||||||
|
|
||||||
|
-- 確認欄位存在
|
||||||
|
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'hr_assessment'
|
||||||
|
AND TABLE_NAME IN ('test_results', 'combined_test_results')
|
||||||
|
AND COLUMN_NAME = 'is_timeout';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 預期結果
|
||||||
|
- `test_results` 表應該包含 `is_timeout` 欄位
|
||||||
|
- `combined_test_results` 表應該包含 `is_timeout` 欄位
|
||||||
|
- 所有現有記錄的 `is_timeout` 值應該為 `FALSE`
|
||||||
|
|
||||||
|
## 回滾方法
|
||||||
|
如果需要移除這些欄位(不建議):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
USE hr_assessment;
|
||||||
|
|
||||||
|
-- 移除 test_results 表的 is_timeout 欄位
|
||||||
|
ALTER TABLE test_results DROP COLUMN is_timeout;
|
||||||
|
|
||||||
|
-- 移除 combined_test_results 表的 is_timeout 欄位
|
||||||
|
ALTER TABLE combined_test_results DROP COLUMN is_timeout;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
1. 此遷移是向後兼容的,不會影響現有功能
|
||||||
|
2. 所有現有記錄的 `is_timeout` 值將設為 `FALSE`
|
||||||
|
3. 新的測試結果將根據實際情況設置 `is_timeout` 值
|
||||||
|
4. 建議在生產環境執行前先在測試環境驗證
|
||||||
|
|
||||||
|
## 完成後的功能
|
||||||
|
- 綜合測試時間從 45 分鐘改為 30 分鐘
|
||||||
|
- 剩餘 5 分鐘時會彈出提醒
|
||||||
|
- 時間到後自動提交並標記為時間到
|
||||||
|
- 管理員可以在結果頁面看到時間到標記
|
@@ -553,6 +553,7 @@ function AdminResultDetailContent() {
|
|||||||
balanceScore={result.details.abilityBalance || 0}
|
balanceScore={result.details.abilityBalance || 0}
|
||||||
level={getScoreLevel(result.score, result.type).level}
|
level={getScoreLevel(result.score, result.type).level}
|
||||||
description={getScoreLevel(result.score, result.type).description}
|
description={getScoreLevel(result.score, result.type).description}
|
||||||
|
isTimeout={result.isTimeout}
|
||||||
logicBreakdown={result.details.breakdown}
|
logicBreakdown={result.details.breakdown}
|
||||||
creativityBreakdown={result.details.breakdown}
|
creativityBreakdown={result.details.breakdown}
|
||||||
// 個別測試結果的詳細資料
|
// 個別測試結果的詳細資料
|
||||||
|
@@ -123,7 +123,8 @@ export async function GET(request: NextRequest) {
|
|||||||
userId: testResult.user_id,
|
userId: testResult.user_id,
|
||||||
type: testResult.test_type,
|
type: testResult.test_type,
|
||||||
score: testResult.score,
|
score: testResult.score,
|
||||||
completedAt: testResult.completed_at
|
completedAt: testResult.completed_at,
|
||||||
|
isTimeout: testResult.is_timeout || false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 獲取詳細答案
|
// 獲取詳細答案
|
||||||
|
@@ -10,7 +10,8 @@ export async function POST(request: NextRequest) {
|
|||||||
const {
|
const {
|
||||||
userId,
|
userId,
|
||||||
answers,
|
answers,
|
||||||
completedAt
|
completedAt,
|
||||||
|
isTimeout = false
|
||||||
} = body
|
} = body
|
||||||
|
|
||||||
// 驗證必要欄位
|
// 驗證必要欄位
|
||||||
@@ -81,7 +82,8 @@ export async function POST(request: NextRequest) {
|
|||||||
score: scorePercentage,
|
score: scorePercentage,
|
||||||
total_questions: questions.length,
|
total_questions: questions.length,
|
||||||
correct_answers: totalScore,
|
correct_answers: totalScore,
|
||||||
completed_at: mysqlCompletedAt
|
completed_at: mysqlCompletedAt,
|
||||||
|
is_timeout: isTimeout
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('測試結果建立結果:', testResult)
|
console.log('測試結果建立結果:', testResult)
|
||||||
|
@@ -136,16 +136,6 @@ export default function CombinedResultsPage() {
|
|||||||
返回首頁
|
返回首頁
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="outline" size="lg" className="w-full">
|
|
||||||
<Link href="/tests/logic">
|
|
||||||
開始邏輯測試
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<Button asChild variant="outline" size="lg" className="w-full">
|
|
||||||
<Link href="/tests/creative">
|
|
||||||
開始創意測試
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@@ -114,16 +114,6 @@ export default function CreativeResultsPage() {
|
|||||||
返回首頁
|
返回首頁
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="outline" size="lg" className="w-full">
|
|
||||||
<Link href="/tests/logic">
|
|
||||||
開始邏輯測試
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<Button asChild variant="outline" size="lg" className="w-full">
|
|
||||||
<Link href="/tests/combined">
|
|
||||||
開始綜合測試
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@@ -147,16 +147,6 @@ export default function LogicResultsPage() {
|
|||||||
返回首頁
|
返回首頁
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="outline" size="lg" className="w-full">
|
|
||||||
<Link href="/tests/creative">
|
|
||||||
開始創意測試
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<Button asChild variant="outline" size="lg" className="w-full">
|
|
||||||
<Link href="/tests/combined">
|
|
||||||
開始綜合測試
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@@ -10,6 +10,7 @@ import { Progress } from "@/components/ui/progress"
|
|||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { calculateCombinedScore } from "@/lib/utils/score-calculator"
|
import { calculateCombinedScore } from "@/lib/utils/score-calculator"
|
||||||
import { useAuth } from "@/lib/hooks/use-auth"
|
import { useAuth } from "@/lib/hooks/use-auth"
|
||||||
|
import { TimeWarningModal } from "@/components/time-warning-modal"
|
||||||
|
|
||||||
interface LogicQuestion {
|
interface LogicQuestion {
|
||||||
id: number
|
id: number
|
||||||
@@ -47,11 +48,22 @@ export default function CombinedTestPage() {
|
|||||||
const [creativeQuestions, setCreativeQuestions] = useState<CreativeQuestion[]>([])
|
const [creativeQuestions, setCreativeQuestions] = useState<CreativeQuestion[]>([])
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
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
|
// Load questions from database
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadQuestions = async () => {
|
const loadQuestions = async () => {
|
||||||
try {
|
try {
|
||||||
|
// 清除之前的提交標記
|
||||||
|
localStorage.removeItem('combinedTestTimeoutSubmitted')
|
||||||
|
|
||||||
// Load logic questions
|
// Load logic questions
|
||||||
const logicResponse = await fetch('/api/logic-questions')
|
const logicResponse = await fetch('/api/logic-questions')
|
||||||
const logicData = await logicResponse.json()
|
const logicData = await logicResponse.json()
|
||||||
@@ -87,12 +99,25 @@ export default function CombinedTestPage() {
|
|||||||
// 檢查是否剩餘5分鐘(300秒)且尚未顯示警告
|
// 檢查是否剩餘5分鐘(300秒)且尚未顯示警告
|
||||||
if (newTime <= 300 && !hasShownWarning) {
|
if (newTime <= 300 && !hasShownWarning) {
|
||||||
setHasShownWarning(true)
|
setHasShownWarning(true)
|
||||||
alert('⚠️ 注意:距離測試結束還有5分鐘,請盡快完成剩餘題目!')
|
setModalMessage('距離測試結束還有5分鐘,請盡快完成剩餘題目!')
|
||||||
|
setShowWarningModal(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 時間到,強制提交
|
// 時間到,強制提交
|
||||||
if (newTime <= 0) {
|
if (newTime <= 0 && !hasTimedOut && !timeoutSubmitted) {
|
||||||
|
// 檢查 localStorage 是否已經提交過
|
||||||
|
const alreadySubmitted = localStorage.getItem('combinedTestTimeoutSubmitted')
|
||||||
|
if (alreadySubmitted) {
|
||||||
|
console.log('⏰ 已經提交過時間到結果,跳過重複提交')
|
||||||
|
setHasTimedOut(true)
|
||||||
|
setTimeoutSubmitted(true)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
console.log('⏰ 時間到!強制提交測驗...')
|
console.log('⏰ 時間到!強制提交測驗...')
|
||||||
|
localStorage.setItem('combinedTestTimeoutSubmitted', 'true')
|
||||||
|
setHasTimedOut(true) // 防止重複提交
|
||||||
|
setTimeoutSubmitted(true) // 確保只提交一次
|
||||||
handleTimeoutSubmit()
|
handleTimeoutSubmit()
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -102,7 +127,7 @@ export default function CombinedTestPage() {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
}, [logicQuestions, creativeQuestions, hasShownWarning])
|
}, [logicQuestions, creativeQuestions, hasShownWarning, hasTimedOut, timeoutSubmitted])
|
||||||
|
|
||||||
const formatTime = (seconds: number) => {
|
const formatTime = (seconds: number) => {
|
||||||
const mins = Math.floor(seconds / 60)
|
const mins = Math.floor(seconds / 60)
|
||||||
@@ -164,17 +189,33 @@ export default function CombinedTestPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleTimeoutSubmit = async () => {
|
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('⏰ 時間到!強制提交綜合測試...')
|
||||||
console.log('用戶狀態:', user)
|
console.log('用戶狀態:', user)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
console.log('❌ 用戶未登入')
|
console.log('❌ 用戶未登入')
|
||||||
alert('⏰ 時間到!但用戶未登入,無法提交結果。')
|
setModalMessage('時間到!但用戶未登入,無法提交結果。')
|
||||||
|
setShowTimeoutModal(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ 用戶已登入,用戶ID:', user.id)
|
console.log('✅ 用戶已登入,用戶ID:', user.id)
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
setHasTimedOut(true)
|
||||||
|
setTimeoutSubmitted(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Calculate logic score
|
// Calculate logic score
|
||||||
@@ -267,12 +308,18 @@ export default function CombinedTestPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 顯示時間到提示
|
// 顯示時間到提示
|
||||||
alert('⏰ 測試時間已到!系統已自動提交您的答案。')
|
setModalMessage('測試時間已到!系統已自動提交您的答案。')
|
||||||
router.push("/results/combined")
|
setShowTimeoutModal(true)
|
||||||
|
|
||||||
|
// 延遲跳轉,讓用戶看到提示
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push("/results/combined")
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 強制提交測驗失敗:', error)
|
console.error('❌ 強制提交測驗失敗:', error)
|
||||||
alert('⏰ 時間到!但提交失敗,請聯繫管理員。')
|
setModalMessage('時間到!但提交失敗,請聯繫管理員。')
|
||||||
|
setShowTimeoutModal(true)
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
@@ -608,6 +655,37 @@ export default function CombinedTestPage() {
|
|||||||
{Object.keys(phase === "logic" ? logicAnswers : creativeAnswers).length} / {currentQuestions.length} 題)
|
{Object.keys(phase === "logic" ? logicAnswers : creativeAnswers).length} / {currentQuestions.length} 題)
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</TestLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ interface CombinedAnalysisProps {
|
|||||||
balanceScore: number
|
balanceScore: number
|
||||||
level: string
|
level: string
|
||||||
description: string
|
description: string
|
||||||
|
isTimeout?: boolean
|
||||||
logicBreakdown?: any
|
logicBreakdown?: any
|
||||||
creativityBreakdown?: any
|
creativityBreakdown?: any
|
||||||
// 個別測試結果的詳細資料
|
// 個別測試結果的詳細資料
|
||||||
@@ -34,6 +35,7 @@ export function CombinedAnalysis({
|
|||||||
balanceScore,
|
balanceScore,
|
||||||
level,
|
level,
|
||||||
description,
|
description,
|
||||||
|
isTimeout = false,
|
||||||
logicBreakdown,
|
logicBreakdown,
|
||||||
creativityBreakdown,
|
creativityBreakdown,
|
||||||
// 個別測試結果的詳細資料
|
// 個別測試結果的詳細資料
|
||||||
@@ -120,11 +122,18 @@ export function CombinedAnalysis({
|
|||||||
>
|
>
|
||||||
<span className="text-3xl font-bold text-white">{overallScore}</span>
|
<span className="text-3xl font-bold text-white">{overallScore}</span>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-3xl mb-2">綜合評估完成!</CardTitle>
|
<CardTitle className="text-3xl mb-2">
|
||||||
|
綜合測試{isTimeout ? '(時間到)' : ''}完成!
|
||||||
|
</CardTitle>
|
||||||
<div className="flex items-center justify-center gap-2 mb-4">
|
<div className="flex items-center justify-center gap-2 mb-4">
|
||||||
<Badge variant="secondary" className="text-lg px-4 py-1 bg-gradient-to-r from-blue-500 to-teal-500 text-white">
|
<Badge variant="secondary" className="text-lg px-4 py-1 bg-gradient-to-r from-blue-500 to-teal-500 text-white">
|
||||||
{level}
|
{level}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{isTimeout && (
|
||||||
|
<Badge variant="destructive" className="text-lg px-4 py-1">
|
||||||
|
時間到強制提交
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg text-muted-foreground mb-3">{description}</p>
|
<p className="text-lg text-muted-foreground mb-3">{description}</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
124
components/time-warning-modal.tsx
Normal file
124
components/time-warning-modal.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { AlertTriangle, Clock, CheckCircle } from "lucide-react"
|
||||||
|
|
||||||
|
interface TimeWarningModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
type: 'warning' | 'timeout' | 'success'
|
||||||
|
title: string
|
||||||
|
message: string
|
||||||
|
showCountdown?: boolean
|
||||||
|
countdownSeconds?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TimeWarningModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
type,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
showCountdown = false,
|
||||||
|
countdownSeconds = 0
|
||||||
|
}: TimeWarningModalProps) {
|
||||||
|
const [remainingSeconds, setRemainingSeconds] = useState(countdownSeconds)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && showCountdown && remainingSeconds > 0) {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setRemainingSeconds(prev => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
clearInterval(timer)
|
||||||
|
onClose()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return prev - 1
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}
|
||||||
|
}, [isOpen, showCountdown, remainingSeconds, onClose])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setRemainingSeconds(countdownSeconds)
|
||||||
|
}
|
||||||
|
}, [isOpen, countdownSeconds])
|
||||||
|
|
||||||
|
const getIcon = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'warning':
|
||||||
|
return <AlertTriangle className="w-8 h-8 text-yellow-500" />
|
||||||
|
case 'timeout':
|
||||||
|
return <Clock className="w-8 h-8 text-red-500" />
|
||||||
|
case 'success':
|
||||||
|
return <CheckCircle className="w-8 h-8 text-green-500" />
|
||||||
|
default:
|
||||||
|
return <AlertTriangle className="w-8 h-8 text-yellow-500" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBgColor = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'warning':
|
||||||
|
return 'bg-yellow-50 border-yellow-200'
|
||||||
|
case 'timeout':
|
||||||
|
return 'bg-red-50 border-red-200'
|
||||||
|
case 'success':
|
||||||
|
return 'bg-green-50 border-green-200'
|
||||||
|
default:
|
||||||
|
return 'bg-yellow-50 border-yellow-200'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className={`sm:max-w-md ${getBgColor()}`}>
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center justify-center mb-4">
|
||||||
|
<div className="p-3 rounded-full bg-white shadow-lg">
|
||||||
|
{getIcon()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogTitle className="text-center text-xl font-bold">
|
||||||
|
{title}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<p className="text-gray-700 leading-relaxed">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{showCountdown && remainingSeconds > 0 && (
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-red-500 text-white flex items-center justify-center font-bold">
|
||||||
|
{remainingSeconds}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-600">秒後自動關閉</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-center space-x-3 pt-4">
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
className={`px-6 ${
|
||||||
|
type === 'warning'
|
||||||
|
? 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||||||
|
: type === 'timeout'
|
||||||
|
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||||
|
: 'bg-green-500 hover:bg-green-600 text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{type === 'warning' ? '我知道了' : type === 'timeout' ? '確認' : '好的'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
46
database-migrations/add-timeout-columns.sql
Normal file
46
database-migrations/add-timeout-columns.sql
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
-- 添加 is_timeout 欄位到現有資料庫表
|
||||||
|
-- 執行時間: 2024-01-XX
|
||||||
|
-- 描述: 為測試結果表添加時間到標記欄位
|
||||||
|
|
||||||
|
USE hr_assessment;
|
||||||
|
|
||||||
|
-- 檢查並添加 test_results 表的 is_timeout 欄位
|
||||||
|
SET @column_exists = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'test_results'
|
||||||
|
AND COLUMN_NAME = 'is_timeout'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE test_results ADD COLUMN is_timeout BOOLEAN DEFAULT FALSE',
|
||||||
|
'SELECT "test_results 表的 is_timeout 欄位已存在,跳過" as message'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 檢查並添加 combined_test_results 表的 is_timeout 欄位
|
||||||
|
SET @column_exists = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'combined_test_results'
|
||||||
|
AND COLUMN_NAME = 'is_timeout'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE combined_test_results ADD COLUMN is_timeout BOOLEAN DEFAULT FALSE',
|
||||||
|
'SELECT "combined_test_results 表的 is_timeout 欄位已存在,跳過" as message'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 驗證欄位是否添加成功
|
||||||
|
DESCRIBE test_results;
|
||||||
|
DESCRIBE combined_test_results;
|
||||||
|
|
||||||
|
-- 顯示成功訊息
|
||||||
|
SELECT 'is_timeout 欄位添加完成!' AS message;
|
@@ -6,6 +6,7 @@ import { createTestResultsTable } from './models/test_result'
|
|||||||
import { createLogicTestAnswersTable } from './models/logic_test_answer'
|
import { createLogicTestAnswersTable } from './models/logic_test_answer'
|
||||||
import { createCreativeTestAnswersTable } from './models/creative_test_answer'
|
import { createCreativeTestAnswersTable } from './models/creative_test_answer'
|
||||||
import { createCombinedTestResultsTable } from './models/combined_test_result'
|
import { createCombinedTestResultsTable } from './models/combined_test_result'
|
||||||
|
import { addTimeoutColumns } from './migrations/add-timeout-columns'
|
||||||
|
|
||||||
// 初始化資料庫
|
// 初始化資料庫
|
||||||
export async function initializeDatabase(): Promise<boolean> {
|
export async function initializeDatabase(): Promise<boolean> {
|
||||||
@@ -40,6 +41,9 @@ export async function initializeDatabase(): Promise<boolean> {
|
|||||||
// 建立綜合測試結果表
|
// 建立綜合測試結果表
|
||||||
await createCombinedTestResultsTable()
|
await createCombinedTestResultsTable()
|
||||||
|
|
||||||
|
// 執行資料庫遷移(添加 is_timeout 欄位)
|
||||||
|
await addTimeoutColumns()
|
||||||
|
|
||||||
console.log('✅ 資料庫初始化完成')
|
console.log('✅ 資料庫初始化完成')
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
71
lib/database/migrations/add-timeout-columns.ts
Normal file
71
lib/database/migrations/add-timeout-columns.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { executeQuery } from '../connection'
|
||||||
|
|
||||||
|
// 添加 is_timeout 欄位到現有資料庫表
|
||||||
|
export async function addTimeoutColumns(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('🔄 開始添加 is_timeout 欄位...')
|
||||||
|
|
||||||
|
// 檢查並添加 test_results 表的 is_timeout 欄位
|
||||||
|
try {
|
||||||
|
// 先檢查欄位是否已存在
|
||||||
|
const checkColumnQuery = `
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'test_results'
|
||||||
|
AND COLUMN_NAME = 'is_timeout'
|
||||||
|
`
|
||||||
|
const result = await executeQuery(checkColumnQuery)
|
||||||
|
const columnExists = result[0]?.count > 0
|
||||||
|
|
||||||
|
if (!columnExists) {
|
||||||
|
await executeQuery(`
|
||||||
|
ALTER TABLE test_results
|
||||||
|
ADD COLUMN is_timeout BOOLEAN DEFAULT FALSE
|
||||||
|
`)
|
||||||
|
console.log('✅ test_results 表已添加 is_timeout 欄位')
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ test_results 表的 is_timeout 欄位已存在,跳過')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ 檢查/添加 test_results 表 is_timeout 欄位失敗:', error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查並添加 combined_test_results 表的 is_timeout 欄位
|
||||||
|
try {
|
||||||
|
// 先檢查欄位是否已存在
|
||||||
|
const checkColumnQuery = `
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'combined_test_results'
|
||||||
|
AND COLUMN_NAME = 'is_timeout'
|
||||||
|
`
|
||||||
|
const result = await executeQuery(checkColumnQuery)
|
||||||
|
const columnExists = result[0]?.count > 0
|
||||||
|
|
||||||
|
if (!columnExists) {
|
||||||
|
await executeQuery(`
|
||||||
|
ALTER TABLE combined_test_results
|
||||||
|
ADD COLUMN is_timeout BOOLEAN DEFAULT FALSE
|
||||||
|
`)
|
||||||
|
console.log('✅ combined_test_results 表已添加 is_timeout 欄位')
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ combined_test_results 表的 is_timeout 欄位已存在,跳過')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ 檢查/添加 combined_test_results 表 is_timeout 欄位失敗:', error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ is_timeout 欄位添加完成')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 添加 is_timeout 欄位失敗:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 執行遷移
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
// 只在伺服器端執行
|
||||||
|
addTimeoutColumns().catch(console.error)
|
||||||
|
}
|
86
scripts/add-timeout-columns.js
Normal file
86
scripts/add-timeout-columns.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
const mysql = require('mysql2/promise');
|
||||||
|
|
||||||
|
// 資料庫配置
|
||||||
|
const dbConfig = {
|
||||||
|
host: 'localhost',
|
||||||
|
user: 'root',
|
||||||
|
password: '123456',
|
||||||
|
database: 'hr_assessment',
|
||||||
|
port: 3306
|
||||||
|
};
|
||||||
|
|
||||||
|
async function addTimeoutColumns() {
|
||||||
|
let connection;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔄 連接到資料庫...');
|
||||||
|
connection = await mysql.createConnection(dbConfig);
|
||||||
|
|
||||||
|
console.log('✅ 資料庫連接成功');
|
||||||
|
console.log('🔄 開始添加 is_timeout 欄位...');
|
||||||
|
|
||||||
|
// 檢查並添加 test_results 表的 is_timeout 欄位
|
||||||
|
try {
|
||||||
|
// 先檢查欄位是否已存在
|
||||||
|
const [checkResult] = await connection.execute(`
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'test_results'
|
||||||
|
AND COLUMN_NAME = 'is_timeout'
|
||||||
|
`);
|
||||||
|
const columnExists = checkResult[0].count > 0;
|
||||||
|
|
||||||
|
if (!columnExists) {
|
||||||
|
await connection.execute(`
|
||||||
|
ALTER TABLE test_results
|
||||||
|
ADD COLUMN is_timeout BOOLEAN DEFAULT FALSE
|
||||||
|
`);
|
||||||
|
console.log('✅ test_results 表已添加 is_timeout 欄位');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ test_results 表的 is_timeout 欄位已存在,跳過');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 檢查/添加 test_results 表 is_timeout 欄位失敗:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查並添加 combined_test_results 表的 is_timeout 欄位
|
||||||
|
try {
|
||||||
|
// 先檢查欄位是否已存在
|
||||||
|
const [checkResult] = await connection.execute(`
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'combined_test_results'
|
||||||
|
AND COLUMN_NAME = 'is_timeout'
|
||||||
|
`);
|
||||||
|
const columnExists = checkResult[0].count > 0;
|
||||||
|
|
||||||
|
if (!columnExists) {
|
||||||
|
await connection.execute(`
|
||||||
|
ALTER TABLE combined_test_results
|
||||||
|
ADD COLUMN is_timeout BOOLEAN DEFAULT FALSE
|
||||||
|
`);
|
||||||
|
console.log('✅ combined_test_results 表已添加 is_timeout 欄位');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ combined_test_results 表的 is_timeout 欄位已存在,跳過');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 檢查/添加 combined_test_results 表 is_timeout 欄位失敗:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ is_timeout 欄位添加完成');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 執行失敗:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
if (connection) {
|
||||||
|
await connection.end();
|
||||||
|
console.log('📝 資料庫連接已關閉');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 執行遷移
|
||||||
|
addTimeoutColumns();
|
Reference in New Issue
Block a user