Files
ai-showcase-platform/components/admin/user-management.tsx
2025-08-05 08:22:44 +08:00

1143 lines
45 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 { 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 { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import { Alert, AlertDescription } from "@/components/ui/alert"
import {
Search,
MoreHorizontal,
UserPlus,
Edit,
Trash2,
Shield,
Eye,
Calendar,
Activity,
User,
Mail,
Building,
Loader2,
CheckCircle,
AlertTriangle,
Clock,
RefreshCw,
Copy,
Link,
ExternalLink,
Code,
Users,
} from "lucide-react"
// User data - empty for production
const initialMockUsers: any[] = []
export function UserManagement() {
const [users, setUsers] = useState(initialMockUsers)
const [searchTerm, setSearchTerm] = useState("")
const [selectedDepartment, setSelectedDepartment] = useState("all")
const [selectedRole, setSelectedRole] = useState("all")
const [selectedStatus, setSelectedStatus] = useState("all")
const [selectedUser, setSelectedUser] = useState<any>(null)
const [showUserDetail, setShowUserDetail] = useState(false)
const [showInviteUser, setShowInviteUser] = useState(false)
const [showEditUser, setShowEditUser] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [showInvitationLink, setShowInvitationLink] = useState(false)
const [userToDelete, setUserToDelete] = useState<any>(null)
const [generatedInvitation, setGeneratedInvitation] = useState<any>(null)
const [isLoading, setIsLoading] = useState(false)
const [success, setSuccess] = useState("")
const [error, setError] = useState("")
// 邀請用戶表單狀態 - 包含電子郵件和預設角色
const [inviteEmail, setInviteEmail] = useState("")
const [inviteRole, setInviteRole] = useState("user")
// 編輯用戶表單狀態
const [editUser, setEditUser] = useState({
id: "",
name: "",
email: "",
department: "",
role: "",
status: "",
})
const filteredUsers = users.filter((user) => {
const matchesSearch =
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
const matchesDepartment = selectedDepartment === "all" || user.department === selectedDepartment
const matchesRole =
selectedRole === "all" ||
user.role === selectedRole ||
(user.status === "invited" && (user as any).invitedRole === selectedRole)
const matchesStatus = selectedStatus === "all" || user.status === selectedStatus
return matchesSearch && matchesDepartment && matchesRole && matchesStatus
})
const handleViewUser = (user: any) => {
setSelectedUser(user)
setShowUserDetail(true)
}
const handleEditUser = (user: any) => {
setEditUser({
id: user.id,
name: user.name,
email: user.email,
department: user.department,
role: user.role,
status: user.status,
})
setShowEditUser(true)
}
const handleDeleteUser = (user: any) => {
setUserToDelete(user)
setShowDeleteConfirm(true)
}
const handleToggleUserStatus = async (userId: string) => {
setIsLoading(true)
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1000))
setUsers(
users.map((user) =>
user.id === userId ? { ...user, status: user.status === "active" ? "inactive" : "active" } : user,
),
)
setIsLoading(false)
setSuccess("用戶狀態更新成功!")
setTimeout(() => setSuccess(""), 3000)
}
const handleChangeUserRole = async (userId: string, newRole: string) => {
setIsLoading(true)
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1000))
setUsers(users.map((user) => (user.id === userId ? { ...user, role: newRole } : user)))
setIsLoading(false)
setSuccess(`用戶權限已更新為${getRoleText(newRole)}`)
setTimeout(() => setSuccess(""), 3000)
}
const handleGenerateInvitation = async () => {
setError("")
// 驗證表單
if (!inviteEmail) {
setError("請輸入電子郵件")
return
}
// 檢查電子郵件格式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(inviteEmail)) {
setError("請輸入有效的電子郵件格式")
return
}
// 檢查電子郵件是否已存在
if (users.some((user) => user.email === inviteEmail)) {
setError("此電子郵件已被使用或已發送邀請")
return
}
setIsLoading(true)
// 模擬生成邀請連結
await new Promise((resolve) => setTimeout(resolve, 1500))
// 生成邀請 token實際應用中會由後端生成
const invitationToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
const invitationLink = `${window.location.origin}/register?token=${invitationToken}&email=${encodeURIComponent(inviteEmail)}&role=${inviteRole}`
const newInvitation = {
id: Date.now().toString(),
name: "",
email: inviteEmail,
department: "",
role: "",
status: "invited",
joinDate: "",
lastLogin: "",
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
invitationSentAt: new Date().toLocaleString("zh-TW", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}),
invitationLink: invitationLink,
invitedRole: inviteRole, // 記錄邀請時的預設角色
}
setUsers([...users, newInvitation])
setGeneratedInvitation(newInvitation)
setInviteEmail("")
setInviteRole("user")
setIsLoading(false)
setShowInviteUser(false)
setShowInvitationLink(true)
}
const handleCopyInvitationLink = async (link: string) => {
try {
await navigator.clipboard.writeText(link)
setSuccess("邀請連結已複製到剪貼簿!")
setTimeout(() => setSuccess(""), 3000)
} catch (err) {
setError("複製失敗,請手動複製連結")
setTimeout(() => setError(""), 3000)
}
}
const handleRegenerateInvitation = async (userId: string, email: string) => {
setIsLoading(true)
// 模擬重新生成邀請連結
await new Promise((resolve) => setTimeout(resolve, 1500))
const newToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
const user = users.find((u) => u.id === userId)
const role = (user as any)?.invitedRole || "user"
const newInvitationLink = `${window.location.origin}/register?token=${newToken}&email=${encodeURIComponent(email)}&role=${role}`
setUsers(
users.map((user) =>
user.id === userId
? {
...user,
invitationLink: newInvitationLink,
invitationSentAt: new Date().toLocaleString("zh-TW", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}),
}
: user,
),
)
setIsLoading(false)
setSuccess(`${email} 的邀請連結已重新生成!`)
setTimeout(() => setSuccess(""), 3000)
}
const handleUpdateUser = async () => {
setError("")
if (!editUser.name || !editUser.email) {
setError("請填寫所有必填欄位")
return
}
// 檢查電子郵件是否被其他用戶使用
if (users.some((user) => user.email === editUser.email && user.id !== editUser.id)) {
setError("此電子郵件已被其他用戶使用")
return
}
setIsLoading(true)
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1500))
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
setIsLoading(false)
setShowEditUser(false)
setSuccess("用戶資料更新成功!")
setTimeout(() => setSuccess(""), 3000)
}
const confirmDeleteUser = async () => {
if (!userToDelete) return
setIsLoading(true)
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1500))
setUsers(users.filter((user) => user.id !== userToDelete.id))
setIsLoading(false)
setShowDeleteConfirm(false)
setUserToDelete(null)
setSuccess("用戶刪除成功!")
setTimeout(() => setSuccess(""), 3000)
}
const getRoleColor = (role: string) => {
switch (role) {
case "admin":
return "bg-purple-100 text-purple-800 border-purple-200"
case "developer":
return "bg-green-100 text-green-800 border-green-200"
case "user":
return "bg-blue-100 text-blue-800 border-blue-200"
default:
return "bg-gray-100 text-gray-800 border-gray-200"
}
}
const getStatusColor = (status: string) => {
switch (status) {
case "active":
return "bg-green-100 text-green-800 border-green-200"
case "inactive":
return "bg-gray-100 text-gray-800 border-gray-200"
case "invited":
return "bg-yellow-100 text-yellow-800 border-yellow-200"
default:
return "bg-gray-100 text-gray-800 border-gray-200"
}
}
const getStatusText = (status: string) => {
switch (status) {
case "active":
return "活躍"
case "inactive":
return "非活躍"
case "invited":
return "已邀請"
default:
return status
}
}
const getRoleText = (role: string) => {
switch (role) {
case "admin":
return "管理員"
case "developer":
return "開發者"
case "user":
return "一般用戶"
default:
return "待設定"
}
}
const getRoleIcon = (role: string) => {
switch (role) {
case "admin":
return <Shield className="w-3 h-3" />
case "developer":
return <Code className="w-3 h-3" />
case "user":
return <User className="w-3 h-3" />
default:
return <User className="w-3 h-3" />
}
}
return (
<div className="space-y-6">
{/* Success/Error Messages */}
{success && (
<Alert className="border-green-200 bg-green-50">
<CheckCircle className="h-4 w-4 text-green-600" />
<AlertDescription className="text-green-800">{success}</AlertDescription>
</Alert>
)}
{error && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900"></h1>
<p className="text-gray-600"></p>
</div>
<Button
onClick={() => setShowInviteUser(true)}
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
>
<UserPlus className="w-4 h-4 mr-2" />
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{users.length}</p>
</div>
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="w-4 h-4 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{users.filter((u) => u.status === "active").length}</p>
</div>
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<Activity className="w-4 h-4 text-green-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{users.filter((u) => u.role === "admin").length}</p>
</div>
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
<Shield className="w-4 h-4 text-purple-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{users.filter((u) => u.role === "developer").length}</p>
</div>
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<Code className="w-4 h-4 text-green-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{users.filter((u) => u.status === "invited").length}</p>
</div>
<div className="w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center">
<Clock className="w-4 h-4 text-yellow-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">2</p>
</div>
<div className="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center">
<UserPlus className="w-4 h-4 text-orange-600" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Filters */}
<Card>
<CardContent className="p-4">
<div className="flex flex-col lg:flex-row gap-4 items-center">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="搜尋用戶姓名或電子郵件..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex gap-3">
<Select value={selectedDepartment} onValueChange={setSelectedDepartment}>
<SelectTrigger className="w-32">
<SelectValue placeholder="部門" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="HQBU">HQBU</SelectItem>
<SelectItem value="ITBU">ITBU</SelectItem>
<SelectItem value="MBU1">MBU1</SelectItem>
<SelectItem value="SBU">SBU</SelectItem>
</SelectContent>
</Select>
<Select value={selectedRole} onValueChange={setSelectedRole}>
<SelectTrigger className="w-32">
<SelectValue placeholder="角色" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="admin"></SelectItem>
<SelectItem value="developer"></SelectItem>
<SelectItem value="user"></SelectItem>
</SelectContent>
</Select>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-32">
<SelectValue placeholder="狀態" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="active"></SelectItem>
<SelectItem value="inactive"></SelectItem>
<SelectItem value="invited"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
{/* Users Table */}
<Card>
<CardHeader>
<CardTitle> ({filteredUsers.length})</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.map((user) => (
<TableRow key={user.id}>
<TableCell>
<div className="flex items-center space-x-3">
<Avatar className="w-8 h-8">
<AvatarFallback className="bg-gradient-to-r from-blue-600 to-purple-600 text-white text-sm">
{user.name ? user.name.charAt(0) : user.email.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{user.name || "待註冊"}</p>
<p className="text-sm text-gray-500">{user.email}</p>
{user.status === "invited" && user.invitationSentAt && (
<p className="text-xs text-yellow-600">{user.invitationSentAt}</p>
)}
</div>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="bg-gray-100 text-gray-700">
{user.department || "待設定"}
</Badge>
</TableCell>
<TableCell>
<Badge variant="outline" className={getRoleColor(user.role || (user as any).invitedRole)}>
<div className="flex items-center space-x-1">
{getRoleIcon(user.role || (user as any).invitedRole)}
<span>{getRoleText(user.role || (user as any).invitedRole)}</span>
</div>
</Badge>
</TableCell>
<TableCell>
<Badge variant="outline" className={getStatusColor(user.status)}>
{getStatusText(user.status)}
</Badge>
</TableCell>
<TableCell className="text-sm text-gray-600">{user.joinDate || "-"}</TableCell>
<TableCell className="text-sm text-gray-600">{user.lastLogin || "-"}</TableCell>
<TableCell>
<div className="text-sm">
<p>{user.totalApps} </p>
<p className="text-gray-500">{user.totalReviews} </p>
</div>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" disabled={isLoading}>
<MoreHorizontal className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleViewUser(user)}>
<Eye className="w-4 h-4 mr-2" />
</DropdownMenuItem>
{user.status !== "invited" && (
<DropdownMenuItem onClick={() => handleEditUser(user)}>
<Edit className="w-4 h-4 mr-2" />
</DropdownMenuItem>
)}
{user.status === "invited" && user.invitationLink && (
<>
<DropdownMenuItem onClick={() => handleCopyInvitationLink(user.invitationLink)}>
<Copy className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleRegenerateInvitation(user.id, user.email)}>
<RefreshCw className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</>
)}
{user.status !== "invited" && user.role && (
<DropdownMenuItem onClick={() => handleToggleUserStatus(user.id)}>
<Activity className="w-4 h-4 mr-2" />
{user.status === "active" ? "停用用戶" : "啟用用戶"}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => handleDeleteUser(user)} className="text-red-600">
<Trash2 className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Invite User Dialog - 包含角色選擇 */}
<Dialog open={showInviteUser} onOpenChange={setShowInviteUser}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email"> *</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
id="email"
type="email"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="請輸入電子郵件"
className="pl-10"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="role"> *</Label>
<Select value={inviteRole} onValueChange={setInviteRole}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">
<div className="flex items-center space-x-2">
<User className="w-4 h-4 text-blue-600" />
<span></span>
</div>
</SelectItem>
<SelectItem value="developer">
<div className="flex items-center space-x-2">
<Code className="w-4 h-4 text-green-600" />
<span></span>
</div>
</SelectItem>
<SelectItem value="admin">
<div className="flex items-center space-x-2">
<Shield className="w-4 h-4 text-purple-600" />
<span></span>
</div>
</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-gray-500"></p>
</div>
<div className="bg-blue-50 p-4 rounded-lg">
<div className="flex items-start space-x-2">
<Link className="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" />
<div className="text-sm text-blue-800">
<p className="font-medium mb-1"></p>
<p>
</p>
</div>
</div>
</div>
<div className="bg-yellow-50 p-4 rounded-lg">
<div className="flex items-start space-x-2">
<AlertTriangle className="w-4 h-4 text-yellow-600 mt-0.5 flex-shrink-0" />
<div className="text-sm text-yellow-800">
<p className="font-medium mb-1"></p>
<ul className="list-disc list-inside space-y-1">
<li>
<strong></strong>
</li>
<li>
<strong></strong>
</li>
<li>
<strong></strong>
</li>
</ul>
</div>
</div>
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowInviteUser(false)} disabled={isLoading}>
</Button>
<Button onClick={handleGenerateInvitation} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Link className="w-4 h-4 mr-2" />
</>
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Invitation Link Dialog */}
<Dialog open={showInvitationLink} onOpenChange={setShowInvitationLink}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<CheckCircle className="w-5 h-5 text-green-600" />
<span></span>
</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{generatedInvitation && (
<div className="space-y-4">
<div className="space-y-2">
<Label></Label>
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<Avatar className="w-10 h-10">
<AvatarFallback className="bg-gradient-to-r from-blue-600 to-purple-600 text-white">
{generatedInvitation.email.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-medium">{generatedInvitation.email}</p>
<div className="flex items-center space-x-2 mt-1">
<Badge variant="outline" className={getRoleColor(generatedInvitation.invitedRole)}>
<div className="flex items-center space-x-1">
{getRoleIcon(generatedInvitation.invitedRole)}
<span>{getRoleText(generatedInvitation.invitedRole)}</span>
</div>
</Badge>
<p className="text-sm text-gray-500">{generatedInvitation.invitationSentAt}</p>
</div>
</div>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex items-center space-x-2">
<Input value={generatedInvitation.invitationLink} readOnly className="font-mono text-sm" />
<Button
size="sm"
onClick={() => handleCopyInvitationLink(generatedInvitation.invitationLink)}
className="flex-shrink-0"
>
<Copy className="w-4 h-4" />
</Button>
</div>
</div>
<div className="bg-yellow-50 p-4 rounded-lg">
<div className="flex items-start space-x-2">
<AlertTriangle className="w-4 h-4 text-yellow-600 mt-0.5 flex-shrink-0" />
<div className="text-sm text-yellow-800">
<p className="font-medium mb-1"></p>
<ul className="list-disc list-inside space-y-1">
<li></li>
<li></li>
<li>{getRoleText(generatedInvitation.invitedRole)}</li>
<li></li>
</ul>
</div>
</div>
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => window.open(generatedInvitation.invitationLink, "_blank")}>
<ExternalLink className="w-4 h-4 mr-2" />
</Button>
<Button onClick={() => setShowInvitationLink(false)}></Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
{/* Edit User Dialog */}
<Dialog open={showEditUser} onOpenChange={setShowEditUser}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="editName"> *</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
id="editName"
value={editUser.name}
onChange={(e) => setEditUser({ ...editUser, name: e.target.value })}
className="pl-10"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="editEmail"> *</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
id="editEmail"
type="email"
value={editUser.email}
onChange={(e) => setEditUser({ ...editUser, email: e.target.value })}
className="pl-10"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="editDepartment"></Label>
<div className="relative">
<Building className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4 z-10" />
<Select
value={editUser.department}
onValueChange={(value) => setEditUser({ ...editUser, department: value })}
>
<SelectTrigger className="pl-10">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="HQBU">HQBU</SelectItem>
<SelectItem value="ITBU">ITBU</SelectItem>
<SelectItem value="MBU1">MBU1</SelectItem>
<SelectItem value="SBU">SBU</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="editRole"></Label>
<Select value={editUser.role} onValueChange={(value) => setEditUser({ ...editUser, role: value })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">
<div className="flex items-center space-x-2">
<User className="w-4 h-4 text-blue-600" />
<span></span>
</div>
</SelectItem>
<SelectItem value="developer">
<div className="flex items-center space-x-2">
<Code className="w-4 h-4 text-green-600" />
<span></span>
</div>
</SelectItem>
<SelectItem value="admin">
<div className="flex items-center space-x-2">
<Shield className="w-4 h-4 text-purple-600" />
<span></span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="editStatus"></Label>
<Select value={editUser.status} onValueChange={(value) => setEditUser({ ...editUser, status: value })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active"></SelectItem>
<SelectItem value="inactive"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowEditUser(false)} disabled={isLoading}>
</Button>
<Button onClick={handleUpdateUser} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
"更新用戶"
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="text-red-600"></DialogTitle>
<DialogDescription>
{userToDelete?.name || userToDelete?.email}
</DialogDescription>
</DialogHeader>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowDeleteConfirm(false)} disabled={isLoading}>
</Button>
<Button variant="destructive" onClick={confirmDeleteUser} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
"確認刪除"
)}
</Button>
</div>
</DialogContent>
</Dialog>
{/* User Detail Dialog */}
<Dialog open={showUserDetail} onOpenChange={setShowUserDetail}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedUser && (
<Tabs defaultValue="info" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="info"></TabsTrigger>
<TabsTrigger value="activity"></TabsTrigger>
<TabsTrigger value="stats"></TabsTrigger>
</TabsList>
<TabsContent value="info" className="space-y-4">
<div className="flex items-center space-x-4">
<Avatar className="w-16 h-16">
<AvatarFallback className="bg-gradient-to-r from-blue-600 to-purple-600 text-white text-xl">
{selectedUser.name ? selectedUser.name.charAt(0) : selectedUser.email.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<h3 className="text-xl font-semibold">{selectedUser.name || "待註冊用戶"}</h3>
<p className="text-gray-600">{selectedUser.email}</p>
<div className="flex items-center space-x-2 mt-2">
<Badge
variant="outline"
className={getRoleColor(selectedUser.role || (selectedUser as any).invitedRole)}
>
<div className="flex items-center space-x-1">
{getRoleIcon(selectedUser.role || (selectedUser as any).invitedRole)}
<span>{getRoleText(selectedUser.role || (selectedUser as any).invitedRole)}</span>
</div>
</Badge>
<Badge variant="outline" className={getStatusColor(selectedUser.status)}>
{getStatusText(selectedUser.status)}
</Badge>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">{selectedUser.department || "待設定"}</p>
</div>
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">{selectedUser.joinDate || "尚未註冊"}</p>
</div>
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">{selectedUser.lastLogin || "尚未登入"}</p>
</div>
<div>
<p className="text-sm text-gray-500">ID</p>
<p className="font-medium">{selectedUser.id}</p>
</div>
{selectedUser.status === "invited" && selectedUser.invitationSentAt && (
<>
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">{selectedUser.invitationSentAt}</p>
</div>
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium text-yellow-600"></p>
</div>
</>
)}
</div>
{selectedUser.status === "invited" && selectedUser.invitationLink && (
<div className="space-y-2">
<Label></Label>
<div className="flex items-center space-x-2">
<Input value={selectedUser.invitationLink} readOnly className="font-mono text-sm" />
<Button
size="sm"
onClick={() => handleCopyInvitationLink(selectedUser.invitationLink)}
className="flex-shrink-0"
>
<Copy className="w-4 h-4" />
</Button>
</div>
</div>
)}
</TabsContent>
<TabsContent value="activity" className="space-y-4">
{selectedUser.status === "invited" ? (
<div className="text-center py-8">
<Clock className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500"></p>
</div>
) : (
<div className="space-y-3">
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<Calendar className="w-4 h-4 text-blue-600" />
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-gray-500">2024-01-20 16:45</p>
</div>
</div>
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<Eye className="w-4 h-4 text-green-600" />
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-gray-500">2024-01-20 15:30</p>
</div>
</div>
</div>
)}
</TabsContent>
<TabsContent value="stats" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">{selectedUser.totalApps}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-green-600">{selectedUser.totalReviews}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-purple-600">{selectedUser.totalLikes}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-orange-600">
{selectedUser.status === "invited" ? 0 : 15}
</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
)}
</DialogContent>
</Dialog>
</div>
)
}