diff --git a/app/api/admin/dashboard/route.ts b/app/api/admin/dashboard/route.ts new file mode 100644 index 0000000..a294553 --- /dev/null +++ b/app/api/admin/dashboard/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from 'next/server' +import { UserService } from '@/lib/services/database-service' + +const userService = new UserService() + +export async function GET(request: NextRequest) { + try { + // 獲取儀表板統計數據 + const stats = await userService.getDashboardStats() + + // 獲取最新活動 + const recentActivities = await userService.getRecentActivities(10) + + // 獲取熱門應用 + const topApps = await userService.getTopApps(5) + + return NextResponse.json({ + success: true, + data: { + stats, + recentActivities, + topApps + } + }) + + } catch (error) { + console.error('獲取儀表板數據錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取儀表板數據時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/admin/users/[id]/activities/route.ts b/app/api/admin/users/[id]/activities/route.ts new file mode 100644 index 0000000..e0d45f8 --- /dev/null +++ b/app/api/admin/users/[id]/activities/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server' +import { UserService } from '@/lib/services/database-service' + +const userService = new UserService() + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: userId } = await params + const { searchParams } = new URL(request.url) + const page = parseInt(searchParams.get('page') || '1') + const limit = parseInt(searchParams.get('limit') || '10') + const search = searchParams.get('search') || '' + const startDate = searchParams.get('startDate') || '' + const endDate = searchParams.get('endDate') || '' + const activityType = searchParams.get('activityType') || 'all' + + if (!userId) { + return NextResponse.json( + { success: false, error: '用戶 ID 是必需的' }, + { status: 400 } + ) + } + + const result = await userService.getUserActivities(userId, page, limit, { + search, + startDate, + endDate, + activityType + }) + + return NextResponse.json({ + success: true, + data: result + }) + + } catch (error) { + console.error('獲取用戶活動記錄錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取用戶活動記錄時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/admin/users/[id]/route.ts b/app/api/admin/users/[id]/route.ts index 113d59f..7d6268e 100644 --- a/app/api/admin/users/[id]/route.ts +++ b/app/api/admin/users/[id]/route.ts @@ -3,71 +3,71 @@ import { UserService } from '@/lib/services/database-service' const userService = new UserService() -export async function GET( - request: NextRequest, - { params }: { params: { id: string } } -) { - try { - const user = await userService.findById(params.id) - - if (!user) { - return NextResponse.json( - { error: '用戶不存在' }, - { status: 404 } - ) - } - - // 獲取用戶統計 - const stats = await userService.getUserStatistics(params.id) - - return NextResponse.json({ - success: true, - data: { - user, - stats - } - }) - - } catch (error) { - console.error('獲取用戶詳情錯誤:', error) - return NextResponse.json( - { error: '獲取用戶詳情時發生錯誤' }, - { status: 500 } - ) - } -} - export async function PUT( request: NextRequest, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { - const updates = await request.json() - - // 移除不允許更新的欄位 - delete updates.id - delete updates.created_at - delete updates.password_hash + const { id: userId } = await params + const body = await request.json() + const { name, department, role, status } = body - const updatedUser = await userService.update(params.id, updates) - - if (!updatedUser) { + if (!userId) { return NextResponse.json( - { error: '用戶不存在或更新失敗' }, - { status: 404 } + { success: false, error: '用戶 ID 是必需的' }, + { status: 400 } ) } - return NextResponse.json({ - success: true, - message: '用戶資料已更新', - data: updatedUser + if (!name || !department || !role || !status) { + return NextResponse.json( + { success: false, error: '請填寫所有必填欄位' }, + { status: 400 } + ) + } + + // 驗證狀態值 + const validStatuses = ['active', 'inactive', 'invited'] + if (!validStatuses.includes(status)) { + return NextResponse.json( + { success: false, error: '無效的狀態值' }, + { status: 400 } + ) + } + + // 驗證角色值 + const validRoles = ['user', 'developer', 'admin'] + if (!validRoles.includes(role)) { + return NextResponse.json( + { success: false, error: '無效的角色值' }, + { status: 400 } + ) + } + + const result = await userService.updateUser(userId, { + name, + department, + role, + status }) + if (result.success) { + return NextResponse.json({ + success: true, + message: '用戶資料更新成功', + data: result.user + }) + } else { + return NextResponse.json( + { success: false, error: result.error }, + { status: 400 } + ) + } + } catch (error) { console.error('更新用戶錯誤:', error) return NextResponse.json( - { error: '更新用戶時發生錯誤' }, + { success: false, error: '更新用戶時發生錯誤' }, { status: 500 } ) } @@ -75,29 +75,37 @@ export async function PUT( export async function DELETE( request: NextRequest, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { - // 軟刪除:將 is_active 設為 false - const result = await userService.update(params.id, { is_active: false }) - - if (!result) { + const { id: userId } = await params + + if (!userId) { return NextResponse.json( - { error: '用戶不存在或刪除失敗' }, - { status: 404 } + { success: false, error: '用戶 ID 是必需的' }, + { status: 400 } ) } - return NextResponse.json({ - success: true, - message: '用戶已刪除' - }) + const result = await userService.deleteUser(userId) + + if (result.success) { + return NextResponse.json({ + success: true, + message: '用戶刪除成功' + }) + } else { + return NextResponse.json( + { success: false, error: result.error }, + { status: 400 } + ) + } } catch (error) { console.error('刪除用戶錯誤:', error) return NextResponse.json( - { error: '刪除用戶時發生錯誤' }, + { success: false, error: '刪除用戶時發生錯誤' }, { status: 500 } ) } -} +} \ No newline at end of file diff --git a/app/api/admin/users/[id]/stats/route.ts b/app/api/admin/users/[id]/stats/route.ts new file mode 100644 index 0000000..12d9e28 --- /dev/null +++ b/app/api/admin/users/[id]/stats/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server' +import { UserService } from '@/lib/services/database-service' + +const userService = new UserService() + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: userId } = await params + + if (!userId) { + return NextResponse.json( + { success: false, error: '用戶 ID 是必需的' }, + { status: 400 } + ) + } + + const stats = await userService.getUserDetailedStats(userId) + + return NextResponse.json({ + success: true, + data: stats + }) + + } catch (error) { + console.error('獲取用戶統計數據錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取用戶統計數據時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/admin/users/route.ts b/app/api/admin/users/route.ts index 2bab304..4434a3d 100644 --- a/app/api/admin/users/route.ts +++ b/app/api/admin/users/route.ts @@ -3,6 +3,73 @@ import { UserService } from '@/lib/services/database-service' const userService = new UserService() +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { email, role } = body + + if (!email || !role) { + return NextResponse.json( + { success: false, error: '請提供電子郵件和角色' }, + { status: 400 } + ) + } + + // 驗證電子郵件格式 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(email)) { + return NextResponse.json( + { success: false, error: '請輸入有效的電子郵件格式' }, + { status: 400 } + ) + } + + // 驗證角色 + const validRoles = ['user', 'developer', 'admin'] + if (!validRoles.includes(role)) { + return NextResponse.json( + { success: false, error: '無效的角色值' }, + { status: 400 } + ) + } + + // 檢查電子郵件是否已存在 + const existingUser = await userService.findByEmail(email) + if (existingUser) { + return NextResponse.json( + { success: false, error: '此電子郵件已被使用' }, + { status: 400 } + ) + } + + // 創建邀請用戶 + const result = await userService.createInvitedUser(email, role) + + if (result.success) { + return NextResponse.json({ + success: true, + message: '邀請用戶創建成功', + data: { + user: result.user, + invitationLink: result.invitationLink + } + }) + } else { + return NextResponse.json( + { success: false, error: result.error }, + { status: 400 } + ) + } + + } catch (error) { + console.error('創建邀請用戶錯誤:', error) + return NextResponse.json( + { success: false, error: '創建邀請用戶時發生錯誤' }, + { status: 500 } + ) + } +} + export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url) @@ -14,7 +81,7 @@ export async function GET(request: NextRequest) { const status = searchParams.get('status') || '' // 構建查詢條件 - let whereConditions = ['is_active = TRUE'] + let whereConditions = ['status IN ("active", "inactive", "invited")'] let params: any[] = [] if (search) { @@ -34,9 +101,11 @@ export async function GET(request: NextRequest) { if (status && status !== 'all') { if (status === 'active') { - whereConditions.push('last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY)') + whereConditions.push('status = "active"') } else if (status === 'inactive') { - whereConditions.push('last_login IS NULL OR last_login < DATE_SUB(NOW(), INTERVAL 30 DAY)') + whereConditions.push('status = "inactive"') + } else if (status === 'invited') { + whereConditions.push('status = "invited"') } } @@ -54,10 +123,21 @@ export async function GET(request: NextRequest) { const stats = await userService.getUserStats() + // 映射用戶數據欄位,將資料庫欄位轉換為前端期望的欄位 + const mappedUsers = users.map(user => ({ + ...user, + joinDate: user.join_date ? new Date(user.join_date).toLocaleDateString('zh-TW') : '-', + lastLogin: user.last_login ? new Date(user.last_login).toLocaleString('zh-TW') : '-', + status: user.status, // 直接使用資料庫的 status 欄位 + totalApps: 0, // 暫時設為 0,後續可以從統計數據中獲取 + totalReviews: 0, // 暫時設為 0,後續可以從統計數據中獲取 + totalLikes: user.total_likes || 0 + })) + return NextResponse.json({ success: true, data: { - users, + users: mappedUsers, pagination: { page, limit, @@ -65,12 +145,12 @@ export async function GET(request: NextRequest) { totalPages: Math.ceil(total / limit) }, stats: { - totalUsers: stats?.total_users || 0, - activeUsers: stats?.active_users || 0, - adminCount: stats?.admin_count || 0, - developerCount: stats?.developer_count || 0, - inactiveUsers: stats?.inactive_users || 0, - newThisMonth: stats?.new_this_month || 0 + totalUsers: stats?.totalUsers || 0, + activeUsers: stats?.activeUsers || 0, + adminCount: stats?.adminCount || 0, + developerCount: stats?.developerCount || 0, + inactiveUsers: stats?.invitedUsers || 0, // 將 invitedUsers 映射為 inactiveUsers(待註冊) + newThisMonth: stats?.newThisMonth || 0 } } }) @@ -84,51 +164,3 @@ export async function GET(request: NextRequest) { } } -export async function POST(request: NextRequest) { - try { - const { email, role } = await request.json() - - if (!email || !role) { - return NextResponse.json( - { error: '請提供電子郵件和角色' }, - { status: 400 } - ) - } - - // 檢查郵箱是否已存在 - const existingUser = await userService.findByEmail(email) - if (existingUser) { - return NextResponse.json( - { error: '該電子郵件地址已被使用' }, - { status: 400 } - ) - } - - // 生成邀請 token - const { v4: uuidv4 } = require('uuid') - const invitationToken = uuidv4() - - // 創建邀請記錄(這裡可以存儲到邀請表或臨時表) - // 暫時返回邀請連結 - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' - const invitationLink = `${baseUrl}/register?token=${invitationToken}&email=${encodeURIComponent(email)}&role=${encodeURIComponent(role)}` - - return NextResponse.json({ - success: true, - message: '用戶邀請已創建', - data: { - invitationLink, - token: invitationToken, - email, - role - } - }) - - } catch (error) { - console.error('創建用戶邀請錯誤:', error) - return NextResponse.json( - { error: '創建用戶邀請時發生錯誤' }, - { status: 500 } - ) - } -} diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 40e574f..8a96568 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -23,42 +23,69 @@ export async function POST(request: NextRequest) { ) } - // 檢查用戶是否已存在 - const existingUser = await userService.findByEmail(email) - if (existingUser) { - return NextResponse.json( - { error: '該電子郵件已被註冊' }, - { status: 409 } - ) - } - // 加密密碼 const saltRounds = 12 const password_hash = await bcrypt.hash(password, saltRounds) - // 創建新用戶 - const newUser = { - id: uuidv4(), - name, - email, - password_hash, - department, - role: role as 'user' | 'developer' | 'admin', - join_date: new Date().toISOString().split('T')[0], - total_likes: 0, - total_views: 0, - is_active: true + // 檢查是否為邀請用戶(狀態為 invited) + const invitedUser = await userService.findInvitedUserByEmail(email) + + if (invitedUser) { + // 更新邀請用戶為正式用戶 + const updatedUser = await userService.completeInvitedUserRegistration( + invitedUser.id, + name, + department, + password_hash, + role + ) + + if (updatedUser.success) { + const { password_hash: _, ...userWithoutPassword } = updatedUser.user + return NextResponse.json({ + success: true, + user: userWithoutPassword + }) + } else { + return NextResponse.json( + { error: updatedUser.error || '完成註冊失敗' }, + { status: 400 } + ) + } + } else { + // 檢查用戶是否已存在(活躍用戶) + const existingUser = await userService.findByEmail(email) + if (existingUser) { + return NextResponse.json( + { error: '該電子郵件已被註冊' }, + { status: 409 } + ) + } + + // 創建新用戶 + const newUser = { + id: uuidv4(), + name, + email, + password_hash, + department, + role: role as 'user' | 'developer' | 'admin', + join_date: new Date().toISOString().split('T')[0], + total_likes: 0, + total_views: 0, + status: 'active' as 'active' | 'inactive' | 'invited' + } + + const createdUser = await userService.create(newUser) + + // 返回用戶信息(不包含密碼) + const { password_hash: _, ...userWithoutPassword } = createdUser + return NextResponse.json({ + success: true, + user: userWithoutPassword + }) } - const createdUser = await userService.create(newUser) - - // 返回用戶信息(不包含密碼) - const { password_hash: _, ...userWithoutPassword } = createdUser - return NextResponse.json({ - success: true, - user: userWithoutPassword - }) - } catch (error) { console.error('註冊錯誤:', error) return NextResponse.json( diff --git a/app/register/page.tsx b/app/register/page.tsx index 444e5ac..20fffeb 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -130,6 +130,7 @@ export default function RegisterPage() { email: formData.email, password: formData.password, department: formData.department, + role: isInvitedUser ? invitedRole : 'user', // 如果是邀請用戶,使用邀請的角色 }) if (success) { diff --git a/components/admin/user-management.tsx b/components/admin/user-management.tsx index 91630af..0aff946 100644 --- a/components/admin/user-management.tsx +++ b/components/admin/user-management.tsx @@ -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(null) + const [userActivities, setUserActivities] = useState([]) + const [userStats, setUserStats] = useState({ + 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 + case 'Heart': + return + case 'ThumbsUp': + return + case 'Eye': + return + case 'Star': + return + case 'Plus': + return + default: + return + } + } + + // 獲取活動顏色 + 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() {

用戶尚未註冊,暫無活動記錄

) : ( -
-
- -
-

登入系統

-

2024-01-20 16:45

+ <> + {/* 篩選控件 */} +
+
+
+
+ + handleActivityFilter({ search: e.target.value })} + className="mt-1 h-8 text-sm" + /> +
+
+ + +
+
+ + handleActivityFilter({ startDate: e.target.value })} + className="mt-1 h-8 text-sm" + /> +
+
+ + handleActivityFilter({ endDate: e.target.value })} + className="mt-1 h-8 text-sm" + /> +
+
+
-
- -
-

