Files
ai-showcase-platform/components/admin/user-management.tsx

1572 lines
58 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, useEffect } 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,
Heart,
ThumbsUp,
Star,
Plus,
ExternalLink,
Code,
Users,
ChevronLeft,
ChevronRight,
Filter,
X,
} from "lucide-react"
export function UserManagement() {
const [users, setUsers] = useState<any[]>([])
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 [userActivities, setUserActivities] = useState<any[]>([])
const [userStats, setUserStats] = useState<any>({
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
loginDays: 0
})
const [activityPagination, setActivityPagination] = useState({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
const [activityFilters, setActivityFilters] = useState({
search: '',
startDate: '',
endDate: '',
activityType: 'all'
})
const [showUserDetail, setShowUserDetail] = useState(false)
const [showInviteUser, setShowInviteUser] = useState(false)
const [showEditUser, setShowEditUser] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [isClient, setIsClient] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [stats, setStats] = useState({
totalUsers: 0,
activeUsers: 0,
adminCount: 0,
developerCount: 0,
inactiveUsers: 0,
newThisMonth: 0
})
const [pagination, setPagination] = useState({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
// Set client state after hydration
useEffect(() => {
setIsClient(true)
}, [])
// 載入用戶統計數據
const loadUserStats = async (userId: string) => {
try {
const response = await fetch(`/api/admin/users/${userId}/stats`)
const data = await response.json()
if (data.success) {
setUserStats(data.data)
} else {
console.error('載入用戶統計數據失敗:', data.error)
setUserStats({
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
loginDays: 0
})
}
} catch (error) {
console.error('載入用戶統計數據錯誤:', error)
setUserStats({
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
loginDays: 0
})
}
}
// 載入用戶活動記錄
const loadUserActivities = async (userId: string, page: number = 1, filters: any = {}) => {
try {
const params = new URLSearchParams({
page: page.toString(),
limit: activityPagination.limit.toString(),
search: filters.search || '',
startDate: filters.startDate || '',
endDate: filters.endDate || '',
activityType: filters.activityType || 'all'
})
const response = await fetch(`/api/admin/users/${userId}/activities?${params}`)
const data = await response.json()
if (data.success) {
setUserActivities(data.data.activities)
setActivityPagination(data.data.pagination)
} else {
console.error('載入活動記錄失敗:', data.error)
setUserActivities([])
setActivityPagination({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
}
} catch (error) {
console.error('載入活動記錄錯誤:', error)
setUserActivities([])
setActivityPagination({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
}
}
// 載入用戶數據
const loadUsers = async () => {
if (!isClient) return
setIsLoading(true)
try {
const params = new URLSearchParams({
page: pagination.page.toString(),
limit: pagination.limit.toString(),
search: searchTerm,
department: selectedDepartment,
role: selectedRole,
status: selectedStatus
})
const response = await fetch(`/api/admin/users?${params}`)
const data = await response.json()
if (data.success) {
setUsers(data.data.users)
setStats(data.data.stats)
setPagination(data.data.pagination)
} else {
console.error('載入用戶失敗:', data.error)
}
} catch (error) {
console.error('載入用戶錯誤:', error)
} finally {
setIsLoading(false)
}
}
// 當篩選條件改變時重新載入
useEffect(() => {
loadUsers()
}, [isClient, searchTerm, selectedDepartment, selectedRole, selectedStatus, pagination.page])
const [showInvitationLink, setShowInvitationLink] = useState(false)
const [userToDelete, setUserToDelete] = useState<any>(null)
const [generatedInvitation, setGeneratedInvitation] = useState<any>(null)
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: "",
})
// 篩選現在由 API 處理,不需要前端篩選
const handleViewUser = (user: any) => {
setSelectedUser(user)
setShowUserDetail(true)
// 重置活動記錄狀態
setActivityPagination({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
setActivityFilters({
search: '',
startDate: '',
endDate: '',
activityType: 'all'
})
// 重置統計數據狀態
setUserStats({
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
loginDays: 0
})
// 載入用戶統計數據和活動記錄
loadUserStats(user.id)
loadUserActivities(user.id, 1, {
search: '',
startDate: '',
endDate: '',
activityType: 'all'
})
}
// 處理活動記錄篩選
const handleActivityFilter = (newFilters: any) => {
const updatedFilters = { ...activityFilters, ...newFilters }
setActivityFilters(updatedFilters)
if (selectedUser) {
loadUserActivities(selectedUser.id, 1, updatedFilters)
}
}
// 處理活動記錄分頁
const handleActivityPageChange = (page: number) => {
setActivityPagination(prev => ({ ...prev, page }))
if (selectedUser) {
loadUserActivities(selectedUser.id, page, activityFilters)
}
}
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)
try {
// 找到要切換狀態的用戶
const user = users.find(u => u.id === userId)
if (!user) {
setError("用戶不存在")
setIsLoading(false)
return
}
const newStatus = user.status === "active" ? "inactive" : "active"
const response = await fetch(`/api/admin/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: user.name,
department: user.department,
role: user.role,
status: newStatus
})
})
const data = await response.json()
if (data.success) {
// 更新本地用戶列表
setUsers(
users.map((u) =>
u.id === userId ? { ...u, status: newStatus } : u
)
)
setSuccess("用戶狀態更新成功!")
setTimeout(() => setSuccess(""), 3000)
} else {
setError(data.error || "更新用戶狀態失敗")
}
} catch (error) {
console.error('更新用戶狀態錯誤:', error)
setError("更新用戶狀態時發生錯誤")
}
setIsLoading(false)
}
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)
try {
const response = await fetch('/api/admin/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: inviteEmail,
role: inviteRole
})
})
const data = await response.json()
if (data.success) {
const newInvitation = {
id: data.data.user.id,
name: data.data.user.name || "",
email: data.data.user.email,
department: data.data.user.department || "",
role: data.data.user.role,
status: "invited",
joinDate: data.data.user.join_date ? new Date(data.data.user.join_date).toLocaleDateString('zh-TW') : "",
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: data.data.invitationLink,
invitedRole: inviteRole, // 記錄邀請時的預設角色
}
setGeneratedInvitation(newInvitation)
setInviteEmail("")
setInviteRole("user")
setShowInviteUser(false)
setShowInvitationLink(true)
setSuccess("邀請連結已生成")
// 重新載入用戶列表
loadUsers()
} else {
setError(data.error || "生成邀請連結失敗")
}
} catch (error) {
console.error('邀請用戶錯誤:', error)
setError("生成邀請連結時發生錯誤")
} finally {
setIsLoading(false)
}
}
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 = isClient
? `${window.location.origin}/register?token=${newToken}&email=${encodeURIComponent(email)}&role=${role}`
: `/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)
try {
const response = await fetch(`/api/admin/users/${editUser.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: editUser.name,
department: editUser.department,
role: editUser.role,
status: editUser.status
})
})
const data = await response.json()
if (data.success) {
// 更新本地用戶列表
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
setShowEditUser(false)
setSuccess("用戶資料更新成功!")
setTimeout(() => setSuccess(""), 3000)
} else {
setError(data.error || "更新用戶失敗")
}
} catch (error) {
console.error('更新用戶錯誤:', error)
setError("更新用戶時發生錯誤")
}
setIsLoading(false)
}
const confirmDeleteUser = async () => {
if (!userToDelete) return
setIsLoading(true)
try {
const response = await fetch(`/api/admin/users/${userToDelete.id}`, {
method: 'DELETE'
})
const data = await response.json()
if (data.success) {
// 從本地用戶列表中移除
setUsers(users.filter((user) => user.id !== userToDelete.id))
setShowDeleteConfirm(false)
setUserToDelete(null)
setSuccess("用戶刪除成功!")
setTimeout(() => setSuccess(""), 3000)
} else {
setError(data.error || "刪除用戶失敗")
}
} catch (error) {
console.error('刪除用戶錯誤:', error)
setError("刪除用戶時發生錯誤")
}
setIsLoading(false)
}
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 getActivityIcon = (iconName: string) => {
switch (iconName) {
case 'Calendar':
return <Calendar className="w-4 h-4" />
case 'Heart':
return <Heart className="w-4 h-4" />
case 'ThumbsUp':
return <ThumbsUp className="w-4 h-4" />
case 'Eye':
return <Eye className="w-4 h-4" />
case 'Star':
return <Star className="w-4 h-4" />
case 'Plus':
return <Plus className="w-4 h-4" />
default:
return <Calendar className="w-4 h-4" />
}
}
// 獲取活動顏色
const getActivityColor = (color: string) => {
switch (color) {
case 'blue':
return 'text-blue-600'
case 'red':
return 'text-red-600'
case 'green':
return 'text-green-600'
case 'purple':
return 'text-purple-600'
case 'yellow':
return 'text-yellow-600'
default:
return 'text-gray-600'
}
}
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">{stats.totalUsers}</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">{stats.activeUsers}</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">{stats.adminCount}</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">{stats.developerCount}</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">{stats.inactiveUsers}</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">{stats.newThisMonth}</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> ({pagination.total})</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>
{users.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={() => isClient && 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 flex-wrap items-end justify-between gap-3">
<div className="flex flex-wrap gap-3 items-end">
<div className="flex-1 min-w-[180px]">
<Label htmlFor="activity-search" className="text-xs"></Label>
<Input
id="activity-search"
placeholder="搜尋活動內容..."
value={activityFilters.search}
onChange={(e) => handleActivityFilter({ search: e.target.value })}
className="mt-1 h-8 text-sm"
/>
</div>
<div className="min-w-[100px]">
<Label htmlFor="activity-type" className="text-xs"></Label>
<Select
value={activityFilters.activityType}
onValueChange={(value) => handleActivityFilter({ activityType: value })}
>
<SelectTrigger className="mt-1 h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="login"></SelectItem>
<SelectItem value="favorite"></SelectItem>
<SelectItem value="like"></SelectItem>
<SelectItem value="view"></SelectItem>
<SelectItem value="rating"></SelectItem>
<SelectItem value="create"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="min-w-[120px]">
<Label htmlFor="start-date" className="text-xs"></Label>
<Input
id="start-date"
type="date"
value={activityFilters.startDate}
onChange={(e) => handleActivityFilter({ startDate: e.target.value })}
className="mt-1 h-8 text-sm"
/>
</div>
<div className="min-w-[120px]">
<Label htmlFor="end-date" className="text-xs"></Label>
<Input
id="end-date"
type="date"
value={activityFilters.endDate}
onChange={(e) => handleActivityFilter({ endDate: e.target.value })}
className="mt-1 h-8 text-sm"
/>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handleActivityFilter({
search: '',
startDate: '',
endDate: '',
activityType: 'all'
})}
className="h-8 px-3 text-xs"
>
<X className="w-3 h-3 mr-1" />
</Button>
</div>
</div>
{/* 活動記錄列表 */}
{userActivities.length === 0 ? (
<div className="text-center py-8">
<Calendar className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500"></p>
</div>
) : (
<>
<div className="space-y-3">
{userActivities.map((activity, index) => (
<div key={index} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<div className={`${getActivityColor(activity.color)}`}>
{getActivityIcon(activity.icon)}
</div>
<div className="flex-1">
<p className="text-sm font-medium">{activity.action_text}</p>
<p className="text-xs text-gray-500">
{new Date(activity.created_at).toLocaleString('zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
</div>
))}
</div>
{/* 分頁控件 */}
{activityPagination.totalPages > 1 && (
<div className="flex items-center justify-between pt-4">
<div className="text-sm text-gray-500">
{((activityPagination.page - 1) * activityPagination.limit) + 1} - {Math.min(activityPagination.page * activityPagination.limit, activityPagination.total)} {activityPagination.total}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleActivityPageChange(activityPagination.page - 1)}
disabled={activityPagination.page <= 1}
>
<ChevronLeft className="w-4 h-4" />
</Button>
<span className="text-sm">
{activityPagination.page} {activityPagination.totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => handleActivityPageChange(activityPagination.page + 1)}
disabled={activityPagination.page >= activityPagination.totalPages}
>
<ChevronRight className="w-4 h-4" />
</Button>
</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">{userStats.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">{userStats.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">{userStats.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">{userStats.loginDays}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
)}
</DialogContent>
</Dialog>
</div>
)
}