實作管理者用戶管理、邀請註冊功能
This commit is contained in:
@@ -33,9 +33,17 @@ import {
|
||||
RefreshCw,
|
||||
Copy,
|
||||
Link,
|
||||
Heart,
|
||||
ThumbsUp,
|
||||
Star,
|
||||
Plus,
|
||||
ExternalLink,
|
||||
Code,
|
||||
Users,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Filter,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
|
||||
export function UserManagement() {
|
||||
@@ -45,6 +53,25 @@ export function UserManagement() {
|
||||
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)
|
||||
@@ -71,6 +98,74 @@ export function UserManagement() {
|
||||
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
|
||||
@@ -133,6 +228,51 @@ export function UserManagement() {
|
||||
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) => {
|
||||
@@ -155,18 +295,51 @@ export function UserManagement() {
|
||||
const handleToggleUserStatus = async (userId: string) => {
|
||||
setIsLoading(true)
|
||||
|
||||
// 模擬 API 調用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
try {
|
||||
// 找到要切換狀態的用戶
|
||||
const user = users.find(u => u.id === userId)
|
||||
if (!user) {
|
||||
setError("用戶不存在")
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setUsers(
|
||||
users.map((user) =>
|
||||
user.id === userId ? { ...user, status: user.status === "active" ? "inactive" : "active" } : user,
|
||||
),
|
||||
)
|
||||
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)
|
||||
setSuccess("用戶狀態更新成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
|
||||
const handleChangeUserRole = async (userId: string, newRole: string) => {
|
||||
@@ -222,13 +395,13 @@ export function UserManagement() {
|
||||
|
||||
if (data.success) {
|
||||
const newInvitation = {
|
||||
id: Date.now().toString(),
|
||||
name: "",
|
||||
email: inviteEmail,
|
||||
department: "",
|
||||
role: "",
|
||||
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: "",
|
||||
joinDate: data.data.user.join_date ? new Date(data.data.user.join_date).toLocaleDateString('zh-TW') : "",
|
||||
lastLogin: "",
|
||||
totalApps: 0,
|
||||
totalReviews: 0,
|
||||
@@ -327,15 +500,38 @@ export function UserManagement() {
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
// 模擬 API 調用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
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
|
||||
})
|
||||
})
|
||||
|
||||
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
|
||||
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)
|
||||
setShowEditUser(false)
|
||||
setSuccess("用戶資料更新成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
|
||||
const confirmDeleteUser = async () => {
|
||||
@@ -343,16 +539,30 @@ export function UserManagement() {
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
// 模擬 API 調用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
try {
|
||||
const response = await fetch(`/api/admin/users/${userToDelete.id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
setUsers(users.filter((user) => user.id !== userToDelete.id))
|
||||
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)
|
||||
setShowDeleteConfirm(false)
|
||||
setUserToDelete(null)
|
||||
setSuccess("用戶刪除成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
|
||||
const getRoleColor = (role: string) => {
|
||||
@@ -407,6 +617,44 @@ export function UserManagement() {
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取活動圖標
|
||||
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":
|
||||
@@ -1140,22 +1388,141 @@ export function UserManagement() {
|
||||
<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 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>
|
||||
<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>
|
||||
|
||||
{/* 活動記錄列表 */}
|
||||
{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>
|
||||
</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>
|
||||
|
||||
@@ -1164,7 +1531,7 @@ export function UserManagement() {
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-blue-600">{selectedUser.totalApps}</p>
|
||||
<p className="text-2xl font-bold text-blue-600">{userStats.totalApps}</p>
|
||||
<p className="text-sm text-gray-600">創建應用</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -1172,7 +1539,7 @@ export function UserManagement() {
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-green-600">{selectedUser.totalReviews}</p>
|
||||
<p className="text-2xl font-bold text-green-600">{userStats.totalReviews}</p>
|
||||
<p className="text-sm text-gray-600">撰寫評價</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -1180,7 +1547,7 @@ export function UserManagement() {
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-purple-600">{selectedUser.totalLikes}</p>
|
||||
<p className="text-2xl font-bold text-purple-600">{userStats.totalLikes}</p>
|
||||
<p className="text-sm text-gray-600">獲得讚數</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -1188,9 +1555,7 @@ export function UserManagement() {
|
||||
<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-2xl font-bold text-orange-600">{userStats.loginDays}</p>
|
||||
<p className="text-sm text-gray-600">登入天數</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
Reference in New Issue
Block a user