實現測驗資料匯出、查看詳細功能
This commit is contained in:
@@ -7,8 +7,9 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Brain, Lightbulb, BarChart3, ArrowLeft, Search, Download, Filter, ChevronLeft, ChevronRight, Loader2 } from "lucide-react"
|
import { Brain, Lightbulb, BarChart3, ArrowLeft, Search, Download, Filter, ChevronLeft, ChevronRight, Loader2, Eye } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useAuth } from "@/lib/hooks/use-auth"
|
import { useAuth } from "@/lib/hooks/use-auth"
|
||||||
|
|
||||||
@@ -81,6 +82,10 @@ function AdminResultsContent() {
|
|||||||
const [testTypeFilter, setTestTypeFilter] = useState("all")
|
const [testTypeFilter, setTestTypeFilter] = useState("all")
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [selectedResult, setSelectedResult] = useState<TestResult | null>(null)
|
||||||
|
const [showDetailModal, setShowDetailModal] = useState(false)
|
||||||
|
const [detailData, setDetailData] = useState<any>(null)
|
||||||
|
const [isLoadingDetail, setIsLoadingDetail] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData()
|
loadData()
|
||||||
@@ -246,6 +251,29 @@ function AdminResultsContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleViewDetail = async (result: TestResult) => {
|
||||||
|
setSelectedResult(result)
|
||||||
|
setShowDetailModal(true)
|
||||||
|
setIsLoadingDetail(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/test-results/detail?testResultId=${result.id}&testType=${result.type}`)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setDetailData(data.data)
|
||||||
|
} else {
|
||||||
|
console.error('獲取詳細結果失敗:', data.message)
|
||||||
|
alert('獲取詳細結果失敗,請稍後再試')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取詳細結果錯誤:', error)
|
||||||
|
alert('獲取詳細結果時發生錯誤,請稍後再試')
|
||||||
|
} finally {
|
||||||
|
setIsLoadingDetail(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -409,6 +437,7 @@ function AdminResultsContent() {
|
|||||||
<TableHead>分數</TableHead>
|
<TableHead>分數</TableHead>
|
||||||
<TableHead>等級</TableHead>
|
<TableHead>等級</TableHead>
|
||||||
<TableHead>完成時間</TableHead>
|
<TableHead>完成時間</TableHead>
|
||||||
|
<TableHead>操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -447,6 +476,17 @@ function AdminResultsContent() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="text-sm">{formatDate(result.completedAt)}</div>
|
<div className="text-sm">{formatDate(result.completedAt)}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleViewDetail(result)}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
查看詳情
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -599,6 +639,273 @@ function AdminResultsContent() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 詳細結果模態框 */}
|
||||||
|
<Dialog open={showDetailModal} onOpenChange={setShowDetailModal}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>測驗詳細結果</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{selectedResult && `${selectedResult.userName} - ${getTestTypeInfo(selectedResult.type).name}`}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{isLoadingDetail ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin mr-2" />
|
||||||
|
<span>載入詳細結果中...</span>
|
||||||
|
</div>
|
||||||
|
) : detailData ? (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 基本資訊 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>基本資訊</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">用戶姓名</label>
|
||||||
|
<p className="text-sm">{detailData.user.name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">用戶郵箱</label>
|
||||||
|
<p className="text-sm">{detailData.user.email}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">部門</label>
|
||||||
|
<p className="text-sm">{detailData.user.department}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">完成時間</label>
|
||||||
|
<p className="text-sm">{formatDate(detailData.result.completedAt)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">總分</label>
|
||||||
|
<p className="text-sm font-bold text-lg">{detailData.result.score}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">等級</label>
|
||||||
|
<Badge className={`${getScoreLevel(detailData.result.score).color} text-white`}>
|
||||||
|
{getScoreLevel(detailData.result.score).level}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 題目詳情 */}
|
||||||
|
{detailData.questions && detailData.questions.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>答題詳情</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 邏輯思維題目 */}
|
||||||
|
{detailData.questions.filter((q: any) => q.type === 'logic').length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Brain className="w-5 h-5 text-blue-600" />
|
||||||
|
邏輯思維題目
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{detailData.questions
|
||||||
|
.filter((q: any) => q.type === 'logic')
|
||||||
|
.map((question: any, index: number) => (
|
||||||
|
<div key={index} className="border rounded-lg p-4 bg-blue-50/30">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<h4 className="font-medium">第 {index + 1} 題</h4>
|
||||||
|
<Badge variant={question.isCorrect ? "default" : "destructive"}>
|
||||||
|
{question.isCorrect ? "正確" : "錯誤"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">題目內容</label>
|
||||||
|
<p className="text-sm mt-1">{question.question}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">選項</label>
|
||||||
|
<div className="space-y-1 mt-1">
|
||||||
|
{question.option_a && <p className="text-sm">A. {question.option_a}</p>}
|
||||||
|
{question.option_b && <p className="text-sm">B. {question.option_b}</p>}
|
||||||
|
{question.option_c && <p className="text-sm">C. {question.option_c}</p>}
|
||||||
|
{question.option_d && <p className="text-sm">D. {question.option_d}</p>}
|
||||||
|
{question.option_e && <p className="text-sm">E. {question.option_e}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">答案</label>
|
||||||
|
<div className="space-y-1 mt-1">
|
||||||
|
<p className="text-sm">用戶答案: <span className="font-bold">{question.userAnswer}</span></p>
|
||||||
|
<p className="text-sm">正確答案: <span className="font-bold text-green-600">{question.correctAnswer}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{question.explanation && (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">解釋</label>
|
||||||
|
<p className="text-sm mt-1">{question.explanation}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 創意能力題目 */}
|
||||||
|
{detailData.questions.filter((q: any) => q.type === 'creative').length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Lightbulb className="w-5 h-5 text-green-600" />
|
||||||
|
創意能力題目
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{detailData.questions
|
||||||
|
.filter((q: any) => q.type === 'creative')
|
||||||
|
.map((question: any, index: number) => (
|
||||||
|
<div key={index} className="border rounded-lg p-4 bg-green-50/30">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<h4 className="font-medium">第 {index + 1} 題</h4>
|
||||||
|
<Badge variant="outline" className="text-green-600 border-green-600">
|
||||||
|
{question.score} 分
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">題目內容</label>
|
||||||
|
<p className="text-sm mt-1">{question.statement}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">用戶答案</label>
|
||||||
|
<p className="text-sm mt-1">{question.userAnswer}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">得分</label>
|
||||||
|
<p className="text-sm mt-1 font-bold">{question.score} 分</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 單一測試類型的題目(邏輯或創意) */}
|
||||||
|
{detailData.result.type !== 'combined' && detailData.questions.map((question: any, index: number) => (
|
||||||
|
<div key={index} className="border rounded-lg p-4">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<h4 className="font-medium">第 {index + 1} 題</h4>
|
||||||
|
{question.isCorrect !== undefined && (
|
||||||
|
<Badge variant={question.isCorrect ? "default" : "destructive"}>
|
||||||
|
{question.isCorrect ? "正確" : "錯誤"}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{question.score !== undefined && (
|
||||||
|
<Badge variant="outline" className="text-green-600 border-green-600">
|
||||||
|
{question.score} 分
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">題目內容</label>
|
||||||
|
<p className="text-sm mt-1">{question.question || question.statement}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{question.type === 'logic' && (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">選項</label>
|
||||||
|
<div className="space-y-1 mt-1">
|
||||||
|
{question.option_a && <p className="text-sm">A. {question.option_a}</p>}
|
||||||
|
{question.option_b && <p className="text-sm">B. {question.option_b}</p>}
|
||||||
|
{question.option_c && <p className="text-sm">C. {question.option_c}</p>}
|
||||||
|
{question.option_d && <p className="text-sm">D. {question.option_d}</p>}
|
||||||
|
{question.option_e && <p className="text-sm">E. {question.option_e}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">答案</label>
|
||||||
|
<div className="space-y-1 mt-1">
|
||||||
|
<p className="text-sm">用戶答案: <span className="font-bold">{question.userAnswer}</span></p>
|
||||||
|
<p className="text-sm">正確答案: <span className="font-bold text-green-600">{question.correctAnswer}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{question.explanation && (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">解釋</label>
|
||||||
|
<p className="text-sm mt-1">{question.explanation}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{question.type === 'creative' && (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">用戶答案</label>
|
||||||
|
<p className="text-sm mt-1">{question.userAnswer}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">得分</label>
|
||||||
|
<p className="text-sm mt-1 font-bold">{question.score} 分</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 綜合測試詳細分析 */}
|
||||||
|
{detailData.result.type === 'combined' && detailData.result.details && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>綜合能力分析</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="text-center p-4 bg-blue-50 rounded-lg">
|
||||||
|
<h4 className="font-medium text-blue-800">邏輯思維</h4>
|
||||||
|
<p className="text-2xl font-bold text-blue-600">{detailData.result.details.logicScore}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-green-50 rounded-lg">
|
||||||
|
<h4 className="font-medium text-green-800">創意能力</h4>
|
||||||
|
<p className="text-2xl font-bold text-green-600">{detailData.result.details.creativeScore}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-purple-50 rounded-lg">
|
||||||
|
<h4 className="font-medium text-purple-800">能力平衡</h4>
|
||||||
|
<p className="text-2xl font-bold text-purple-600">{detailData.result.details.abilityBalance}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
無法載入詳細結果
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
157
app/api/admin/test-results/detail/route.ts
Normal file
157
app/api/admin/test-results/detail/route.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
import { getTestResultById } from "@/lib/database/models/test_result"
|
||||||
|
import { getLogicTestAnswersByTestResultId } from "@/lib/database/models/logic_test_answer"
|
||||||
|
import { getCreativeTestAnswersByTestResultId } from "@/lib/database/models/creative_test_answer"
|
||||||
|
import { getCombinedTestResultById } from "@/lib/database/models/combined_test_result"
|
||||||
|
import { findUserById } from "@/lib/database/models/user"
|
||||||
|
import { findLogicQuestionById } from "@/lib/database/models/logic_question"
|
||||||
|
import { findCreativeQuestionById } from "@/lib/database/models/creative_question"
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const testResultId = searchParams.get("testResultId")
|
||||||
|
const testType = searchParams.get("testType") as "logic" | "creative" | "combined"
|
||||||
|
|
||||||
|
if (!testResultId || !testType) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "缺少必要參數" },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: any = null
|
||||||
|
let user: any = null
|
||||||
|
let questions: any[] = []
|
||||||
|
let answers: any[] = []
|
||||||
|
|
||||||
|
// 獲取用戶資訊
|
||||||
|
if (testType === "combined") {
|
||||||
|
const combinedResult = await getCombinedTestResultById(testResultId)
|
||||||
|
if (combinedResult) {
|
||||||
|
user = await findUserById(combinedResult.user_id)
|
||||||
|
result = {
|
||||||
|
id: combinedResult.id,
|
||||||
|
userId: combinedResult.user_id,
|
||||||
|
type: "combined",
|
||||||
|
score: combinedResult.overall_score,
|
||||||
|
completedAt: combinedResult.completed_at,
|
||||||
|
details: {
|
||||||
|
logicScore: combinedResult.logic_score,
|
||||||
|
creativeScore: combinedResult.creativity_score,
|
||||||
|
abilityBalance: combinedResult.balance_score,
|
||||||
|
breakdown: combinedResult.logic_breakdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取綜合測試的詳細答題資料
|
||||||
|
// 從 logic_breakdown 中獲取邏輯題答案
|
||||||
|
if (combinedResult.logic_breakdown && typeof combinedResult.logic_breakdown === 'object') {
|
||||||
|
const logicBreakdown = combinedResult.logic_breakdown as any
|
||||||
|
if (logicBreakdown.answers && Array.isArray(logicBreakdown.answers)) {
|
||||||
|
for (const answer of logicBreakdown.answers) {
|
||||||
|
const question = await findLogicQuestionById(answer.questionId)
|
||||||
|
if (question) {
|
||||||
|
questions.push({
|
||||||
|
...question,
|
||||||
|
type: 'logic',
|
||||||
|
userAnswer: answer.userAnswer,
|
||||||
|
isCorrect: answer.isCorrect,
|
||||||
|
correctAnswer: answer.correctAnswer,
|
||||||
|
explanation: answer.explanation
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 從 creativity_breakdown 中獲取創意題答案
|
||||||
|
if (combinedResult.creativity_breakdown && typeof combinedResult.creativity_breakdown === 'object') {
|
||||||
|
const creativityBreakdown = combinedResult.creativity_breakdown as any
|
||||||
|
if (creativityBreakdown.answers && Array.isArray(creativityBreakdown.answers)) {
|
||||||
|
for (const answer of creativityBreakdown.answers) {
|
||||||
|
const question = await findCreativeQuestionById(answer.questionId)
|
||||||
|
if (question) {
|
||||||
|
questions.push({
|
||||||
|
...question,
|
||||||
|
type: 'creative',
|
||||||
|
userAnswer: answer.userAnswer,
|
||||||
|
score: answer.score,
|
||||||
|
isReverse: answer.isReverse
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const testResult = await getTestResultById(testResultId)
|
||||||
|
if (testResult) {
|
||||||
|
user = await findUserById(testResult.user_id)
|
||||||
|
result = {
|
||||||
|
id: testResult.id,
|
||||||
|
userId: testResult.user_id,
|
||||||
|
type: testResult.test_type,
|
||||||
|
score: testResult.score,
|
||||||
|
completedAt: testResult.completed_at
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取詳細答案
|
||||||
|
if (testType === "logic") {
|
||||||
|
answers = await getLogicTestAnswersByTestResultId(testResultId)
|
||||||
|
// 獲取對應的題目
|
||||||
|
for (const answer of answers) {
|
||||||
|
const question = await findLogicQuestionById(answer.question_id)
|
||||||
|
if (question) {
|
||||||
|
questions.push({
|
||||||
|
...question,
|
||||||
|
userAnswer: answer.user_answer,
|
||||||
|
isCorrect: answer.is_correct,
|
||||||
|
correctAnswer: answer.correct_answer,
|
||||||
|
explanation: answer.explanation
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (testType === "creative") {
|
||||||
|
answers = await getCreativeTestAnswersByTestResultId(testResultId)
|
||||||
|
// 獲取對應的題目
|
||||||
|
for (const answer of answers) {
|
||||||
|
const question = await findCreativeQuestionById(answer.question_id)
|
||||||
|
if (question) {
|
||||||
|
questions.push({
|
||||||
|
...question,
|
||||||
|
userAnswer: answer.user_answer,
|
||||||
|
score: answer.score,
|
||||||
|
isReverse: answer.is_reverse
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result || !user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "找不到測試結果" },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
result,
|
||||||
|
user,
|
||||||
|
questions,
|
||||||
|
answers
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("獲取詳細測試結果失敗:", error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "獲取詳細結果失敗", error: error instanceof Error ? error.message : String(error) },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
70
scripts/test-detail-api.js
Normal file
70
scripts/test-detail-api.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
async function testDetailAPI() {
|
||||||
|
console.log('🔍 測試詳細結果 API');
|
||||||
|
console.log('==============================');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先獲取測試結果列表
|
||||||
|
const listResponse = await fetch('http://localhost:3000/api/admin/test-results');
|
||||||
|
const listData = await listResponse.json();
|
||||||
|
|
||||||
|
if (listData.success && listData.data.results.length > 0) {
|
||||||
|
const firstResult = listData.data.results[0];
|
||||||
|
console.log(`📋 測試結果: ${firstResult.userName} - ${firstResult.type}`);
|
||||||
|
console.log(` 分數: ${firstResult.score}, 等級: ${firstResult.grade}`);
|
||||||
|
|
||||||
|
// 測試詳細結果 API
|
||||||
|
const detailResponse = await fetch(`http://localhost:3000/api/admin/test-results/detail?testResultId=${firstResult.id}&testType=${firstResult.type}`);
|
||||||
|
const detailData = await detailResponse.json();
|
||||||
|
|
||||||
|
if (detailData.success) {
|
||||||
|
console.log('✅ 詳細結果獲取成功');
|
||||||
|
console.log('📊 用戶資訊:', {
|
||||||
|
name: detailData.data.user.name,
|
||||||
|
email: detailData.data.user.email,
|
||||||
|
department: detailData.data.user.department
|
||||||
|
});
|
||||||
|
console.log('📈 測試結果:', {
|
||||||
|
type: detailData.data.result.type,
|
||||||
|
score: detailData.data.result.score,
|
||||||
|
completedAt: detailData.data.result.completedAt
|
||||||
|
});
|
||||||
|
console.log('📝 題目數量:', detailData.data.questions?.length || 0);
|
||||||
|
console.log('💡 答案數量:', detailData.data.answers?.length || 0);
|
||||||
|
|
||||||
|
if (detailData.data.questions && detailData.data.questions.length > 0) {
|
||||||
|
console.log('\n📋 第一題詳情:');
|
||||||
|
const firstQuestion = detailData.data.questions[0];
|
||||||
|
console.log(' 題目:', firstQuestion.question || firstQuestion.statement);
|
||||||
|
console.log(' 用戶答案:', firstQuestion.userAnswer);
|
||||||
|
if (firstQuestion.isCorrect !== undefined) {
|
||||||
|
console.log(' 是否正確:', firstQuestion.isCorrect ? '是' : '否');
|
||||||
|
}
|
||||||
|
if (firstQuestion.score !== undefined) {
|
||||||
|
console.log(' 得分:', firstQuestion.score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detailData.data.result.type === 'combined' && detailData.data.result.details) {
|
||||||
|
console.log('\n🎯 綜合能力分析:');
|
||||||
|
console.log(' 邏輯思維:', detailData.data.result.details.logicScore);
|
||||||
|
console.log(' 創意能力:', detailData.data.result.details.creativeScore);
|
||||||
|
console.log(' 能力平衡:', detailData.data.result.details.abilityBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error('❌ 詳細結果獲取失敗:', detailData.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 沒有找到測試結果');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 測試錯誤:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('==============================\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
testDetailAPI();
|
113
scripts/test-detailed-answers.js
Normal file
113
scripts/test-detailed-answers.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
async function testDetailedAnswers() {
|
||||||
|
console.log('🔍 測試詳細答題結果功能');
|
||||||
|
console.log('==============================');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先獲取測試結果列表
|
||||||
|
const listResponse = await fetch('http://localhost:3000/api/admin/test-results');
|
||||||
|
const listData = await listResponse.json();
|
||||||
|
|
||||||
|
if (listData.success && listData.data.results.length > 0) {
|
||||||
|
// 測試綜合能力測試的詳細結果
|
||||||
|
const combinedResult = listData.data.results.find(r => r.type === 'combined');
|
||||||
|
if (combinedResult) {
|
||||||
|
console.log(`📋 測試綜合能力結果: ${combinedResult.userName}`);
|
||||||
|
console.log(` 分數: ${combinedResult.score}, 等級: ${combinedResult.grade}`);
|
||||||
|
|
||||||
|
const detailResponse = await fetch(`http://localhost:3000/api/admin/test-results/detail?testResultId=${combinedResult.id}&testType=combined`);
|
||||||
|
const detailData = await detailResponse.json();
|
||||||
|
|
||||||
|
if (detailData.success) {
|
||||||
|
console.log('✅ 綜合能力詳細結果獲取成功');
|
||||||
|
console.log('📊 用戶資訊:', {
|
||||||
|
name: detailData.data.user.name,
|
||||||
|
email: detailData.data.user.email,
|
||||||
|
department: detailData.data.user.department
|
||||||
|
});
|
||||||
|
console.log('📈 測試結果:', {
|
||||||
|
type: detailData.data.result.type,
|
||||||
|
score: detailData.data.result.score,
|
||||||
|
completedAt: detailData.data.result.completedAt
|
||||||
|
});
|
||||||
|
console.log('🎯 能力分析:', {
|
||||||
|
logicScore: detailData.data.result.details.logicScore,
|
||||||
|
creativeScore: detailData.data.result.details.creativeScore,
|
||||||
|
abilityBalance: detailData.data.result.details.abilityBalance
|
||||||
|
});
|
||||||
|
console.log('📝 題目總數:', detailData.data.questions?.length || 0);
|
||||||
|
|
||||||
|
if (detailData.data.questions && detailData.data.questions.length > 0) {
|
||||||
|
const logicQuestions = detailData.data.questions.filter(q => q.type === 'logic');
|
||||||
|
const creativeQuestions = detailData.data.questions.filter(q => q.type === 'creative');
|
||||||
|
|
||||||
|
console.log(`\n🧠 邏輯思維題目: ${logicQuestions.length} 題`);
|
||||||
|
console.log(`💡 創意能力題目: ${creativeQuestions.length} 題`);
|
||||||
|
|
||||||
|
if (logicQuestions.length > 0) {
|
||||||
|
console.log('\n📋 第一題邏輯題詳情:');
|
||||||
|
const firstLogic = logicQuestions[0];
|
||||||
|
console.log(' 題目:', firstLogic.question);
|
||||||
|
console.log(' 用戶答案:', firstLogic.userAnswer);
|
||||||
|
console.log(' 正確答案:', firstLogic.correctAnswer);
|
||||||
|
console.log(' 是否正確:', firstLogic.isCorrect ? '是' : '否');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creativeQuestions.length > 0) {
|
||||||
|
console.log('\n📋 第一題創意題詳情:');
|
||||||
|
const firstCreative = creativeQuestions[0];
|
||||||
|
console.log(' 題目:', firstCreative.statement);
|
||||||
|
console.log(' 用戶答案:', firstCreative.userAnswer);
|
||||||
|
console.log(' 得分:', firstCreative.score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error('❌ 綜合能力詳細結果獲取失敗:', detailData.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 沒有找到綜合能力測試結果');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 測試單一測試類型的詳細結果
|
||||||
|
const singleResult = listData.data.results.find(r => r.type !== 'combined');
|
||||||
|
if (singleResult) {
|
||||||
|
console.log(`\n📋 測試單一類型結果: ${singleResult.userName} - ${singleResult.type}`);
|
||||||
|
|
||||||
|
const detailResponse = await fetch(`http://localhost:3000/api/admin/test-results/detail?testResultId=${singleResult.id}&testType=${singleResult.type}`);
|
||||||
|
const detailData = await detailResponse.json();
|
||||||
|
|
||||||
|
if (detailData.success) {
|
||||||
|
console.log('✅ 單一類型詳細結果獲取成功');
|
||||||
|
console.log('📝 題目數量:', detailData.data.questions?.length || 0);
|
||||||
|
|
||||||
|
if (detailData.data.questions && detailData.data.questions.length > 0) {
|
||||||
|
console.log('\n📋 第一題詳情:');
|
||||||
|
const firstQuestion = detailData.data.questions[0];
|
||||||
|
console.log(' 題目:', firstQuestion.question || firstQuestion.statement);
|
||||||
|
console.log(' 用戶答案:', firstQuestion.userAnswer);
|
||||||
|
if (firstQuestion.isCorrect !== undefined) {
|
||||||
|
console.log(' 是否正確:', firstQuestion.isCorrect ? '是' : '否');
|
||||||
|
}
|
||||||
|
if (firstQuestion.score !== undefined) {
|
||||||
|
console.log(' 得分:', firstQuestion.score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('❌ 單一類型詳細結果獲取失敗:', detailData.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 沒有找到測試結果');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 測試錯誤:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('==============================\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
testDetailedAnswers();
|
Reference in New Issue
Block a user