新增連結測驗邀請註冊功能

This commit is contained in:
2025-10-04 22:18:50 +08:00
parent c5a48807e5
commit c6bfed931f
2 changed files with 304 additions and 405 deletions

View File

@@ -1,435 +1,114 @@
"use client"
import { useState, useEffect } 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,
Copy,
CheckCircle,
Clock,
Users,
Link as LinkIcon,
Send,
RefreshCw,
Eye,
Trash2,
Plus,
Loader2,
} from "lucide-react"
import Link from "next/link"
// 定義測驗連結類型
interface TestLink {
id: string
name: string
type: 'logic' | 'creative' | 'combined'
url: string
createdAt: string
expiresAt?: string
isActive: boolean
totalSent: number
completedCount: number
pendingCount: number
}
// 定義已送出連結的狀態
interface SentLinkStatus {
id: string
linkId: string
recipientName: string
recipientEmail: string
sentAt: string
status: 'pending' | 'completed' | 'expired'
completedAt?: string
testType: 'logic' | 'creative' | 'combined'
score?: number
}
export default function TestLinksPage() {
const [currentLinks, setCurrentLinks] = useState<TestLink[]>([])
const [sentLinks, setSentLinks] = useState<SentLinkStatus[]>([])
const [loading, setLoading] = useState(true)
const [newLinkName, setNewLinkName] = useState("")
const [newLinkType, setNewLinkType] = useState<'logic' | 'creative' | 'combined'>('combined')
const [showCreateForm, setShowCreateForm] = useState(false)
// 模擬數據
useEffect(() => {
const mockCurrentLinks: TestLink[] = [
{
id: '1',
name: '2024年度綜合能力測驗',
type: 'combined',
url: 'https://hr-assessment.com/test/combined/abc123',
createdAt: '2024-01-15T10:00:00Z',
expiresAt: '2024-12-31T23:59:59Z',
isActive: true,
totalSent: 45,
completedCount: 38,
pendingCount: 7
},
{
id: '2',
name: '創意能力專項測驗',
type: 'creative',
url: 'https://hr-assessment.com/test/creative/def456',
createdAt: '2024-01-20T14:30:00Z',
isActive: true,
totalSent: 23,
completedCount: 20,
pendingCount: 3
}
]
const mockSentLinks: SentLinkStatus[] = [
{
id: '1',
linkId: '1',
recipientName: '張小明',
recipientEmail: 'zhang.xiaoming@company.com',
sentAt: '2024-01-15T10:30:00Z',
status: 'completed',
completedAt: '2024-01-16T09:15:00Z',
testType: 'combined',
score: 85
},
{
id: '2',
linkId: '1',
recipientName: '李美華',
recipientEmail: 'li.meihua@company.com',
sentAt: '2024-01-15T11:00:00Z',
status: 'completed',
completedAt: '2024-01-17T14:20:00Z',
testType: 'combined',
score: 92
},
{
id: '3',
linkId: '1',
recipientName: '王大偉',
recipientEmail: 'wang.dawei@company.com',
sentAt: '2024-01-15T11:30:00Z',
status: 'pending',
testType: 'combined'
},
{
id: '4',
linkId: '2',
recipientName: '陳小芳',
recipientEmail: 'chen.xiaofang@company.com',
sentAt: '2024-01-20T15:00:00Z',
status: 'completed',
completedAt: '2024-01-21T10:45:00Z',
testType: 'creative',
score: 78
}
]
setTimeout(() => {
setCurrentLinks(mockCurrentLinks)
setSentLinks(mockSentLinks)
setLoading(false)
}, 1000)
}, [])
const handleCopyLink = (url: string) => {
const handleCopyLink = () => {
const url = `${typeof window !== 'undefined' ? window.location.origin : ''}/test-link`
navigator.clipboard.writeText(url)
// 這裡可以添加 toast 提示
}
const handleCreateLink = () => {
if (!newLinkName.trim()) return
const newLink: TestLink = {
id: Date.now().toString(),
name: newLinkName,
type: newLinkType,
url: `https://hr-assessment.com/test/${newLinkType}/${Math.random().toString(36).substr(2, 9)}`,
createdAt: new Date().toISOString(),
isActive: true,
totalSent: 0,
completedCount: 0,
pendingCount: 0
}
setCurrentLinks([...currentLinks, newLink])
setNewLinkName("")
setShowCreateForm(false)
}
const getStatusBadge = (status: string) => {
switch (status) {
case 'completed':
return <Badge variant="default" className="bg-green-500"></Badge>
case 'pending':
return <Badge variant="secondary"></Badge>
case 'expired':
return <Badge variant="destructive"></Badge>
default:
return <Badge variant="outline"></Badge>
}
}
const getTypeName = (type: string) => {
switch (type) {
case 'logic':
return '邏輯思維'
case 'creative':
return '創意能力'
case 'combined':
return '綜合能力'
default:
return '未知'
}
}
if (loading) {
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
</div>
</div>
)
alert('連結已複製到剪貼簿')
}
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-3">
<Button variant="ghost" size="sm" asChild>
<Link href="/dashboard">
<ArrowLeft className="w-4 h-4 mr-2" />
</Link>
</Button>
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
<LinkIcon className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-foreground"></h1>
<p className="text-sm text-muted-foreground">
</p>
<ProtectedRoute>
<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-3">
<Button variant="ghost" size="sm" asChild>
<Link href="/dashboard">
<ArrowLeft className="w-4 h-4 mr-2" />
</Link>
</Button>
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
<LinkIcon className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-foreground"></h1>
<p className="text-sm text-muted-foreground">
</p>
</div>
</div>
</div>
</div>
</header>
</header>
<div className="container mx-auto px-4 py-8">
<div className="max-w-6xl mx-auto space-y-6">
<Tabs defaultValue="current" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="current"></TabsTrigger>
<TabsTrigger value="sent"></TabsTrigger>
</TabsList>
{/* 目前測驗連結 */}
<TabsContent value="current" className="space-y-6">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle></CardTitle>
<CardDescription>
</CardDescription>
</div>
<Button onClick={() => setShowCreateForm(!showCreateForm)}>
<Plus className="w-4 h-4 mr-2" />
{/* Main Content */}
<div className="container mx-auto px-4 py-8">
<div className="max-w-2xl mx-auto">
<Card>
<CardHeader className="text-center">
<CardTitle className="flex items-center justify-center gap-2">
<LinkIcon className="w-6 h-6" />
</CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* 連結顯示區域 */}
<div className="space-y-3">
<Label></Label>
<div className="flex items-center gap-2">
<Input
value={`${typeof window !== 'undefined' ? window.location.origin : ''}/test-link`}
readOnly
className="text-sm"
/>
<Button
onClick={() => {
const url = `${typeof window !== 'undefined' ? window.location.origin : ''}/test-link`
navigator.clipboard.writeText(url)
alert('連結已複製到剪貼簿')
}}
>
<Copy className="w-4 h-4" />
</Button>
</div>
</CardHeader>
<CardContent>
{/* 新增連結表單 */}
{showCreateForm && (
<Card className="mb-6">
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="linkName"></Label>
<Input
id="linkName"
value={newLinkName}
onChange={(e) => setNewLinkName(e.target.value)}
placeholder="請輸入連結名稱"
/>
</div>
<div>
<Label htmlFor="linkType"></Label>
<Select value={newLinkType} onValueChange={(value: any) => setNewLinkType(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="logic"></SelectItem>
<SelectItem value="creative"></SelectItem>
<SelectItem value="combined"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex gap-2">
<Button onClick={handleCreateLink} disabled={!newLinkName.trim()}>
</Button>
<Button variant="outline" onClick={() => setShowCreateForm(false)}>
</Button>
</div>
</CardContent>
</Card>
)}
</div>
{/* 連結列表 */}
<div className="space-y-4">
{currentLinks.map((link) => (
<Card key={link.id}>
<CardContent className="p-6">
<div className="flex items-start justify-between">
<div className="space-y-2">
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold">{link.name}</h3>
<Badge variant={link.isActive ? "default" : "secondary"}>
{link.isActive ? "啟用中" : "已停用"}
</Badge>
<Badge variant="outline">{getTypeName(link.type)}</Badge>
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span>: {new Date(link.createdAt).toLocaleString('zh-TW')}</span>
{link.expiresAt && (
<span>: {new Date(link.expiresAt).toLocaleString('zh-TW')}</span>
)}
</div>
<div className="flex items-center gap-6 text-sm">
<span className="flex items-center gap-1">
<Users className="w-4 h-4" />
: {link.totalSent}
</span>
<span className="flex items-center gap-1 text-green-600">
<CheckCircle className="w-4 h-4" />
: {link.completedCount}
</span>
<span className="flex items-center gap-1 text-orange-600">
<Clock className="w-4 h-4" />
: {link.pendingCount}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleCopyLink(link.url)}
>
<Copy className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" size="sm" asChild>
<Link href={`/test/${link.type}`} target="_blank">
<Eye className="w-4 h-4 mr-2" />
</Link>
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</CardContent>
</Card>
</TabsContent>
{/* 功能說明 */}
<div className="bg-muted/50 rounded-lg p-4 space-y-2">
<h4 className="font-medium text-sm"></h4>
<ul className="text-sm text-muted-foreground space-y-1">
<li> </li>
<li> Aa123456</li>
<li> </li>
<li> </li>
</ul>
</div>
{/* 已送出連結狀態 */}
<TabsContent value="sent" className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sentLinks.map((sentLink) => (
<TableRow key={sentLink.id}>
<TableCell>
<div>
<div className="font-medium">{sentLink.recipientName}</div>
<div className="text-sm text-muted-foreground">{sentLink.recipientEmail}</div>
</div>
</TableCell>
<TableCell>
<Badge variant="outline">{getTypeName(sentLink.testType)}</Badge>
</TableCell>
<TableCell>
{new Date(sentLink.sentAt).toLocaleString('zh-TW')}
</TableCell>
<TableCell>
{getStatusBadge(sentLink.status)}
</TableCell>
<TableCell>
{sentLink.completedAt
? new Date(sentLink.completedAt).toLocaleString('zh-TW')
: '-'
}
</TableCell>
<TableCell>
{sentLink.score ? `${sentLink.score}` : '-'}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" asChild>
<Link href={`/admin/results?user=${sentLink.recipientEmail}`}>
<Eye className="w-4 h-4" />
</Link>
</Button>
{sentLink.status === 'pending' && (
<Button variant="outline" size="sm">
<Send className="w-4 h-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* 預覽按鈕 */}
<div className="flex justify-center">
<Button
variant="outline"
onClick={() => window.open('/test-link', '_blank')}
className="w-full sm:w-auto"
>
<Eye className="w-4 h-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
</ProtectedRoute>
)
}

220
app/test-link/page.tsx Normal file
View File

@@ -0,0 +1,220 @@
"use client"
import { useState } from "react"
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 { Alert, AlertDescription } from "@/components/ui/alert"
import { Loader2, User, Mail, Building, CheckCircle } from "lucide-react"
import { useRouter } from "next/navigation"
export default function TestLinkPage() {
const router = useRouter()
const [userInfo, setUserInfo] = useState({
name: '',
email: '',
department: ''
})
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [error, setError] = useState('')
const handleUserInfoChange = (field: string, value: string) => {
setUserInfo(prev => ({
...prev,
[field]: value
}))
setError('') // 清除錯誤訊息
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!userInfo.name || !userInfo.email || !userInfo.department) {
setError('請填寫所有必填欄位')
return
}
setIsSubmitting(true)
setError('')
try {
// 註冊用戶
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: userInfo.name,
email: userInfo.email,
department: userInfo.department,
password: 'Aa123456', // 預設密碼
role: 'user'
}),
})
if (response.ok) {
// 註冊成功後自動登入
const loginResponse = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: userInfo.email,
password: 'Aa123456'
}),
})
if (loginResponse.ok) {
const loginData = await loginResponse.json()
// 存儲 token 和用戶資料到 localStorage
if (loginData.accessToken && loginData.refreshToken) {
localStorage.setItem('accessToken', loginData.accessToken)
localStorage.setItem('refreshToken', loginData.refreshToken)
localStorage.setItem('hr_current_user', JSON.stringify(loginData.user))
}
setIsSuccess(true)
// 2秒後跳轉到個人專區儀表板
setTimeout(() => {
// 使用 window.location.href 確保頁面完全重新載入
window.location.href = '/home'
}, 2000)
} else {
setError('註冊成功但自動登入失敗,請手動登入')
}
} else {
const errorData = await response.json()
setError(errorData.message || '註冊失敗,請稍後再試')
}
} catch (error) {
console.error('Registration error:', error)
setError('註冊失敗,請稍後再試')
} finally {
setIsSubmitting(false)
}
}
if (isSuccess) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardContent className="pt-6">
<div className="text-center space-y-4">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<div>
<h2 className="text-xl font-semibold text-green-600"></h2>
<p className="text-muted-foreground mt-2">
...
</p>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl"></CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="space-y-2">
<Label htmlFor="name" className="flex items-center gap-2">
<User className="w-4 h-4" />
</Label>
<Input
id="name"
placeholder="請輸入您的姓名"
value={userInfo.name}
onChange={(e) => handleUserInfoChange('name', e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="email" className="flex items-center gap-2">
<Mail className="w-4 h-4" />
</Label>
<Input
id="email"
type="email"
placeholder="請輸入電子郵件"
value={userInfo.email}
onChange={(e) => handleUserInfoChange('email', e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="department" className="flex items-center gap-2">
<Building className="w-4 h-4" />
</Label>
<Select
value={userInfo.department}
onValueChange={(value) => handleUserInfoChange('department', value)}
required
>
<SelectTrigger>
<SelectValue placeholder="請選擇部門" />
</SelectTrigger>
<SelectContent>
<SelectItem value="資訊技術部"></SelectItem>
<SelectItem value="人力資源部"></SelectItem>
<SelectItem value="財務部"></SelectItem>
<SelectItem value="行銷部"></SelectItem>
<SelectItem value="營運部"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
</div>
<Button
type="submit"
className="w-full"
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
'開始測驗'
)}
</Button>
</form>
<div className="mt-6 p-3 bg-muted/50 rounded-lg">
<p className="text-xs text-muted-foreground text-center">
<strong>Aa123456</strong>
</p>
</div>
</CardContent>
</Card>
</div>
)
}