整合資料庫、完成登入註冊忘記密碼功能

This commit is contained in:
2025-09-09 12:00:22 +08:00
parent af88c0f037
commit 32b19e9a0f
85 changed files with 11672 additions and 2350 deletions

View File

@@ -77,17 +77,27 @@ const mockNotifications: Notification[] = []
const mockSearchData: SearchResult[] = []
export function AdminLayout({ children, currentPage, onPageChange }: AdminLayoutProps) {
const { user, logout, isLoading } = useAuth()
// Move ALL hooks to the top, before any conditional logic
const { user, logout, isLoading, isInitialized } = useAuth()
const [sidebarOpen, setSidebarOpen] = useState(true)
const [isClient, setIsClient] = useState(false)
// Search state
const [searchQuery, setSearchQuery] = useState("")
const [searchResults, setSearchResults] = useState<SearchResult[]>([])
const [showSearchResults, setShowSearchResults] = useState(false)
// Notification state
const [notifications, setNotifications] = useState<Notification[]>(mockNotifications)
const [showNotifications, setShowNotifications] = useState(false)
// Logout confirmation state
const [showLogoutDialog, setShowLogoutDialog] = useState(false)
// Set client state after hydration
useEffect(() => {
setIsClient(true)
}, [])
// Handle search
useEffect(() => {
if (searchQuery.trim()) {
@@ -104,46 +114,6 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
}
}, [searchQuery])
// 認證檢查 - moved after all hooks
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span className="text-gray-600">...</span>
</div>
</div>
)
}
if (!user) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4"></h1>
<p className="text-gray-600 mb-6"></p>
<Button onClick={() => window.location.href = '/'}>
</Button>
</div>
</div>
)
}
if (user.role !== 'admin') {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4"></h1>
<p className="text-gray-600 mb-6"></p>
<Button onClick={() => window.location.href = '/'}>
</Button>
</div>
</div>
)
}
// Get unread notification count
const unreadCount = notifications.filter((n) => !n.read).length
@@ -207,13 +177,15 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
setShowLogoutDialog(false)
// Check if this is a popup/new tab opened from main site
if (typeof window !== 'undefined' && window.opener && !window.opener.closed) {
// If opened from another window, close this tab and focus parent
window.opener.focus()
window.close()
} else {
// If this is the main window or standalone, redirect to homepage
window.location.href = "/"
if (isClient) {
if (window.opener && !window.opener.closed) {
// If opened from another window, close this tab and focus parent
window.opener.focus()
window.close()
} else {
// If this is the main window or standalone, redirect to homepage
window.location.href = "/"
}
}
}
@@ -236,6 +208,19 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
}
}
// 如果還在載入中,顯示載入畫面
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
<p className="text-gray-600">...</p>
</div>
</div>
)
}
// 檢查用戶權限
if (!user || user.role !== "admin") {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
@@ -246,11 +231,20 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
<div>
<h2 className="text-2xl font-bold text-gray-900 mb-2"></h2>
<p className="text-gray-600 mb-4"></p>
{process.env.NODE_ENV === 'development' && (
<div className="text-xs text-gray-500 mb-4">
調試信息: 用戶={user ? '已登入' : '未登入'}, ={user?.role || '無'}
</div>
)}
<div className="space-x-3">
<Button onClick={() => (window.location.href = "/")} variant="outline">
<Button onClick={() => {
if (isClient) {
window.location.href = "/"
}
}} variant="outline">
</Button>
{typeof window !== 'undefined' && window.opener && !window.opener.closed && (
{isClient && window.opener && !window.opener.closed && (
<Button
onClick={() => {
window.opener.focus()

View File

@@ -15,7 +15,7 @@ export function AdminPanel() {
const renderPage = () => {
switch (currentPage) {
case "dashboard":
return <AdminDashboard onPageChange={setCurrentPage} />
return <AdminDashboard />
case "users":
return <UserManagement />
case "apps":
@@ -27,7 +27,7 @@ export function AdminPanel() {
case "settings":
return <SystemSettings />
default:
return <AdminDashboard onPageChange={setCurrentPage} />
return <AdminDashboard />
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,19 +20,9 @@ const recentActivities: any[] = []
const topApps: any[] = []
interface AdminDashboardProps {
onPageChange?: (page: string) => void
}
export function AdminDashboard({ onPageChange }: AdminDashboardProps) {
export function AdminDashboard() {
const { competitions } = useCompetition()
const handleManageUsers = () => {
if (onPageChange) {
onPageChange("users")
}
}
return (
<div className="space-y-6">
{/* Welcome Section */}
@@ -160,7 +150,7 @@ export function AdminDashboard({ onPageChange }: AdminDashboardProps) {
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Button className="h-20 flex flex-col space-y-2" onClick={handleManageUsers}>
<Button className="h-20 flex flex-col space-y-2">
<Users className="w-6 h-6" />
<span></span>
</Button>

View File

@@ -18,6 +18,9 @@ import {
Users,
Bell,
Save,
Eye,
EyeOff,
Lock,
TestTube,
CheckCircle,
HardDrive,
@@ -67,6 +70,7 @@ export function SystemSettings() {
const [activeTab, setActiveTab] = useState("general")
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle")
const [showSmtpPassword, setShowSmtpPassword] = useState(false)
const handleSave = async () => {
setSaveStatus("saving")
@@ -78,8 +82,8 @@ export function SystemSettings() {
}
const handleTestEmail = () => {
// 測試郵件功能 - 僅用於開發測試
console.log("測試郵件功能")
// 測試郵件功能
alert("測試郵件已發送!")
}
const updateSetting = (key: string, value: any) => {
@@ -286,12 +290,23 @@ export function SystemSettings() {
</div>
<div className="space-y-2">
<Label htmlFor="smtpPassword">SMTP </Label>
<Input
id="smtpPassword"
type="password"
value={settings.smtpPassword}
onChange={(e) => updateSetting("smtpPassword", e.target.value)}
/>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
id="smtpPassword"
type={showSmtpPassword ? "text" : "password"}
value={settings.smtpPassword}
onChange={(e) => updateSetting("smtpPassword", e.target.value)}
className="pl-10 pr-10"
/>
<button
type="button"
onClick={() => setShowSmtpPassword(!showSmtpPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showSmtpPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
</div>

View File

@@ -13,7 +13,6 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination"
import {
Search,
MoreHorizontal,
@@ -39,11 +38,8 @@ import {
Users,
} from "lucide-react"
// User data - empty for production
const initialMockUsers: any[] = []
export function UserManagement() {
const [users, setUsers] = useState(initialMockUsers)
const [users, setUsers] = useState<any[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [selectedDepartment, setSelectedDepartment] = useState("all")
const [selectedRole, setSelectedRole] = useState("all")
@@ -53,94 +49,70 @@ export function UserManagement() {
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 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 [isLoading, setIsLoading] = useState(false)
const [success, setSuccess] = useState("")
const [error, setError] = useState("")
const [stats, setStats] = useState({
total: 0,
admin: 0,
developer: 0,
user: 0,
today: 0,
totalApps: 0,
totalReviews: 0
})
// Pagination state
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [totalUsers, setTotalUsers] = useState(0)
const [itemsPerPage] = useState(10) // Default to 10 items per page
// 載入用戶資料
useEffect(() => {
const fetchUsers = async () => {
try {
setIsLoading(true)
// 獲取用戶列表 with pagination
const usersResponse = await fetch(`/api/users?page=${currentPage}&limit=${itemsPerPage}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (usersResponse.ok) {
const usersData = await usersResponse.json()
setUsers(usersData.users || [])
setTotalPages(usersData.pagination?.totalPages || 1)
setTotalUsers(usersData.pagination?.total || 0)
} else {
const errorData = await usersResponse.json().catch(() => ({}))
console.error('獲取用戶列表失敗:', errorData.error || usersResponse.statusText)
setError(errorData.error || '獲取用戶列表失敗')
}
// 獲取統計資料
const statsResponse = await fetch('/api/users/stats', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (statsResponse.ok) {
const statsData = await statsResponse.json()
setStats(statsData)
} else {
const errorData = await statsResponse.json().catch(() => ({}))
console.error('獲取統計資料失敗:', errorData.error || statsResponse.statusText)
}
} catch (error) {
console.error('載入用戶資料失敗:', error)
setError('載入用戶資料失敗')
} finally {
setIsLoading(false)
}
}
fetchUsers()
}, [currentPage, itemsPerPage]) // Re-fetch when page changes
// 重新獲取統計數據的函數
const refreshStats = async () => {
try {
const statsResponse = await fetch('/api/users/stats', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (statsResponse.ok) {
const statsData = await statsResponse.json()
setStats(statsData)
}
} catch (error) {
console.error('重新獲取統計資料失敗:', error)
}
}
// 邀請用戶表單狀態 - 包含電子郵件和預設角色
const [inviteEmail, setInviteEmail] = useState("")
@@ -156,65 +128,11 @@ export function UserManagement() {
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
// 篩選現在由 API 處理,不需要前端篩選
return matchesSearch && matchesDepartment && matchesRole && matchesStatus
})
const handleViewUser = async (user: any) => {
setIsLoading(true)
try {
const response = await fetch(`/api/users/${user.id}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (response.ok) {
const userData = await response.json()
// 獲取用戶活動記錄
const activityResponse = await fetch(`/api/users/${user.id}/activity`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
let activityData = []
if (activityResponse.ok) {
activityData = await activityResponse.json()
}
// 合併用戶資料和活動記錄
const userWithActivity = {
...userData,
activities: activityData
}
setSelectedUser(userWithActivity)
setShowUserDetail(true)
} else {
const errorData = await response.json()
setError(errorData.error || "獲取用戶詳情失敗")
setTimeout(() => setError(""), 3000)
}
} catch (error) {
console.error('Error fetching user details:', error)
setError("獲取用戶詳情失敗")
setTimeout(() => setError(""), 3000)
} finally {
setIsLoading(false)
}
const handleViewUser = (user: any) => {
setSelectedUser(user)
setShowUserDetail(true)
}
const handleEditUser = (user: any) => {
@@ -237,39 +155,18 @@ export function UserManagement() {
const handleToggleUserStatus = async (userId: string) => {
setIsLoading(true)
try {
const newStatus = users.find(user => user.id === userId)?.status === "active" ? "inactive" : "active"
const response = await fetch(`/api/users/${userId}/status`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ status: newStatus })
})
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1000))
if (response.ok) {
setUsers(
users.map((user) =>
user.id === userId ? { ...user, status: newStatus } : user,
),
)
setSuccess("用戶狀態更新成功!")
setTimeout(() => setSuccess(""), 3000)
refreshStats() // 更新統計數據
} else {
const errorData = await response.json()
setError(errorData.error || "更新用戶狀態失敗")
setTimeout(() => setError(""), 3000)
}
} catch (error) {
console.error('Error updating user status:', error)
setError("更新用戶狀態失敗")
setTimeout(() => setError(""), 3000)
} finally {
setIsLoading(false)
}
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) => {
@@ -283,7 +180,6 @@ export function UserManagement() {
setIsLoading(false)
setSuccess(`用戶權限已更新為${getRoleText(newRole)}`)
setTimeout(() => setSuccess(""), 3000)
refreshStats() // 更新統計數據
}
const handleGenerateInvitation = async () => {
@@ -310,43 +206,62 @@ export function UserManagement() {
setIsLoading(true)
// 模擬生成邀請連結
await new Promise((resolve) => setTimeout(resolve, 1500))
try {
const response = await fetch('/api/admin/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: inviteEmail,
role: inviteRole
})
})
// 生成邀請 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 data = await response.json()
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, // 記錄邀請時的預設角色
if (data.success) {
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: 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)
}
setUsers([...users, newInvitation])
setGeneratedInvitation(newInvitation)
setInviteEmail("")
setInviteRole("user")
setIsLoading(false)
setShowInviteUser(false)
setShowInvitationLink(true)
}
const handleCopyInvitationLink = async (link: string) => {
@@ -369,7 +284,9 @@ export function UserManagement() {
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}`
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) =>
@@ -402,42 +319,23 @@ export function UserManagement() {
return
}
// 檢查電子郵件是否被其他用戶使用
if (users.some((user) => user.email === editUser.email && user.id !== editUser.id)) {
setError("此電子郵件已被其他用戶使用")
return
}
setIsLoading(true)
try {
const response = await fetch(`/api/users/${editUser.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
name: editUser.name,
email: editUser.email,
department: editUser.department,
role: editUser.role
})
})
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1500))
if (response.ok) {
const result = await response.json()
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
setShowEditUser(false)
setSuccess("用戶資料更新成功!")
setTimeout(() => setSuccess(""), 3000)
refreshStats() // 更新統計數據
} else {
const errorData = await response.json()
setError(errorData.error || "更新用戶資料失敗")
setTimeout(() => setError(""), 3000)
}
} catch (error) {
console.error('Error updating user:', error)
setError("更新用戶資料失敗")
setTimeout(() => setError(""), 3000)
} finally {
setIsLoading(false)
}
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
setIsLoading(false)
setShowEditUser(false)
setSuccess("用戶資料更新成功!")
setTimeout(() => setSuccess(""), 3000)
}
const confirmDeleteUser = async () => {
@@ -445,33 +343,16 @@ export function UserManagement() {
setIsLoading(true)
try {
const response = await fetch(`/api/users/${userToDelete.id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1500))
if (response.ok) {
setUsers(users.filter((user) => user.id !== userToDelete.id))
setShowDeleteConfirm(false)
setUserToDelete(null)
setSuccess("用戶刪除成功!")
setTimeout(() => setSuccess(""), 3000)
refreshStats() // 更新統計數據
} else {
const errorData = await response.json()
setError(errorData.error || "刪除用戶失敗")
setTimeout(() => setError(""), 3000)
}
} catch (error) {
console.error('Error deleting user:', error)
setError("刪除用戶失敗")
setTimeout(() => setError(""), 3000)
} finally {
setIsLoading(false)
}
setUsers(users.filter((user) => user.id !== userToDelete.id))
setIsLoading(false)
setShowDeleteConfirm(false)
setUserToDelete(null)
setSuccess("用戶刪除成功!")
setTimeout(() => setSuccess(""), 3000)
}
const getRoleColor = (role: string) => {
@@ -572,13 +453,13 @@ export function UserManagement() {
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-8 gap-4">
<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.total}</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" />
@@ -592,7 +473,7 @@ export function UserManagement() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.total}</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" />
@@ -606,7 +487,7 @@ export function UserManagement() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.admin}</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" />
@@ -620,7 +501,7 @@ export function UserManagement() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.developer}</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" />
@@ -633,11 +514,11 @@ export function UserManagement() {
<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.user}</p>
<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">
<User className="w-4 h-4 text-yellow-600" />
<Clock className="w-4 h-4 text-yellow-600" />
</div>
</div>
</CardContent>
@@ -647,8 +528,8 @@ export function UserManagement() {
<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.today}</p>
<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" />
@@ -656,34 +537,6 @@ export function UserManagement() {
</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.totalApps}</p>
</div>
<div className="w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
<Code className="w-4 h-4 text-indigo-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.totalReviews}</p>
</div>
<div className="w-8 h-8 bg-pink-100 rounded-full flex items-center justify-center">
<Activity className="w-4 h-4 text-pink-600" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Filters */}
@@ -744,40 +597,25 @@ export function UserManagement() {
{/* Users Table */}
<Card>
<CardHeader>
<CardTitle> ({totalUsers} )</CardTitle>
<CardDescription>
- {currentPage} {totalPages}
{totalPages > 1 && ` (每頁 ${itemsPerPage} 筆)`}
</CardDescription>
<CardTitle> ({pagination.total})</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
<span className="ml-2 text-gray-600">...</span>
</div>
) : filteredUsers.length === 0 ? (
<div className="text-center py-8">
<Users className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600"></p>
<p className="text-sm text-gray-500 mt-1">調</p>
</div>
) : (
<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) => (
<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">
@@ -868,70 +706,9 @@ export function UserManagement() {
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center items-center py-4">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault()
if (currentPage > 1) setCurrentPage(currentPage - 1)
}}
className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
{/* Page numbers */}
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNum
if (totalPages <= 5) {
pageNum = i + 1
} else if (currentPage <= 3) {
pageNum = i + 1
} else if (currentPage >= totalPages - 2) {
pageNum = totalPages - 4 + i
} else {
pageNum = currentPage - 2 + i
}
return (
<PaginationItem key={pageNum}>
<PaginationLink
href="#"
onClick={(e) => {
e.preventDefault()
setCurrentPage(pageNum)
}}
isActive={currentPage === pageNum}
>
{pageNum}
</PaginationLink>
</PaginationItem>
)
})}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault()
if (currentPage < totalPages) setCurrentPage(currentPage + 1)
}}
className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
{/* Invite User Dialog - 包含角色選擇 */}
<Dialog open={showInviteUser} onOpenChange={setShowInviteUser}>
<DialogContent className="max-w-md">
@@ -1106,7 +883,7 @@ export function UserManagement() {
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => window.open(generatedInvitation.invitationLink, "_blank")}>
<Button variant="outline" onClick={() => isClient && window.open(generatedInvitation.invitationLink, "_blank")}>
<ExternalLink className="w-4 h-4 mr-2" />
</Button>
@@ -1362,32 +1139,22 @@ export function UserManagement() {
<Clock className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500"></p>
</div>
) : selectedUser.activities && selectedUser.activities.length > 0 ? (
<div className="space-y-3">
{selectedUser.activities.map((activity: any, index: number) => (
<div key={index} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
{activity.type === 'login' ? (
<Calendar className="w-4 h-4 text-blue-600" />
) : activity.type === 'view_app' ? (
<Eye className="w-4 h-4 text-green-600" />
) : activity.type === 'create_app' ? (
<Code className="w-4 h-4 text-purple-600" />
) : activity.type === 'review' ? (
<Activity className="w-4 h-4 text-orange-600" />
) : (
<Activity className="w-4 h-4 text-gray-600" />
)}
<div>
<p className="text-sm font-medium">{activity.description}</p>
<p className="text-xs text-gray-500">{activity.timestamp}</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<Activity className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500"></p>
<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>
@@ -1422,7 +1189,7 @@ export function UserManagement() {
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-orange-600">
{selectedUser.status === "invited" ? 0 : (selectedUser.loginDays || 0)}
{selectedUser.status === "invited" ? 0 : 15}
</p>
<p className="text-sm text-gray-600"></p>
</div>