查看應用:智能對話助手

-

2024-01-20 15:30

+ + {/* 活動記錄列表 */} + {userActivities.length === 0 ? ( +
+ +

暫無活動記錄

-
-
+ ) : ( + <> +
+ {userActivities.map((activity, index) => ( +
+
+ {getActivityIcon(activity.icon)} +
+
+

{activity.action_text}

+

+ {new Date(activity.created_at).toLocaleString('zh-TW', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + })} +

+
+
+ ))} +
+ + {/* 分頁控件 */} + {activityPagination.totalPages > 1 && ( +
+
+ 顯示第 {((activityPagination.page - 1) * activityPagination.limit) + 1} - {Math.min(activityPagination.page * activityPagination.limit, activityPagination.total)} 項,共 {activityPagination.total} 項 +
+
+ + + 第 {activityPagination.page} 頁,共 {activityPagination.totalPages} 頁 + + +
+
+ )} + + )} + )} @@ -1164,7 +1531,7 @@ export function UserManagement() {
-

{selectedUser.totalApps}

+

{userStats.totalApps}

創建應用

@@ -1172,7 +1539,7 @@ export function UserManagement() {
-

{selectedUser.totalReviews}

+

{userStats.totalReviews}

撰寫評價

@@ -1180,7 +1547,7 @@ export function UserManagement() {
-

{selectedUser.totalLikes}

+

{userStats.totalLikes}

獲得讚數

@@ -1188,9 +1555,7 @@ export function UserManagement() {
-

- {selectedUser.status === "invited" ? 0 : 15} -

+

{userStats.loginDays}

登入天數

diff --git a/contexts/auth-context.tsx b/contexts/auth-context.tsx index 63d89d7..3288ed1 100644 --- a/contexts/auth-context.tsx +++ b/contexts/auth-context.tsx @@ -61,6 +61,7 @@ interface RegisterData { email: string password: string department: string + role?: string } const AuthContext = createContext(undefined) diff --git a/lib/models.ts b/lib/models.ts index f59db7b..a78eccb 100644 --- a/lib/models.ts +++ b/lib/models.ts @@ -14,7 +14,7 @@ export interface User { join_date: string; total_likes: number; total_views: number; - is_active: boolean; + status: 'active' | 'inactive' | 'invited'; last_login?: string; phone?: string; location?: string; @@ -34,7 +34,7 @@ export interface UserProfile { join_date: string; total_likes: number; total_views: number; - is_active: boolean; + status: 'active' | 'inactive' | 'invited'; last_login?: string; phone?: string; location?: string; diff --git a/lib/services/database-service.ts b/lib/services/database-service.ts index d0176cb..9cbb8f5 100644 --- a/lib/services/database-service.ts +++ b/lib/services/database-service.ts @@ -3,6 +3,8 @@ // ===================================================== import { db } from '../database'; +import bcrypt from 'bcryptjs'; +import crypto from 'crypto'; import type { User, Judge, @@ -37,7 +39,7 @@ export class UserService { // 創建用戶 async create(userData: Omit): Promise { const sql = ` - INSERT INTO users (id, name, email, password_hash, avatar, department, role, join_date, total_likes, total_views, is_active, last_login) + INSERT INTO users (id, name, email, password_hash, avatar, department, role, join_date, total_likes, total_views, status, last_login) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; const params = [ @@ -51,7 +53,7 @@ export class UserService { userData.join_date, userData.total_likes, userData.total_views, - userData.is_active, + userData.status, userData.last_login || null ]; @@ -61,13 +63,13 @@ export class UserService { // 根據郵箱獲取用戶 async findByEmail(email: string): Promise { - const sql = 'SELECT * FROM users WHERE email = ? AND is_active = TRUE'; + const sql = 'SELECT * FROM users WHERE email = ? AND status = "active"'; return await db.queryOne(sql, [email]); } // 根據ID獲取用戶 async findById(id: string): Promise { - const sql = 'SELECT * FROM users WHERE id = ? AND is_active = TRUE'; + const sql = 'SELECT * FROM users WHERE id = ? AND status = "active"'; return await db.queryOne(sql, [id]); } @@ -111,7 +113,7 @@ export class UserService { const { search, department, role, status, page = 1, limit = 10 } = filters; // 構建查詢條件 - let whereConditions = ['is_active = TRUE']; + let whereConditions: string[] = []; let params: any[] = []; if (search) { @@ -131,9 +133,14 @@ export class UserService { if (status && status !== 'all') { if (status === 'active') { - whereConditions.push('last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY)'); + whereConditions.push('status = ? AND last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY)'); + params.push('active'); } else if (status === 'inactive') { - whereConditions.push('last_login IS NULL OR last_login < DATE_SUB(NOW(), INTERVAL 30 DAY)'); + whereConditions.push('status = ?'); + params.push('inactive'); + } else if (status === 'invited') { + whereConditions.push('status = ?'); + params.push('invited'); } } @@ -149,7 +156,7 @@ export class UserService { const usersSql = ` SELECT id, name, email, avatar, department, role, join_date, - total_likes, total_views, is_active, last_login, created_at, updated_at + total_likes, total_views, status, last_login, created_at, updated_at FROM users ${whereClause} ORDER BY created_at DESC @@ -166,19 +173,20 @@ export class UserService { activeUsers: number; adminCount: number; developerCount: number; + invitedUsers: number; inactiveUsers: number; newThisMonth: number; }> { const sql = ` SELECT COUNT(*) as total_users, - COUNT(CASE WHEN last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as active_users, + COUNT(CASE WHEN status = 'active' AND last_login IS NOT NULL AND last_login >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as active_users, COUNT(CASE WHEN role = 'admin' THEN 1 END) as admin_count, COUNT(CASE WHEN role = 'developer' THEN 1 END) as developer_count, - COUNT(CASE WHEN last_login IS NULL OR last_login < DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as inactive_users, - COUNT(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as new_this_month - FROM users - WHERE is_active = TRUE + COUNT(CASE WHEN status = 'invited' THEN 1 END) as invited_users, + COUNT(CASE WHEN status = 'inactive' THEN 1 END) as inactive_users, + COUNT(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_this_month + FROM users `; const result = await this.query(sql); const stats = result[0] || {}; @@ -188,16 +196,544 @@ export class UserService { activeUsers: stats.active_users || 0, adminCount: stats.admin_count || 0, developerCount: stats.developer_count || 0, + invitedUsers: stats.invited_users || 0, inactiveUsers: stats.inactive_users || 0, newThisMonth: stats.new_this_month || 0 }; } + // 獲取用戶活動記錄 + async getUserActivities( + userId: string, + page: number = 1, + limit: number = 10, + filters: { + search?: string; + startDate?: string; + endDate?: string; + activityType?: string; + } = {} + ): Promise<{ activities: any[]; pagination: any }> { + // 構建篩選條件 + let whereConditions = []; + let dateFilter = ''; + + // 日期篩選 + if (filters.startDate && filters.endDate) { + dateFilter = `AND created_at BETWEEN '${filters.startDate} 00:00:00' AND '${filters.endDate} 23:59:59'`; + } else if (filters.startDate) { + dateFilter = `AND created_at >= '${filters.startDate} 00:00:00'`; + } else if (filters.endDate) { + dateFilter = `AND created_at <= '${filters.endDate} 23:59:59'`; + } + + // 活動類型篩選 + let activityTypeFilter = ''; + if (filters.activityType && filters.activityType !== 'all') { + activityTypeFilter = `AND activity_type = '${filters.activityType}'`; + } + + // 搜尋篩選 + let searchFilter = ''; + if (filters.search) { + searchFilter = `AND action_text LIKE '%${filters.search}%'`; + } + + // 使用字符串插值避免 UNION ALL 參數化查詢問題 + const baseSql = ` + SELECT + 'login' as activity_type, + '登入系統' as action_text, + 'Calendar' as icon, + 'blue' as color, + u.last_login as created_at, + NULL as app_name, + NULL as app_id + FROM users u + WHERE u.id = '${userId}' AND u.last_login IS NOT NULL + + UNION ALL + + SELECT + 'favorite' as activity_type, + CONCAT('收藏應用:', a.name) as action_text, + 'Heart' as icon, + 'red' as color, + uf.created_at, + a.name as app_name, + a.id as app_id + FROM user_favorites uf + JOIN apps a ON uf.app_id = a.id + WHERE uf.user_id = '${userId}' + + UNION ALL + + SELECT + 'like' as activity_type, + CONCAT('按讚應用:', a.name) as action_text, + 'ThumbsUp' as icon, + 'green' as color, + ul.liked_at as created_at, + a.name as app_name, + a.id as app_id + FROM user_likes ul + JOIN apps a ON ul.app_id = a.id + WHERE ul.user_id = '${userId}' + + UNION ALL + + SELECT + 'view' as activity_type, + CONCAT('查看應用:', a.name) as action_text, + 'Eye' as icon, + 'purple' as color, + uv.viewed_at as created_at, + a.name as app_name, + a.id as app_id + FROM user_views uv + JOIN apps a ON uv.app_id = a.id + WHERE uv.user_id = '${userId}' + + UNION ALL + + SELECT + 'rating' as activity_type, + CONCAT('評價應用:', a.name, ' (', ur.rating, '分)') as action_text, + 'Star' as icon, + 'yellow' as color, + ur.rated_at as created_at, + a.name as app_name, + a.id as app_id + FROM user_ratings ur + JOIN apps a ON ur.app_id = a.id + WHERE ur.user_id = '${userId}' + + UNION ALL + + SELECT + 'create' as activity_type, + CONCAT('創建應用:', a.name) as action_text, + 'Plus' as icon, + 'blue' as color, + a.created_at, + a.name as app_name, + a.id as app_id + FROM apps a + WHERE a.creator_id = '${userId}' + `; + + // 先獲取總數 + const countSql = ` + SELECT COUNT(*) as total FROM ( + ${baseSql} + ) as activities + WHERE 1=1 ${dateFilter} ${activityTypeFilter} ${searchFilter} + `; + + const countResult = await this.query(countSql, []); + const total = countResult[0]?.total || 0; + const totalPages = Math.ceil(total / limit); + const offset = (page - 1) * limit; + + // 獲取分頁數據 + const dataSql = ` + SELECT * FROM ( + ${baseSql} + ) as activities + WHERE 1=1 ${dateFilter} ${activityTypeFilter} ${searchFilter} + ORDER BY created_at DESC + LIMIT ${limit} OFFSET ${offset} + `; + + const activities = await this.query(dataSql, []); + + return { + activities, + pagination: { + page, + limit, + total, + totalPages + } + }; + } + + // 獲取用戶詳細統計數據 + async getUserDetailedStats(userId: string): Promise { + // 獲取創建應用數量 + const appsSql = ` + SELECT COUNT(*) as total_apps + FROM apps + WHERE creator_id = '${userId}' AND is_active = TRUE + `; + const appsResult = await this.query(appsSql, []); + const totalApps = appsResult[0]?.total_apps || 0; + + // 獲取撰寫評價數量 + const reviewsSql = ` + SELECT COUNT(*) as total_reviews + FROM user_ratings + WHERE user_id = '${userId}' + `; + const reviewsResult = await this.query(reviewsSql, []); + const totalReviews = reviewsResult[0]?.total_reviews || 0; + + // 獲取獲得讚數(用戶創建的應用獲得的總讚數) + const likesSql = ` + SELECT COALESCE(SUM(a.likes_count), 0) as total_likes + FROM apps a + WHERE a.creator_id = '${userId}' AND a.is_active = TRUE + `; + const likesResult = await this.query(likesSql, []); + const totalLikes = likesResult[0]?.total_likes || 0; + + // 獲取登入天數(從註冊日期到現在的天數) + const loginDaysSql = ` + SELECT DATEDIFF(CURDATE(), join_date) as login_days + FROM users + WHERE id = '${userId}' + `; + const loginDaysResult = await this.query(loginDaysSql, []); + const loginDays = loginDaysResult[0]?.login_days || 0; + + return { + totalApps, + totalReviews, + totalLikes, + loginDays + }; + } + + // 更新用戶資料 + async updateUser(userId: string, userData: { + name: string; + department: string; + role: string; + status: string; + }): Promise<{ success: boolean; user?: any; error?: string }> { + try { + const sql = ` + UPDATE users + SET name = ?, department = ?, role = ?, status = ?, updated_at = NOW() + WHERE id = ? + `; + + await this.query(sql, [ + userData.name, + userData.department, + userData.role, + userData.status, + userId + ]); + + // 獲取更新後的用戶資料 + const updatedUserSql = ` + SELECT id, name, email, department, role, status, join_date, last_login, total_likes, total_views, created_at, updated_at + FROM users + WHERE id = ? + `; + + const updatedUser = await this.queryOne(updatedUserSql, [userId]); + + if (!updatedUser) { + return { success: false, error: '用戶不存在' }; + } + + return { success: true, user: updatedUser }; + } catch (error) { + console.error('更新用戶錯誤:', error); + return { success: false, error: '更新用戶時發生錯誤' }; + } + } + + // 刪除用戶 + async deleteUser(userId: string): Promise<{ success: boolean; error?: string }> { + try { + // 檢查用戶是否存在 + const checkUserSql = `SELECT id FROM users WHERE id = ?`; + const user = await this.queryOne(checkUserSql, [userId]); + + if (!user) { + return { success: false, error: '用戶不存在' }; + } + + // 刪除用戶(由於外鍵約束,相關的活動記錄也會被自動刪除) + const deleteSql = `DELETE FROM users WHERE id = ?`; + await this.query(deleteSql, [userId]); + + return { success: true }; + } catch (error) { + console.error('刪除用戶錯誤:', error); + return { success: false, error: '刪除用戶時發生錯誤' }; + } + } + + // 創建邀請用戶 + async createInvitedUser(email: string, role: string): Promise<{ success: boolean; user?: any; invitationLink?: string; error?: string }> { + try { + // 生成邀請 token + const invitationToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) + + // 創建邀請用戶記錄 + const userId = crypto.randomUUID() + const sql = ` + INSERT INTO users (id, name, email, password_hash, department, role, status, join_date, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, CURDATE(), NOW(), NOW()) + `; + + // 為邀請用戶創建一個臨時密碼(用戶註冊時會覆蓋) + const tempPassword = 'temp_' + Math.random().toString(36).substring(2, 15) + const tempPasswordHash = await bcrypt.hash(tempPassword, 10) + + await this.query(sql, [ + userId, + '', // 姓名留空,用戶註冊時填寫 + email, + tempPasswordHash, + '', // 部門留空,用戶註冊時填寫 + role, + 'invited' + ]); + + // 獲取創建的用戶資料 + const userSql = ` + SELECT id, name, email, department, role, status, join_date, created_at, updated_at + FROM users + WHERE id = ? + `; + + const user = await this.queryOne(userSql, [userId]) + + if (!user) { + return { success: false, error: '創建邀請用戶失敗' } + } + + // 生成邀請連結 + const invitationLink = `${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/register?token=${invitationToken}&email=${encodeURIComponent(email)}&role=${role}` + + return { + success: true, + user: { + ...user, + invitationToken, + invitationLink + }, + invitationLink + } + } catch (error) { + console.error('創建邀請用戶錯誤:', error); + return { success: false, error: '創建邀請用戶時發生錯誤' }; + } + } + + // 查找邀請用戶(狀態為 invited) + async findInvitedUserByEmail(email: string): Promise { + const sql = 'SELECT * FROM users WHERE email = ? AND status = "invited"'; + return await this.queryOne(sql, [email]); + } + + // 完成邀請用戶註冊 + async completeInvitedUserRegistration( + userId: string, + name: string, + department: string, + passwordHash: string, + role: string + ): Promise<{ success: boolean; user?: any; error?: string }> { + try { + const sql = ` + UPDATE users + SET name = ?, department = ?, password_hash = ?, role = ?, status = 'active', updated_at = NOW() + WHERE id = ? AND status = 'invited' + `; + + await this.query(sql, [name, department, passwordHash, role, userId]); + + // 獲取更新後的用戶資料 + const updatedUserSql = ` + SELECT id, name, email, department, role, status, join_date, last_login, total_likes, total_views, created_at, updated_at + FROM users + WHERE id = ? + `; + + const updatedUser = await this.queryOne(updatedUserSql, [userId]); + + if (!updatedUser) { + return { success: false, error: '用戶不存在或狀態不正確' }; + } + + return { success: true, user: updatedUser }; + } catch (error) { + console.error('完成邀請用戶註冊錯誤:', error); + return { success: false, error: '完成註冊時發生錯誤' }; + } + } + // 通用查詢方法 async query(sql: string, params: any[] = []): Promise { return await db.query(sql, params); } + // 獲取儀表板統計數據 + async getDashboardStats(): Promise<{ + totalUsers: number; + activeUsers: number; + totalApps: number; + totalCompetitions: number; + totalReviews: number; + totalViews: number; + totalLikes: number; + newAppsThisMonth: number; + activeCompetitions: number; + growthRate: number; + }> { + try { + // 用戶統計 + const userStats = await this.getUserStats(); + + // 應用統計 + const appStatsSql = ` + SELECT + COUNT(*) as total_apps, + COUNT(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_apps_this_month, + COALESCE(SUM(view_count), 0) as total_views, + COALESCE(SUM(like_count), 0) as total_likes + FROM apps + WHERE is_active = TRUE + `; + const appStats = await this.queryOne(appStatsSql); + + // 評論統計 + const reviewStatsSql = ` + SELECT COUNT(*) as total_reviews + FROM user_ratings + `; + const reviewStats = await this.queryOne(reviewStatsSql); + + // 競賽統計 + const competitionStatsSql = ` + SELECT + COUNT(*) as total_competitions, + COUNT(CASE WHEN status = 'active' THEN 1 END) as active_competitions + FROM competitions + `; + const competitionStats = await this.queryOne(competitionStatsSql); + + // 計算增長率(與上個月比較) + const lastMonthUsersSql = ` + SELECT COUNT(*) as last_month_users + FROM users + WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 60 DAY) + AND created_at < DATE_SUB(CURDATE(), INTERVAL 30 DAY) + `; + const lastMonthUsers = await this.queryOne(lastMonthUsersSql); + + const currentMonthUsers = userStats.newThisMonth; + const growthRate = lastMonthUsers?.last_month_users > 0 + ? Math.round(((currentMonthUsers - lastMonthUsers.last_month_users) / lastMonthUsers.last_month_users) * 100) + : 0; + + return { + totalUsers: userStats.totalUsers, + activeUsers: userStats.activeUsers, + totalApps: appStats?.total_apps || 0, + totalCompetitions: competitionStats?.total_competitions || 0, + totalReviews: reviewStats?.total_reviews || 0, + totalViews: appStats?.total_views || 0, + totalLikes: appStats?.total_likes || 0, + newAppsThisMonth: appStats?.new_apps_this_month || 0, + activeCompetitions: competitionStats?.active_competitions || 0, + growthRate: growthRate + }; + } catch (error) { + console.error('獲取儀表板統計數據錯誤:', error); + return { + totalUsers: 0, + activeUsers: 0, + totalApps: 0, + totalCompetitions: 0, + totalReviews: 0, + totalViews: 0, + totalLikes: 0, + newAppsThisMonth: 0, + activeCompetitions: 0, + growthRate: 0 + }; + } + } + + // 獲取最新活動 + async getRecentActivities(limit: number = 10): Promise { + try { + const sql = ` + SELECT + 'user_register' as activity_type, + '用戶註冊' as activity_name, + CONCAT(name, ' 註冊了平台') as description, + created_at as activity_time, + 'user' as icon_type, + 'blue' as color + FROM users + WHERE status = 'active' + ORDER BY created_at DESC + LIMIT ? + `; + + const activities = await this.query(sql, [limit]); + + return activities.map(activity => ({ + id: `user_${activity.activity_time}`, + type: activity.activity_type, + message: activity.description, + time: new Date(activity.activity_time).toLocaleString('zh-TW'), + icon: 'Users', + color: 'text-blue-600' + })); + } catch (error) { + console.error('獲取最新活動錯誤:', error); + return []; + } + } + + // 獲取熱門應用 + async getTopApps(limit: number = 5): Promise { + try { + const sql = ` + SELECT + a.id, + a.name, + a.description, + a.view_count as views, + a.like_count as likes, + COALESCE(AVG(ur.rating), 0) as rating, + a.category, + a.created_at + FROM apps a + LEFT JOIN user_ratings ur ON a.id = ur.app_id + WHERE a.is_active = TRUE + GROUP BY a.id, a.name, a.description, a.view_count, a.like_count, a.category, a.created_at + ORDER BY (a.view_count + a.like_count * 2) DESC + LIMIT ? + `; + + const apps = await this.query(sql, [limit]); + + return apps.map(app => ({ + id: app.id, + name: app.name, + description: app.description, + views: app.views || 0, + likes: app.likes || 0, + rating: Math.round(app.rating * 10) / 10, + category: app.category, + created_at: app.created_at + })); + } catch (error) { + console.error('獲取熱門應用錯誤:', error); + return []; + } + } + // 通用單一查詢方法 async queryOne(sql: string, params: any[] = []): Promise { return await db.queryOne(sql, params);