Files
hr-assessment-system/app/admin/questions/page.tsx
2025-09-25 12:30:25 +08:00

355 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import type React from "react"
import { useState } from "react"
import { ProtectedRoute } from "@/components/protected-route"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
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 { Alert, AlertDescription } from "@/components/ui/alert"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
ArrowLeft,
Upload,
Download,
Brain,
Lightbulb,
FileSpreadsheet,
CheckCircle,
AlertCircle,
Info,
} from "lucide-react"
import Link from "next/link"
import { logicQuestions } from "@/lib/questions/logic-questions"
import { creativeQuestions } from "@/lib/questions/creative-questions"
import { parseExcelFile, type ImportResult } from "@/lib/utils/excel-parser"
export default function QuestionsManagementPage() {
return (
<ProtectedRoute adminOnly>
<QuestionsManagementContent />
</ProtectedRoute>
)
}
function QuestionsManagementContent() {
const [activeTab, setActiveTab] = useState("logic")
const [isImporting, setIsImporting] = useState(false)
const [importResult, setImportResult] = useState<ImportResult | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [importType, setImportType] = useState<"logic" | "creative">("logic")
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
if (
file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
file.type === "application/vnd.ms-excel" ||
file.name.endsWith(".xlsx") ||
file.name.endsWith(".xls")
) {
setSelectedFile(file)
setImportResult(null)
} else {
setImportResult({
success: false,
message: "請選擇 Excel 檔案 (.xlsx 或 .xls)",
})
}
}
}
const handleImport = async () => {
if (!selectedFile) {
setImportResult({
success: false,
message: "請先選擇要匯入的 Excel 檔案",
})
return
}
setIsImporting(true)
try {
const result = await parseExcelFile(selectedFile, importType)
setImportResult(result)
if (result.success) {
setSelectedFile(null)
// 重置檔案輸入
const fileInput = document.getElementById("file-input") as HTMLInputElement
if (fileInput) fileInput.value = ""
}
} catch (error) {
setImportResult({
success: false,
message: "匯入失敗,請檢查檔案格式是否正確",
errors: [error instanceof Error ? error.message : "未知錯誤"],
})
} finally {
setIsImporting(false)
}
}
const downloadTemplate = (type: "logic" | "creative") => {
let csvContent = ""
if (type === "logic") {
csvContent = [
["題目ID", "題目內容", "選項A", "選項B", "選項C", "選項D", "正確答案", "解釋"],
[
"1",
"範例題目如果所有A都是B所有B都是C那麼",
"所有A都是C",
"所有C都是A",
"有些A不是C",
"無法確定",
"A",
"根據邏輯推理...",
],
["2", "在序列 2, 4, 8, 16, ? 中,下一個數字是?", "24", "32", "30", "28", "B", "每個數字都是前一個數字的2倍"],
]
.map((row) => row.join(","))
.join("\n")
} else {
csvContent = [
["題目ID", "陳述內容", "類別", "是否反向計分"],
["1", "我經常能想出創新的解決方案", "innovation", "否"],
["2", "我更喜歡按照既定規則工作", "flexibility", "是"],
["3", "我喜歡嘗試新的做事方法", "innovation", "否"],
]
.map((row) => row.join(","))
.join("\n")
}
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", `${type === "logic" ? "邏輯思維" : "創意能力"}題目範本.csv`)
link.style.visibility = "hidden"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b bg-card/50 backdrop-blur-sm">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild>
<Link href="/dashboard">
<ArrowLeft className="w-4 h-4 mr-2" />
</Link>
</Button>
<div>
<h1 className="text-xl font-bold text-foreground"></h1>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
</div>
</header>
<div className="container mx-auto px-4 py-8">
<div className="max-w-6xl mx-auto space-y-8">
{/* Import Section */}
<Card className="border-2 border-primary/20">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="w-5 h-5 text-primary" />
Excel
</CardTitle>
<CardDescription> Excel </CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* File Upload */}
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="file-input"> Excel </Label>
<Input
id="file-input"
type="file"
accept=".xlsx,.xls"
onChange={handleFileSelect}
className="cursor-pointer"
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select value={importType} onValueChange={(value: "logic" | "creative") => setImportType(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="logic"></SelectItem>
<SelectItem value="creative"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
{selectedFile && (
<div className="flex items-center gap-2 p-3 bg-blue-50 rounded-lg border border-blue-200">
<FileSpreadsheet className="w-4 h-4 text-blue-600" />
<span className="text-sm text-blue-800">{selectedFile.name}</span>
</div>
)}
<div className="flex gap-3">
<Button onClick={handleImport} disabled={!selectedFile || isImporting} className="flex-1">
{isImporting ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
...
</>
) : (
<>
<Upload className="w-4 h-4 mr-2" />
</>
)}
</Button>
</div>
</div>
{/* Import Result */}
{importResult && (
<Alert variant={importResult.success ? "default" : "destructive"}>
{importResult.success ? <CheckCircle className="h-4 w-4" /> : <AlertCircle className="h-4 w-4" />}
<AlertDescription>
{importResult.message}
{importResult.errors && (
<ul className="mt-2 list-disc list-inside">
{importResult.errors.map((error, index) => (
<li key={index} className="text-sm">
{error}
</li>
))}
</ul>
)}
</AlertDescription>
</Alert>
)}
{/* Template Download */}
<div className="border-t pt-4">
<div className="flex items-center gap-2 mb-3">
<Info className="w-4 h-4 text-blue-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="flex gap-3">
<Button variant="outline" onClick={() => downloadTemplate("logic")} className="flex-1">
<Download className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" onClick={() => downloadTemplate("creative")} className="flex-1">
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Current Questions */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="logic" className="flex items-center gap-2">
<Brain className="w-4 h-4" />
({logicQuestions.length})
</TabsTrigger>
<TabsTrigger value="creative" className="flex items-center gap-2">
<Lightbulb className="w-4 h-4" />
({creativeQuestions.length})
</TabsTrigger>
</TabsList>
<TabsContent value="logic" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold"></h3>
<Badge variant="outline">{logicQuestions.length} </Badge>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logicQuestions.slice(0, 10).map((question) => (
<TableRow key={question.id}>
<TableCell className="font-medium">{question.id}</TableCell>
<TableCell className="max-w-md truncate">{question.question}</TableCell>
<TableCell>{question.options.length}</TableCell>
<TableCell>
<Badge className="bg-green-500 text-white">{question.correctAnswer}</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TabsContent>
<TabsContent value="creative" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold"></h3>
<Badge variant="outline">{creativeQuestions.length} </Badge>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{creativeQuestions.slice(0, 10).map((question) => (
<TableRow key={question.id}>
<TableCell className="font-medium">{question.id}</TableCell>
<TableCell className="max-w-md truncate">{question.statement}</TableCell>
<TableCell>
<Badge variant="secondary">{question.category}</Badge>
</TableCell>
<TableCell>
{question.isReverse ? (
<Badge className="bg-orange-500 text-white"></Badge>
) : (
<Badge variant="outline"></Badge>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
</div>
</div>
)
}