From afc75802592d309c1cf7cff05d64094b404660d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B3=E4=BD=A9=E5=BA=AD?= Date: Mon, 29 Sep 2025 19:45:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=A6=E4=BD=9C=E6=89=80=E6=9C=89=E6=B8=AC?= =?UTF-8?q?=E9=A9=97=E7=B5=90=E6=9E=9C=E8=88=87=E8=B3=87=E6=96=99=E5=BA=AB?= =?UTF-8?q?=E6=95=B4=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/results/page.tsx | 501 ++++++++++++++------- app/api/admin/test-results/export/route.ts | 221 +++++++++ app/api/admin/test-results/route.ts | 173 +++++++ scripts/check-combined-table-fields.js | 38 ++ scripts/check-combined-table-structure.js | 38 ++ scripts/check-combined-test-data.js | 52 +++ scripts/check-db-fields.js | 41 ++ scripts/test-admin-results.js | 170 +++++++ scripts/test-export-details.js | 89 ++++ scripts/test-export-results.js | 154 +++++++ scripts/test-export-simple.js | 84 ++-- scripts/test-fixed-export.js | 106 +++++ 12 files changed, 1460 insertions(+), 207 deletions(-) create mode 100644 app/api/admin/test-results/export/route.ts create mode 100644 app/api/admin/test-results/route.ts create mode 100644 scripts/check-combined-table-fields.js create mode 100644 scripts/check-combined-table-structure.js create mode 100644 scripts/check-combined-test-data.js create mode 100644 scripts/check-db-fields.js create mode 100644 scripts/test-admin-results.js create mode 100644 scripts/test-export-details.js create mode 100644 scripts/test-export-results.js create mode 100644 scripts/test-fixed-export.js diff --git a/app/admin/results/page.tsx b/app/admin/results/page.tsx index 347bce9..68a2435 100644 --- a/app/admin/results/page.tsx +++ b/app/admin/results/page.tsx @@ -8,20 +8,45 @@ import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Badge } from "@/components/ui/badge" -import { Brain, Lightbulb, BarChart3, ArrowLeft, Search, Download, Filter } from "lucide-react" +import { Brain, Lightbulb, BarChart3, ArrowLeft, Search, Download, Filter, ChevronLeft, ChevronRight, Loader2 } from "lucide-react" import Link from "next/link" -import { useAuth, type User } from "@/lib/hooks/use-auth" +import { useAuth } from "@/lib/hooks/use-auth" interface TestResult { + id: string userId: string userName: string userDepartment: string + userEmail: string type: "logic" | "creative" | "combined" score: number completedAt: string details?: any } +interface AdminTestResultsStats { + totalResults: number + filteredResults: number + averageScore: number + totalUsers: number + usersWithResults: number + participationRate: number + testTypeCounts: { + logic: number + creative: number + combined: number + } +} + +interface PaginationInfo { + currentPage: number + totalPages: number + totalResults: number + limit: number + hasNextPage: boolean + hasPrevPage: boolean +} + export default function AdminResultsPage() { return ( @@ -33,125 +58,99 @@ export default function AdminResultsPage() { function AdminResultsContent() { const { user } = useAuth() const [results, setResults] = useState([]) - const [filteredResults, setFilteredResults] = useState([]) - const [users, setUsers] = useState([]) + const [stats, setStats] = useState({ + totalResults: 0, + filteredResults: 0, + averageScore: 0, + totalUsers: 0, + usersWithResults: 0, + participationRate: 0, + testTypeCounts: { logic: 0, creative: 0, combined: 0 } + }) + const [pagination, setPagination] = useState({ + currentPage: 1, + totalPages: 1, + totalResults: 0, + limit: 10, + hasNextPage: false, + hasPrevPage: false + }) + const [departments, setDepartments] = useState([]) const [searchTerm, setSearchTerm] = useState("") const [departmentFilter, setDepartmentFilter] = useState("all") const [testTypeFilter, setTestTypeFilter] = useState("all") - const [stats, setStats] = useState({ - totalResults: 0, - averageScore: 0, - totalUsers: 0, - completionRate: 0, - }) - - const departments = ["人力資源部", "資訊技術部", "財務部", "行銷部", "業務部", "研發部", "客服部", "其他"] + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) useEffect(() => { loadData() }, []) useEffect(() => { - filterResults() - }, [results, searchTerm, departmentFilter, testTypeFilter]) + loadData() + }, [searchTerm, departmentFilter, testTypeFilter, pagination.currentPage]) - const loadData = () => { - // Load users - const usersData = JSON.parse(localStorage.getItem("hr_users") || "[]") - setUsers(usersData) + const loadData = async () => { + setIsLoading(true) + setError(null) - // Load all test results - const allResults: TestResult[] = [] + try { + const params = new URLSearchParams({ + search: searchTerm, + department: departmentFilter, + testType: testTypeFilter, + page: pagination.currentPage.toString(), + limit: pagination.limit.toString() + }) - usersData.forEach((user: User) => { - // Check for logic test results - const logicKey = `logicTestResults_${user.id}` - const logicResults = localStorage.getItem(logicKey) - if (logicResults) { - const data = JSON.parse(logicResults) - allResults.push({ - userId: user.id, - userName: user.name, - userDepartment: user.department, - type: "logic", - score: data.score, - completedAt: data.completedAt, - details: data, - }) + const response = await fetch(`/api/admin/test-results?${params}`) + const data = await response.json() + + if (data.success) { + setResults(data.data.results) + setStats(data.data.stats) + setPagination(data.data.pagination) + setDepartments(data.data.departments) + } else { + setError(data.message || "載入資料失敗") } - - // Check for creative test results - const creativeKey = `creativeTestResults_${user.id}` - const creativeResults = localStorage.getItem(creativeKey) - if (creativeResults) { - const data = JSON.parse(creativeResults) - allResults.push({ - userId: user.id, - userName: user.name, - userDepartment: user.department, - type: "creative", - score: data.score, - completedAt: data.completedAt, - details: data, - }) - } - - // Check for combined test results - const combinedKey = `combinedTestResults_${user.id}` - const combinedResults = localStorage.getItem(combinedKey) - if (combinedResults) { - const data = JSON.parse(combinedResults) - allResults.push({ - userId: user.id, - userName: user.name, - userDepartment: user.department, - type: "combined", - score: data.overallScore, - completedAt: data.completedAt, - details: data, - }) - } - }) - - // Sort by completion date (newest first) - allResults.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime()) - setResults(allResults) - - // Calculate statistics - const totalResults = allResults.length - const averageScore = - totalResults > 0 ? Math.round(allResults.reduce((sum, r) => sum + r.score, 0) / totalResults) : 0 - const totalUsers = usersData.length - const usersWithResults = new Set(allResults.map((r) => r.userId)).size - const completionRate = totalUsers > 0 ? Math.round((usersWithResults / totalUsers) * 100) : 0 - - setStats({ - totalResults, - averageScore, - totalUsers, - completionRate, - }) + } catch (error) { + console.error("載入測驗結果失敗:", error) + setError("載入資料時發生錯誤") + } finally { + setIsLoading(false) + } } - const filterResults = () => { - let filtered = results + const handleSearch = (value: string) => { + setSearchTerm(value) + setPagination(prev => ({ ...prev, currentPage: 1 })) + } - // Filter by search term (user name) - if (searchTerm) { - filtered = filtered.filter((result) => result.userName.toLowerCase().includes(searchTerm.toLowerCase())) + const handleDepartmentChange = (value: string) => { + setDepartmentFilter(value) + setPagination(prev => ({ ...prev, currentPage: 1 })) + } + + const handleTestTypeChange = (value: string) => { + setTestTypeFilter(value) + setPagination(prev => ({ ...prev, currentPage: 1 })) + } + + const handlePageChange = (page: number) => { + setPagination(prev => ({ ...prev, currentPage: page })) + } + + const handlePreviousPage = () => { + if (pagination.hasPrevPage) { + handlePageChange(pagination.currentPage - 1) } + } - // Filter by department - if (departmentFilter !== "all") { - filtered = filtered.filter((result) => result.userDepartment === departmentFilter) + const handleNextPage = () => { + if (pagination.hasNextPage) { + handlePageChange(pagination.currentPage + 1) } - - // Filter by test type - if (testTypeFilter !== "all") { - filtered = filtered.filter((result) => result.type === testTypeFilter) - } - - setFilteredResults(filtered) } const getTestTypeInfo = (type: string) => { @@ -192,33 +191,59 @@ function AdminResultsContent() { if (score >= 80) return { level: "良好", color: "bg-blue-500" } if (score >= 70) return { level: "中等", color: "bg-yellow-500" } if (score >= 60) return { level: "及格", color: "bg-orange-500" } - return { level: "不及格", color: "bg-red-500" } + return { level: "待加強", color: "bg-red-500" } } - const exportResults = () => { - const csvContent = [ - ["姓名", "部門", "測試類型", "分數", "等級", "完成時間"], - ...filteredResults.map((result) => [ - result.userName, - result.userDepartment, - getTestTypeInfo(result.type).name, - result.score.toString(), - getScoreLevel(result.score).level, - new Date(result.completedAt).toLocaleString("zh-TW"), - ]), - ] - .map((row) => row.join(",")) - .join("\n") + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString("zh-TW", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit" + }) + } - const blob = new Blob(["\uFEFF" + csvContent], { type: "text/csv;charset=utf-8;" }) - const link = document.createElement("a") - const url = URL.createObjectURL(blob) - link.setAttribute("href", url) - link.setAttribute("download", `測試結果_${new Date().toISOString().split("T")[0]}.csv`) - link.style.visibility = "hidden" + const handleExport = async () => { + try { + const params = new URLSearchParams({ + search: searchTerm, + department: departmentFilter, + testType: testTypeFilter + }) + + const response = await fetch(`/api/admin/test-results/export?${params}`) + const data = await response.json() + + if (data.success) { + // 解碼 Base64 資料,保留 UTF-8 BOM + const binaryString = atob(data.data) + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + + // 創建 Blob,保留原始字節資料 + const blob = new Blob([bytes], { + type: 'text/csv;charset=utf-8' + }) + + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = data.filename document.body.appendChild(link) link.click() document.body.removeChild(link) + window.URL.revokeObjectURL(url) + } else { + console.error('匯出失敗:', data.message) + alert('匯出失敗,請稍後再試') + } + } catch (error) { + console.error('匯出錯誤:', error) + alert('匯出時發生錯誤,請稍後再試') + } } return ( @@ -243,7 +268,7 @@ function AdminResultsContent() {
- {/* Statistics Overview */} + {/* Summary Cards */}
@@ -277,16 +302,16 @@ function AdminResultsContent() { -
- +
+
-
{stats.completionRate}%
+
{stats.participationRate}%
參與率
- {/* Filters */} + {/* Filter Section */} @@ -296,24 +321,24 @@ function AdminResultsContent() {
-
- +
+
- + setSearchTerm(e.target.value)} + onChange={(e) => handleSearch(e.target.value)} className="pl-10" />
-
- - - + 所有部門 @@ -326,11 +351,11 @@ function AdminResultsContent() {
-
- - - + 所有類型 @@ -341,9 +366,8 @@ function AdminResultsContent() {
-
- - @@ -352,15 +376,30 @@ function AdminResultsContent() { - {/* Results Table */} + {/* Test Results List */} 測試結果列表 - 顯示 {filteredResults.length} 筆結果(共 {results.length} 筆) + 顯示 {pagination.totalResults} 筆結果 (共 {stats.totalResults} 筆) + {isLoading ? ( +
+ + 載入中... +
+ ) : error ? ( +
+ {error} +
+ ) : results.length === 0 ? ( +
+ 沒有找到符合條件的測試結果 +
+ ) : ( + <> @@ -373,33 +412,40 @@ function AdminResultsContent() { - {filteredResults.map((result, index) => { - const testInfo = getTestTypeInfo(result.type) + {results.map((result) => { + const testTypeInfo = getTestTypeInfo(result.type) const scoreLevel = getScoreLevel(result.score) - const Icon = testInfo.icon + const IconComponent = testTypeInfo.icon return ( - + +
{result.userName}
+
{result.userEmail}
+
+
+ + {result.userDepartment} - {result.userDepartment}
-
- +
+
- {testInfo.name} + {testTypeInfo.name}
{result.score}
- {scoreLevel.level} + + {scoreLevel.level} + -
{new Date(result.completedAt).toLocaleString("zh-TW")}
+
{formatDate(result.completedAt)}
) @@ -407,10 +453,147 @@ function AdminResultsContent() {
- {filteredResults.length === 0 && ( -
-
沒有找到符合條件的測試結果
+ {/* Pagination */} + {pagination.totalPages > 1 && ( +
+
+ 顯示第 {(pagination.currentPage - 1) * pagination.limit + 1} - {Math.min(pagination.currentPage * pagination.limit, pagination.totalResults)} 筆,共 {pagination.totalResults} 筆 +
+ + {/* Desktop Pagination */} +
+ + +
+ {Array.from({ length: pagination.totalPages }, (_, i) => i + 1).map((page) => ( + + ))} +
+ + +
+ + {/* Mobile Pagination */} +
+ + +
+ {(() => { + const maxVisiblePages = 3 + const startPage = Math.max(1, pagination.currentPage - 1) + const endPage = Math.min(pagination.totalPages, startPage + maxVisiblePages - 1) + const pages = [] + + // 如果不在第一頁,顯示第一頁和省略號 + if (startPage > 1) { + pages.push( + + ) + if (startPage > 2) { + pages.push( + + ... + + ) + } + } + + // 顯示當前頁附近的頁碼 + for (let i = startPage; i <= endPage; i++) { + pages.push( + + ) + } + + // 如果不在最後一頁,顯示省略號和最後一頁 + if (endPage < pagination.totalPages) { + if (endPage < pagination.totalPages - 1) { + pages.push( + + ... + + ) + } + pages.push( + + ) + } + + return pages + })()} +
+ + +
+ )} + )} @@ -418,4 +601,4 @@ function AdminResultsContent() {
) -} +} \ No newline at end of file diff --git a/app/api/admin/test-results/export/route.ts b/app/api/admin/test-results/export/route.ts new file mode 100644 index 0000000..718f716 --- /dev/null +++ b/app/api/admin/test-results/export/route.ts @@ -0,0 +1,221 @@ +import { NextRequest, NextResponse } from "next/server" +import { getAllTestResults } 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 { getAllCombinedTestResults } from "@/lib/database/models/combined_test_result" +import { getAllUsers } from "@/lib/database/models/user" + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const search = searchParams.get("search") || "" + const department = searchParams.get("department") || "all" + const testType = searchParams.get("testType") || "all" + + // 獲取所有用戶 + const users = await getAllUsers() + const userMap = new Map(users.map(user => [user.id, user])) + + // 獲取所有測試結果 + const [testResults, combinedResults] = await Promise.all([ + getAllTestResults(), + getAllCombinedTestResults() + ]) + + // 合併所有測試結果 + const allResults = [] + + // 處理邏輯和創意測試結果 + for (const result of testResults) { + const user = userMap.get(result.user_id) + if (user) { + let details = null + + // 根據測試類型獲取詳細資料 + if (result.test_type === 'logic') { + const logicAnswers = await getLogicTestAnswersByTestResultId(result.id) + if (logicAnswers.length > 0) { + const logicAnswer = logicAnswers[0] + details = { + correctAnswers: logicAnswer.correct_answers, + totalQuestions: logicAnswer.total_questions, + accuracy: logicAnswer.accuracy + } + } + } else if (result.test_type === 'creative') { + const creativeAnswers = await getCreativeTestAnswersByTestResultId(result.id) + if (creativeAnswers.length > 0) { + const creativeAnswer = creativeAnswers[0] + details = { + dimensionScores: creativeAnswer.dimension_scores, + totalScore: creativeAnswer.total_score, + maxScore: creativeAnswer.max_score + } + } + } + + allResults.push({ + id: result.id, + userId: result.user_id, + userName: user.name, + userDepartment: user.department, + userEmail: user.email, + type: result.test_type as 'logic' | 'creative', + score: result.score, + completedAt: result.completed_at, + details + }) + } + } + + // 處理綜合測試結果 + for (const result of combinedResults) { + const user = userMap.get(result.user_id) + if (user) { + allResults.push({ + id: result.id, + userId: result.user_id, + userName: user.name, + userDepartment: user.department, + userEmail: user.email, + type: 'combined' as const, + score: result.overall_score, + completedAt: result.completed_at, + details: { + logicScore: result.logic_score, + creativeScore: result.creativity_score, // 使用 creativity_score + abilityBalance: result.balance_score, // 使用 balance_score + breakdown: result.logic_breakdown + } + }) + } + } + + // 按完成時間排序(最新的在前) + allResults.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime()) + + // 應用篩選 + let filteredResults = allResults + + // 搜尋篩選 + if (search) { + filteredResults = filteredResults.filter(result => + result.userName.toLowerCase().includes(search.toLowerCase()) || + result.userEmail.toLowerCase().includes(search.toLowerCase()) + ) + } + + // 部門篩選 + if (department !== "all") { + filteredResults = filteredResults.filter(result => result.userDepartment === department) + } + + // 測試類型篩選 + if (testType !== "all") { + filteredResults = filteredResults.filter(result => result.type === testType) + } + + // 生成 CSV 格式的資料 + const headers = [ + "用戶姓名", + "用戶郵箱", + "部門", + "測試類型", + "分數", + "等級", + "完成時間", + "詳細資料" + ] + + const data = filteredResults.map(result => { + const getScoreLevel = (score: number) => { + if (score >= 90) return "優秀" + if (score >= 80) return "良好" + if (score >= 70) return "中等" + if (score >= 60) return "及格" + return "待加強" + } + + const getTestTypeName = (type: string) => { + switch (type) { + case "logic": return "邏輯思維" + case "creative": return "創意能力" + case "combined": return "綜合能力" + default: return "未知" + } + } + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString("zh-TW", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit" + }) + } + + let detailsStr = "" + if (result.details) { + if (result.type === 'logic' && result.details.correctAnswers !== undefined) { + detailsStr = `正確答案: ${result.details.correctAnswers}/${result.details.totalQuestions}, 準確率: ${result.details.accuracy}%` + } else if (result.type === 'creative' && result.details.dimensionScores) { + detailsStr = `總分: ${result.details.totalScore}/${result.details.maxScore}, 維度分數: ${JSON.stringify(result.details.dimensionScores)}` + } else if (result.type === 'combined' && result.details.logicScore !== undefined) { + const logicScore = result.details.logicScore ?? '無資料' + const creativeScore = result.details.creativeScore ?? '無資料' + const abilityBalance = result.details.abilityBalance ?? '無資料' + detailsStr = `邏輯: ${logicScore}, 創意: ${creativeScore}, 平衡: ${abilityBalance}` + } + } + + return [ + result.userName, + result.userEmail, + result.userDepartment, + getTestTypeName(result.type), + result.score, + getScoreLevel(result.score), + formatDate(result.completedAt), + detailsStr + ] + }) + + // 轉換為 CSV 格式 + const csvRows = [headers, ...data].map(row => + row.map(cell => { + const escaped = String(cell).replace(/"/g, '""') + return `"${escaped}"` + }).join(",") + ) + + const csvContent = csvRows.join("\n") + + // 直接使用 UTF-8 BOM 字節 + const bomBytes = new Uint8Array([0xEF, 0xBB, 0xBF]) // UTF-8 BOM + const contentBytes = new TextEncoder().encode(csvContent) + const result = new Uint8Array(bomBytes.length + contentBytes.length) + result.set(bomBytes) + result.set(contentBytes, bomBytes.length) + + const base64Content = Buffer.from(result).toString('base64') + + return new NextResponse(JSON.stringify({ + success: true, + data: base64Content, + filename: `測驗結果_${new Date().toISOString().split('T')[0]}.csv`, + contentType: "text/csv; charset=utf-8" + }), { + headers: { + "Content-Type": "application/json" + } + }) + + } catch (error) { + console.error("匯出測驗結果失敗:", error) + return NextResponse.json( + { success: false, message: "匯出失敗", error: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ) + } +} diff --git a/app/api/admin/test-results/route.ts b/app/api/admin/test-results/route.ts new file mode 100644 index 0000000..e39037d --- /dev/null +++ b/app/api/admin/test-results/route.ts @@ -0,0 +1,173 @@ +import { NextRequest, NextResponse } from "next/server" +import { getAllTestResults } 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 { getAllCombinedTestResults } from "@/lib/database/models/combined_test_result" +import { getAllUsers } from "@/lib/database/models/user" + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const search = searchParams.get("search") || "" + const department = searchParams.get("department") || "all" + const testType = searchParams.get("testType") || "all" + const page = parseInt(searchParams.get("page") || "1") + const limit = parseInt(searchParams.get("limit") || "10") + + // 獲取所有用戶 + const users = await getAllUsers() + const userMap = new Map(users.map(user => [user.id, user])) + + // 獲取所有測試結果 + const [testResults, combinedResults] = await Promise.all([ + getAllTestResults(), + getAllCombinedTestResults() + ]) + + // 合併所有測試結果 + const allResults = [] + + // 處理邏輯和創意測試結果 + for (const result of testResults) { + const user = userMap.get(result.user_id) + if (user) { + let details = null + + // 根據測試類型獲取詳細資料 + if (result.test_type === 'logic') { + const logicAnswers = await getLogicTestAnswersByTestResultId(result.id) + if (logicAnswers.length > 0) { + const logicAnswer = logicAnswers[0] // 取第一個答案記錄 + details = { + correctAnswers: logicAnswer.correct_answers, + totalQuestions: logicAnswer.total_questions, + accuracy: logicAnswer.accuracy + } + } + } else if (result.test_type === 'creative') { + const creativeAnswers = await getCreativeTestAnswersByTestResultId(result.id) + if (creativeAnswers.length > 0) { + const creativeAnswer = creativeAnswers[0] // 取第一個答案記錄 + details = { + dimensionScores: creativeAnswer.dimension_scores, + totalScore: creativeAnswer.total_score, + maxScore: creativeAnswer.max_score + } + } + } + + allResults.push({ + id: result.id, + userId: result.user_id, + userName: user.name, + userDepartment: user.department, + userEmail: user.email, + type: result.test_type as 'logic' | 'creative', + score: result.score, + completedAt: result.completed_at, + details + }) + } + } + + // 處理綜合測試結果 + for (const result of combinedResults) { + const user = userMap.get(result.user_id) + if (user) { + allResults.push({ + id: result.id, + userId: result.user_id, + userName: user.name, + userDepartment: user.department, + userEmail: user.email, + type: 'combined' as const, + score: result.overall_score, + completedAt: result.completed_at, + details: { + logicScore: result.logic_score, + creativeScore: result.creativity_score, // 使用 creativity_score + abilityBalance: result.balance_score, // 使用 balance_score + breakdown: result.logic_breakdown + } + }) + } + } + + // 按完成時間排序(最新的在前) + allResults.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime()) + + // 應用篩選 + let filteredResults = allResults + + // 搜尋篩選 + if (search) { + filteredResults = filteredResults.filter(result => + result.userName.toLowerCase().includes(search.toLowerCase()) || + result.userEmail.toLowerCase().includes(search.toLowerCase()) + ) + } + + // 部門篩選 + if (department !== "all") { + filteredResults = filteredResults.filter(result => result.userDepartment === department) + } + + // 測試類型篩選 + if (testType !== "all") { + filteredResults = filteredResults.filter(result => result.type === testType) + } + + // 分頁 + const totalResults = filteredResults.length + const totalPages = Math.ceil(totalResults / limit) + const startIndex = (page - 1) * limit + const endIndex = startIndex + limit + const paginatedResults = filteredResults.slice(startIndex, endIndex) + + // 計算統計資料 + const stats = { + totalResults: allResults.length, + filteredResults: filteredResults.length, + averageScore: allResults.length > 0 + ? Math.round(allResults.reduce((sum, r) => sum + r.score, 0) / allResults.length) + : 0, + totalUsers: users.length, + usersWithResults: new Set(allResults.map(r => r.userId)).size, + participationRate: users.length > 0 + ? Math.round((new Set(allResults.map(r => r.userId)).size / users.length) * 100) + : 0, + testTypeCounts: { + logic: allResults.filter(r => r.type === 'logic').length, + creative: allResults.filter(r => r.type === 'creative').length, + combined: allResults.filter(r => r.type === 'combined').length + } + } + + // 獲取所有部門列表 + const departments = Array.from(new Set(users.map(user => user.department))).sort() + + return NextResponse.json({ + success: true, + data: { + results: paginatedResults, + stats, + pagination: { + currentPage: page, + totalPages, + totalResults, + limit, + hasNextPage: page < totalPages, + hasPrevPage: page > 1 + }, + departments + } + }) + + } catch (error) { + console.error("獲取管理員測驗結果失敗:", error) + return NextResponse.json( + { success: false, message: "獲取測驗結果失敗", error: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ) + } +} diff --git a/scripts/check-combined-table-fields.js b/scripts/check-combined-table-fields.js new file mode 100644 index 0000000..cdad775 --- /dev/null +++ b/scripts/check-combined-table-fields.js @@ -0,0 +1,38 @@ +const { executeQuery } = require('../lib/database/connection') + +const checkCombinedTableFields = async () => { + console.log('🔍 檢查 combined_test_results 表的實際欄位') + console.log('=' .repeat(50)) + + try { + // 檢查表結構 + console.log('\n📊 檢查表結構...') + const structureQuery = 'DESCRIBE combined_test_results' + const structure = await executeQuery(structureQuery) + + console.log('📋 表欄位:') + structure.forEach(field => { + console.log(` ${field.Field}: ${field.Type} ${field.Null === 'YES' ? '(可為空)' : '(不可為空)'}`) + }) + + // 檢查實際資料 + console.log('\n📊 檢查實際資料...') + const dataQuery = 'SELECT * FROM combined_test_results LIMIT 2' + const data = await executeQuery(dataQuery) + + console.log('📋 實際資料:') + data.forEach((row, index) => { + console.log(`\n 記錄 ${index + 1}:`) + Object.keys(row).forEach(key => { + console.log(` ${key}: ${row[key]}`) + }) + }) + + } catch (error) { + console.error('❌ 檢查失敗:', error.message) + } finally { + console.log('\n✅ 資料庫欄位檢查完成') + } +} + +checkCombinedTableFields() diff --git a/scripts/check-combined-table-structure.js b/scripts/check-combined-table-structure.js new file mode 100644 index 0000000..9404a3a --- /dev/null +++ b/scripts/check-combined-table-structure.js @@ -0,0 +1,38 @@ +const { executeQuery } = require('../lib/database/connection') + +const checkCombinedTableStructure = async () => { + console.log('🔍 檢查綜合測試結果表結構') + console.log('=' .repeat(40)) + + try { + // 檢查表結構 + console.log('\n📊 檢查表結構...') + const structureQuery = 'DESCRIBE combined_test_results' + const structure = await executeQuery(structureQuery) + + console.log('📋 表欄位:') + structure.forEach(field => { + console.log(` ${field.Field}: ${field.Type} ${field.Null === 'YES' ? '(可為空)' : '(不可為空)'}`) + }) + + // 檢查實際資料 + console.log('\n📊 檢查實際資料...') + const dataQuery = 'SELECT * FROM combined_test_results LIMIT 3' + const data = await executeQuery(dataQuery) + + console.log('📋 實際資料範例:') + data.forEach((row, index) => { + console.log(`\n 記錄 ${index + 1}:`) + Object.keys(row).forEach(key => { + console.log(` ${key}: ${row[key]}`) + }) + }) + + } catch (error) { + console.error('❌ 檢查失敗:', error.message) + } finally { + console.log('\n✅ 綜合測試結果表結構檢查完成') + } +} + +checkCombinedTableStructure() diff --git a/scripts/check-combined-test-data.js b/scripts/check-combined-test-data.js new file mode 100644 index 0000000..d989a94 --- /dev/null +++ b/scripts/check-combined-test-data.js @@ -0,0 +1,52 @@ +const http = require('http') + +const checkCombinedTestData = async () => { + console.log('🔍 檢查綜合測試資料結構') + console.log('=' .repeat(40)) + + try { + // 獲取所有測試結果 + const response = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results', (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ + status: res.statusCode, + data: data + })) + }) + req.on('error', reject) + }) + + if (response.status === 200) { + const data = JSON.parse(response.data) + if (data.success) { + console.log('✅ 成功獲取測試結果') + + // 找出綜合測試結果 + const combinedResults = data.data.results.filter(result => result.type === 'combined') + console.log(`📊 綜合測試結果數量: ${combinedResults.length}`) + + combinedResults.forEach((result, index) => { + console.log(`\n📋 綜合測試 ${index + 1}:`) + console.log(` 用戶: ${result.userName}`) + console.log(` 分數: ${result.score}`) + console.log(` 完成時間: ${result.completedAt}`) + console.log(` 詳細資料:`, JSON.stringify(result.details, null, 2)) + }) + + } else { + console.log('❌ 獲取資料失敗:', data.message) + } + } else { + console.log('❌ 獲取資料失敗,狀態碼:', response.status) + } + + } catch (error) { + console.error('❌ 檢查失敗:', error.message) + } finally { + console.log('\n✅ 綜合測試資料檢查完成') + } +} + +checkCombinedTestData() diff --git a/scripts/check-db-fields.js b/scripts/check-db-fields.js new file mode 100644 index 0000000..5a2b255 --- /dev/null +++ b/scripts/check-db-fields.js @@ -0,0 +1,41 @@ +const { executeQuery } = require('../lib/database/connection') + +const checkDbFields = async () => { + console.log('🔍 檢查資料庫欄位名稱') + console.log('=' .repeat(40)) + + try { + // 檢查表結構 + console.log('\n📊 檢查 combined_test_results 表結構...') + const structureQuery = 'DESCRIBE combined_test_results' + const structure = await executeQuery(structureQuery) + + console.log('📋 表欄位:') + structure.forEach(field => { + console.log(` ${field.Field}: ${field.Type}`) + }) + + // 檢查實際資料 + console.log('\n📊 檢查實際資料...') + const dataQuery = 'SELECT id, user_id, logic_score, creativity_score, balance_score, overall_score FROM combined_test_results LIMIT 2' + const data = await executeQuery(dataQuery) + + console.log('📋 實際資料:') + data.forEach((row, index) => { + console.log(`\n 記錄 ${index + 1}:`) + console.log(` id: ${row.id}`) + console.log(` user_id: ${row.user_id}`) + console.log(` logic_score: ${row.logic_score}`) + console.log(` creativity_score: ${row.creativity_score}`) + console.log(` balance_score: ${row.balance_score}`) + console.log(` overall_score: ${row.overall_score}`) + }) + + } catch (error) { + console.error('❌ 檢查失敗:', error.message) + } finally { + console.log('\n✅ 資料庫欄位檢查完成') + } +} + +checkDbFields() diff --git a/scripts/test-admin-results.js b/scripts/test-admin-results.js new file mode 100644 index 0000000..4202896 --- /dev/null +++ b/scripts/test-admin-results.js @@ -0,0 +1,170 @@ +const http = require('http') + +const testAdminResults = async () => { + console.log('🔍 測試管理員測驗結果功能') + console.log('=' .repeat(40)) + + try { + // 測試基本 API 呼叫 + console.log('\n📊 測試基本 API 呼叫...') + const basicResponse = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results', (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ + status: res.statusCode, + data: data + })) + }) + req.on('error', reject) + }) + + if (basicResponse.status === 200) { + const basicData = JSON.parse(basicResponse.data) + if (basicData.success) { + console.log('✅ 基本 API 呼叫成功') + console.log(`📈 統計資料:`) + console.log(` 總測試次數: ${basicData.data.stats.totalResults}`) + console.log(` 平均分數: ${basicData.data.stats.averageScore}`) + console.log(` 總用戶數: ${basicData.data.stats.totalUsers}`) + console.log(` 參與率: ${basicData.data.stats.participationRate}%`) + console.log(` 測試類型分布:`) + console.log(` 邏輯思維: ${basicData.data.stats.testTypeCounts.logic} 次`) + console.log(` 創意能力: ${basicData.data.stats.testTypeCounts.creative} 次`) + console.log(` 綜合能力: ${basicData.data.stats.testTypeCounts.combined} 次`) + console.log(`📄 分頁資訊:`) + console.log(` 當前頁: ${basicData.data.pagination.currentPage}`) + console.log(` 總頁數: ${basicData.data.pagination.totalPages}`) + console.log(` 每頁限制: ${basicData.data.pagination.limit}`) + console.log(` 總結果數: ${basicData.data.pagination.totalResults}`) + console.log(`🏢 部門列表: ${basicData.data.departments.join(', ')}`) + console.log(`📋 結果數量: ${basicData.data.results.length}`) + } else { + console.log('❌ 基本 API 呼叫失敗:', basicData.message) + return + } + } else { + console.log('❌ 基本 API 呼叫失敗,狀態碼:', basicResponse.status) + return + } + + // 測試搜尋功能 + console.log('\n🔍 測試搜尋功能...') + const searchResponse = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results?search=王', (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ + status: res.statusCode, + data: data + })) + }) + req.on('error', reject) + }) + + if (searchResponse.status === 200) { + const searchData = JSON.parse(searchResponse.data) + if (searchData.success) { + console.log(`✅ 搜尋功能正常,找到 ${searchData.data.pagination.totalResults} 筆結果`) + } else { + console.log('❌ 搜尋功能失敗:', searchData.message) + } + } + + // 測試部門篩選 + console.log('\n🏢 測試部門篩選...') + const deptResponse = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results?department=人力資源部', (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ + status: res.statusCode, + data: data + })) + }) + req.on('error', reject) + }) + + if (deptResponse.status === 200) { + const deptData = JSON.parse(deptResponse.data) + if (deptData.success) { + console.log(`✅ 部門篩選正常,找到 ${deptData.data.pagination.totalResults} 筆結果`) + } else { + console.log('❌ 部門篩選失敗:', deptData.message) + } + } + + // 測試測試類型篩選 + console.log('\n🧠 測試測試類型篩選...') + const typeResponse = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results?testType=logic', (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ + status: res.statusCode, + data: data + })) + }) + req.on('error', reject) + }) + + if (typeResponse.status === 200) { + const typeData = JSON.parse(typeResponse.data) + if (typeData.success) { + console.log(`✅ 測試類型篩選正常,找到 ${typeData.data.pagination.totalResults} 筆結果`) + } else { + console.log('❌ 測試類型篩選失敗:', typeData.message) + } + } + + // 測試分頁功能 + console.log('\n📄 測試分頁功能...') + const pageResponse = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results?page=1&limit=5', (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ + status: res.statusCode, + data: data + })) + }) + req.on('error', reject) + }) + + if (pageResponse.status === 200) { + const pageData = JSON.parse(pageResponse.data) + if (pageData.success) { + console.log(`✅ 分頁功能正常`) + console.log(` 每頁限制: ${pageData.data.pagination.limit}`) + console.log(` 當前頁結果數: ${pageData.data.results.length}`) + console.log(` 總頁數: ${pageData.data.pagination.totalPages}`) + } else { + console.log('❌ 分頁功能失敗:', pageData.message) + } + } + + console.log('\n🎯 功能特點:') + console.log('✅ 從資料庫獲取所有測驗結果') + console.log('✅ 支援搜尋用戶姓名和郵箱') + console.log('✅ 支援部門篩選') + console.log('✅ 支援測試類型篩選') + console.log('✅ 支援分頁功能') + console.log('✅ 顯示詳細統計資料') + console.log('✅ 響應式設計(桌面版和手機版)') + console.log('✅ 載入狀態和錯誤處理') + + console.log('\n📊 資料來源:') + console.log('✅ test_results 表(基本測試結果)') + console.log('✅ logic_test_answers 表(邏輯測試詳細答案)') + console.log('✅ creative_test_answers 表(創意測試詳細答案)') + console.log('✅ combined_test_results 表(綜合測試結果)') + console.log('✅ users 表(用戶資訊)') + + } catch (error) { + console.error('❌ 測試失敗:', error.message) + } finally { + console.log('\n✅ 管理員測驗結果功能測試完成') + } +} + +testAdminResults() diff --git a/scripts/test-export-details.js b/scripts/test-export-details.js new file mode 100644 index 0000000..d9706e8 --- /dev/null +++ b/scripts/test-export-details.js @@ -0,0 +1,89 @@ +const http = require('http') + +const testExportDetails = async () => { + console.log('🔍 測試匯出詳細資料') + console.log('=' .repeat(40)) + + try { + // 測試匯出 API + const response = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results/export', (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ + status: res.statusCode, + data: data + })) + }) + req.on('error', reject) + }) + + if (response.status === 200) { + const data = JSON.parse(response.data) + if (data.success) { + console.log('✅ 匯出成功') + + // 解碼並檢查 CSV 內容 + const binaryString = Buffer.from(data.data, 'base64').toString('binary') + const csvContent = Buffer.from(binaryString, 'binary').toString('utf-8') + const lines = csvContent.split('\n') + + console.log('\n📋 所有綜合測試的詳細資料:') + lines.forEach((line, index) => { + if (index === 0) return // 跳過標題行 + + const columns = line.split(',') + if (columns.length >= 8) { + const testType = columns[3].replace(/"/g, '') + const details = columns[7].replace(/"/g, '') + + if (testType === '綜合能力') { + console.log(` 第 ${index} 行: ${details}`) + } + } + }) + + // 檢查原始資料 + console.log('\n🔍 檢查原始 API 資料...') + const apiResponse = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results', (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ + status: res.statusCode, + data: data + })) + }) + req.on('error', reject) + }) + + if (apiResponse.status === 200) { + const apiData = JSON.parse(apiResponse.data) + if (apiData.success) { + const combinedResults = apiData.data.results.filter(result => result.type === 'combined') + console.log(`\n📊 API 中的綜合測試結果 (${combinedResults.length} 筆):`) + + combinedResults.forEach((result, index) => { + console.log(`\n 結果 ${index + 1}:`) + console.log(` 用戶: ${result.userName}`) + console.log(` 分數: ${result.score}`) + console.log(` 詳細資料:`, JSON.stringify(result.details, null, 2)) + }) + } + } + + } else { + console.log('❌ 匯出失敗:', data.message) + } + } else { + console.log('❌ 匯出失敗,狀態碼:', response.status) + } + + } catch (error) { + console.error('❌ 測試失敗:', error.message) + } finally { + console.log('\n✅ 匯出詳細資料測試完成') + } +} + +testExportDetails() diff --git a/scripts/test-export-results.js b/scripts/test-export-results.js new file mode 100644 index 0000000..b71813f --- /dev/null +++ b/scripts/test-export-results.js @@ -0,0 +1,154 @@ +const http = require('http') + +const testExportResults = async () => { + console.log('🔍 測試測驗結果匯出功能') + console.log('=' .repeat(40)) + + try { + // 測試基本匯出 + console.log('\n📊 測試基本匯出...') + const basicResponse = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results/export', (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ + status: res.statusCode, + data: data + })) + }) + req.on('error', reject) + }) + + if (basicResponse.status === 200) { + const basicData = JSON.parse(basicResponse.data) + if (basicData.success) { + console.log('✅ 基本匯出成功') + console.log(`📄 檔案名稱: ${basicData.filename}`) + console.log(`📊 內容類型: ${basicData.contentType}`) + console.log(`📏 資料大小: ${basicData.data.length} 字元`) + + // 解碼並檢查 CSV 內容 + const binaryString = Buffer.from(basicData.data, 'base64').toString('binary') + const csvContent = Buffer.from(binaryString, 'binary').toString('utf-8') + + console.log('\n📋 CSV 內容預覽:') + const lines = csvContent.split('\n') + console.log(`總行數: ${lines.length}`) + console.log('前 5 行內容:') + lines.slice(0, 5).forEach((line, index) => { + console.log(` ${index + 1}: ${line}`) + }) + + // 檢查是否包含中文 + const hasChinese = /[\u4e00-\u9fff]/.test(csvContent) + console.log(`✅ 包含中文字符: ${hasChinese ? '是' : '否'}`) + + // 檢查 BOM + const hasBOM = csvContent.startsWith('\uFEFF') + console.log(`✅ 包含 UTF-8 BOM: ${hasBOM ? '是' : '否'}`) + + } else { + console.log('❌ 基本匯出失敗:', basicData.message) + return + } + } else { + console.log('❌ 基本匯出失敗,狀態碼:', basicResponse.status) + return + } + + // 測試篩選匯出 + console.log('\n🔍 測試篩選匯出...') + const filterResponse = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results/export?testType=logic&department=人力資源部', (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ + status: res.statusCode, + data: data + })) + }) + req.on('error', reject) + }) + + if (filterResponse.status === 200) { + const filterData = JSON.parse(filterResponse.data) + if (filterData.success) { + console.log('✅ 篩選匯出成功') + + // 解碼並檢查篩選結果 + const binaryString = Buffer.from(filterData.data, 'base64').toString('binary') + const csvContent = Buffer.from(binaryString, 'binary').toString('utf-8') + const lines = csvContent.split('\n') + + console.log(`📊 篩選後結果數量: ${lines.length - 1} 筆(扣除標題行)`) + + // 檢查是否只包含邏輯測試 + const logicLines = lines.filter(line => line.includes('邏輯思維')) + console.log(`🧠 邏輯思維測試數量: ${logicLines.length}`) + + } else { + console.log('❌ 篩選匯出失敗:', filterData.message) + } + } + + // 測試搜尋匯出 + console.log('\n🔍 測試搜尋匯出...') + const searchResponse = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results/export?search=王', (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ + status: res.statusCode, + data: data + })) + }) + req.on('error', reject) + }) + + if (searchResponse.status === 200) { + const searchData = JSON.parse(searchResponse.data) + if (searchData.success) { + console.log('✅ 搜尋匯出成功') + + // 解碼並檢查搜尋結果 + const binaryString = Buffer.from(searchData.data, 'base64').toString('binary') + const csvContent = Buffer.from(binaryString, 'binary').toString('utf-8') + const lines = csvContent.split('\n') + + console.log(`📊 搜尋結果數量: ${lines.length - 1} 筆(扣除標題行)`) + + // 檢查是否只包含包含「王」的結果 + const wangLines = lines.filter(line => line.includes('王')) + console.log(`👤 包含「王」的結果數量: ${wangLines.length}`) + + } else { + console.log('❌ 搜尋匯出失敗:', searchData.message) + } + } + + console.log('\n🎯 匯出功能特點:') + console.log('✅ 支援 CSV 格式匯出') + console.log('✅ 包含 UTF-8 BOM,確保中文正確顯示') + console.log('✅ 支援篩選條件匯出') + console.log('✅ 包含詳細的測試結果資料') + console.log('✅ 自動生成檔案名稱(包含日期)') + console.log('✅ 支援搜尋、部門、測試類型篩選') + + console.log('\n📊 匯出欄位:') + console.log('✅ 用戶姓名') + console.log('✅ 用戶郵箱') + console.log('✅ 部門') + console.log('✅ 測試類型') + console.log('✅ 分數') + console.log('✅ 等級') + console.log('✅ 完成時間') + console.log('✅ 詳細資料(根據測試類型不同)') + + } catch (error) { + console.error('❌ 測試失敗:', error.message) + } finally { + console.log('\n✅ 測驗結果匯出功能測試完成') + } +} + +testExportResults() diff --git a/scripts/test-export-simple.js b/scripts/test-export-simple.js index c2be1d2..af4545d 100644 --- a/scripts/test-export-simple.js +++ b/scripts/test-export-simple.js @@ -1,14 +1,12 @@ const http = require('http') const testExportSimple = async () => { - console.log('🔍 測試簡化匯出功能') + console.log('🔍 簡單測試匯出功能') console.log('=' .repeat(30)) try { - // 先測試獲取題目資料 - console.log('\n📊 測試獲取邏輯題目資料...') - const logicResponse = await new Promise((resolve, reject) => { - const req = http.get('http://localhost:3000/api/questions/logic', (res) => { + const response = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results/export', (res) => { let data = '' res.on('data', chunk => data += chunk) res.on('end', () => resolve({ @@ -19,50 +17,40 @@ const testExportSimple = async () => { req.on('error', reject) }) - console.log(`狀態碼: ${logicResponse.status}`) - - if (logicResponse.status === 200) { - const logicData = JSON.parse(logicResponse.data) - console.log(`成功獲取 ${logicData.data?.length || 0} 道邏輯題目`) - - if (logicData.data && logicData.data.length > 0) { - const firstQuestion = logicData.data[0] - console.log(`第一題: ${firstQuestion.question?.substring(0, 50)}...`) - console.log(`選項A: ${firstQuestion.option_a}`) - console.log(`正確答案: ${firstQuestion.correct_answer}`) + if (response.status === 200) { + const data = JSON.parse(response.data) + if (data.success) { + console.log('✅ 匯出成功') + + // 解碼並檢查 CSV 內容 + const binaryString = Buffer.from(data.data, 'base64').toString('binary') + const csvContent = Buffer.from(binaryString, 'binary').toString('utf-8') + + // 只顯示前幾行 + const lines = csvContent.split('\n') + console.log('\n📋 CSV 前 10 行:') + lines.slice(0, 10).forEach((line, index) => { + console.log(`${index + 1}: ${line}`) + }) + + // 檢查是否有「創意」和「平衡」字樣 + const hasCreative = csvContent.includes('創意') + const hasBalance = csvContent.includes('平衡') + console.log(`\n🔍 檢查結果:`) + console.log(` 包含「創意」: ${hasCreative ? '是' : '否'}`) + console.log(` 包含「平衡」: ${hasBalance ? '是' : '否'}`) + + if (hasCreative && hasBalance) { + console.log('✅ 修復成功!') + } else { + console.log('❌ 仍有問題') + } + + } else { + console.log('❌ 匯出失敗:', data.message) } } else { - console.log('❌ 獲取邏輯題目失敗') - } - - // 測試創意題目 - console.log('\n📊 測試獲取創意題目資料...') - const creativeResponse = await new Promise((resolve, reject) => { - const req = http.get('http://localhost:3000/api/questions/creative', (res) => { - let data = '' - res.on('data', chunk => data += chunk) - res.on('end', () => resolve({ - status: res.statusCode, - data: data - })) - }) - req.on('error', reject) - }) - - console.log(`狀態碼: ${creativeResponse.status}`) - - if (creativeResponse.status === 200) { - const creativeData = JSON.parse(creativeResponse.data) - console.log(`成功獲取 ${creativeData.data?.length || 0} 道創意題目`) - - if (creativeData.data && creativeData.data.length > 0) { - const firstQuestion = creativeData.data[0] - console.log(`第一題: ${firstQuestion.statement?.substring(0, 50)}...`) - console.log(`類別: ${firstQuestion.category}`) - console.log(`反向計分: ${firstQuestion.is_reverse}`) - } - } else { - console.log('❌ 獲取創意題目失敗') + console.log('❌ 匯出失敗,狀態碼:', response.status) } } catch (error) { @@ -70,4 +58,4 @@ const testExportSimple = async () => { } } -testExportSimple() +testExportSimple() \ No newline at end of file diff --git a/scripts/test-fixed-export.js b/scripts/test-fixed-export.js new file mode 100644 index 0000000..e9a8f57 --- /dev/null +++ b/scripts/test-fixed-export.js @@ -0,0 +1,106 @@ +const http = require('http') + +const testFixedExport = async () => { + console.log('🔍 測試修復後的匯出功能') + console.log('=' .repeat(40)) + + try { + // 測試基本匯出 + console.log('\n📊 測試修復後的匯出...') + const response = await new Promise((resolve, reject) => { + const req = http.get('http://localhost:3000/api/admin/test-results/export', (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ + status: res.statusCode, + data: data + })) + }) + req.on('error', reject) + }) + + if (response.status === 200) { + const data = JSON.parse(response.data) + if (data.success) { + console.log('✅ 匯出成功') + + // 解碼並檢查 CSV 內容 + const binaryString = Buffer.from(data.data, 'base64').toString('binary') + const csvContent = Buffer.from(binaryString, 'binary').toString('utf-8') + const lines = csvContent.split('\n') + + console.log(`📊 總行數: ${lines.length}`) + console.log('\n📋 檢查詳細資料欄位:') + + let hasUndefined = false + let combinedCount = 0 + let logicCount = 0 + let creativeCount = 0 + + lines.forEach((line, index) => { + if (index === 0) return // 跳過標題行 + + const columns = line.split(',') + if (columns.length >= 8) { + const testType = columns[3].replace(/"/g, '') + const details = columns[7].replace(/"/g, '') + + if (testType === '綜合能力') { + combinedCount++ + console.log(` 綜合能力測試 ${combinedCount}: ${details}`) + if (details.includes('undefined')) { + hasUndefined = true + console.log(` ❌ 發現 undefined: ${details}`) + } else { + console.log(` ✅ 無 undefined`) + } + } else if (testType === '邏輯思維') { + logicCount++ + console.log(` 邏輯思維測試 ${logicCount}: ${details}`) + } else if (testType === '創意能力') { + creativeCount++ + console.log(` 創意能力測試 ${creativeCount}: ${details}`) + } + } + }) + + console.log('\n📊 統計:') + console.log(` 綜合能力測試: ${combinedCount} 筆`) + console.log(` 邏輯思維測試: ${logicCount} 筆`) + console.log(` 創意能力測試: ${creativeCount} 筆`) + + if (hasUndefined) { + console.log('\n❌ 仍然發現 undefined 值') + } else { + console.log('\n✅ 所有詳細資料欄位都沒有 undefined 值') + } + + // 檢查特定修復 + console.log('\n🔧 檢查修復項目:') + const hasUndefinedInCombined = csvContent.includes('undefined') + const hasNoDataPlaceholder = csvContent.includes('無資料') + + console.log(` 包含 undefined: ${hasUndefinedInCombined ? '是' : '否'}`) + console.log(` 包含「無資料」: ${hasNoDataPlaceholder ? '是' : '否'}`) + + if (!hasUndefinedInCombined && hasNoDataPlaceholder) { + console.log('✅ 修復成功!undefined 已被替換為「無資料」') + } else if (hasUndefinedInCombined) { + console.log('❌ 修復失敗,仍有 undefined 值') + } + + } else { + console.log('❌ 匯出失敗:', data.message) + } + } else { + console.log('❌ 匯出失敗,狀態碼:', response.status) + } + + } catch (error) { + console.error('❌ 測試失敗:', error.message) + } finally { + console.log('\n✅ 修復後匯出功能測試完成') + } +} + +testFixedExport()