實作管理者用戶管理、邀請註冊功能
This commit is contained in:
33
app/api/admin/dashboard/route.ts
Normal file
33
app/api/admin/dashboard/route.ts
Normal file
@@ -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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
46
app/api/admin/users/[id]/activities/route.ts
Normal file
46
app/api/admin/users/[id]/activities/route.ts
Normal file
@@ -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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -3,71 +3,71 @@ import { UserService } from '@/lib/services/database-service'
|
|||||||
|
|
||||||
const userService = new UserService()
|
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(
|
export async function PUT(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const updates = await request.json()
|
const { id: userId } = await params
|
||||||
|
const body = await request.json()
|
||||||
|
const { name, department, role, status } = body
|
||||||
|
|
||||||
// 移除不允許更新的欄位
|
if (!userId) {
|
||||||
delete updates.id
|
|
||||||
delete updates.created_at
|
|
||||||
delete updates.password_hash
|
|
||||||
|
|
||||||
const updatedUser = await userService.update(params.id, updates)
|
|
||||||
|
|
||||||
if (!updatedUser) {
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '用戶不存在或更新失敗' },
|
{ success: false, error: '用戶 ID 是必需的' },
|
||||||
{ status: 404 }
|
{ status: 400 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
if (!name || !department || !role || !status) {
|
||||||
success: true,
|
return NextResponse.json(
|
||||||
message: '用戶資料已更新',
|
{ success: false, error: '請填寫所有必填欄位' },
|
||||||
data: updatedUser
|
{ 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) {
|
} catch (error) {
|
||||||
console.error('更新用戶錯誤:', error)
|
console.error('更新用戶錯誤:', error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '更新用戶時發生錯誤' },
|
{ success: false, error: '更新用戶時發生錯誤' },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -75,28 +75,36 @@ export async function PUT(
|
|||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// 軟刪除:將 is_active 設為 false
|
const { id: userId } = await params
|
||||||
const result = await userService.update(params.id, { is_active: false })
|
|
||||||
|
|
||||||
if (!result) {
|
if (!userId) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '用戶不存在或刪除失敗' },
|
{ success: false, error: '用戶 ID 是必需的' },
|
||||||
{ status: 404 }
|
{ status: 400 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
const result = await userService.deleteUser(userId)
|
||||||
success: true,
|
|
||||||
message: '用戶已刪除'
|
if (result.success) {
|
||||||
})
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '用戶刪除成功'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: result.error },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('刪除用戶錯誤:', error)
|
console.error('刪除用戶錯誤:', error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '刪除用戶時發生錯誤' },
|
{ success: false, error: '刪除用戶時發生錯誤' },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
34
app/api/admin/users/[id]/stats/route.ts
Normal file
34
app/api/admin/users/[id]/stats/route.ts
Normal file
@@ -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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -3,6 +3,73 @@ import { UserService } from '@/lib/services/database-service'
|
|||||||
|
|
||||||
const userService = new UserService()
|
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) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url)
|
const { searchParams } = new URL(request.url)
|
||||||
@@ -14,7 +81,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const status = searchParams.get('status') || ''
|
const status = searchParams.get('status') || ''
|
||||||
|
|
||||||
// 構建查詢條件
|
// 構建查詢條件
|
||||||
let whereConditions = ['is_active = TRUE']
|
let whereConditions = ['status IN ("active", "inactive", "invited")']
|
||||||
let params: any[] = []
|
let params: any[] = []
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
@@ -34,9 +101,11 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
if (status && status !== 'all') {
|
if (status && status !== 'all') {
|
||||||
if (status === 'active') {
|
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') {
|
} 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 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({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
users,
|
users: mappedUsers,
|
||||||
pagination: {
|
pagination: {
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
@@ -65,12 +145,12 @@ export async function GET(request: NextRequest) {
|
|||||||
totalPages: Math.ceil(total / limit)
|
totalPages: Math.ceil(total / limit)
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
totalUsers: stats?.total_users || 0,
|
totalUsers: stats?.totalUsers || 0,
|
||||||
activeUsers: stats?.active_users || 0,
|
activeUsers: stats?.activeUsers || 0,
|
||||||
adminCount: stats?.admin_count || 0,
|
adminCount: stats?.adminCount || 0,
|
||||||
developerCount: stats?.developer_count || 0,
|
developerCount: stats?.developerCount || 0,
|
||||||
inactiveUsers: stats?.inactive_users || 0,
|
inactiveUsers: stats?.invitedUsers || 0, // 將 invitedUsers 映射為 inactiveUsers(待註冊)
|
||||||
newThisMonth: stats?.new_this_month || 0
|
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 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -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 saltRounds = 12
|
||||||
const password_hash = await bcrypt.hash(password, saltRounds)
|
const password_hash = await bcrypt.hash(password, saltRounds)
|
||||||
|
|
||||||
// 創建新用戶
|
// 檢查是否為邀請用戶(狀態為 invited)
|
||||||
const newUser = {
|
const invitedUser = await userService.findInvitedUserByEmail(email)
|
||||||
id: uuidv4(),
|
|
||||||
name,
|
if (invitedUser) {
|
||||||
email,
|
// 更新邀請用戶為正式用戶
|
||||||
password_hash,
|
const updatedUser = await userService.completeInvitedUserRegistration(
|
||||||
department,
|
invitedUser.id,
|
||||||
role: role as 'user' | 'developer' | 'admin',
|
name,
|
||||||
join_date: new Date().toISOString().split('T')[0],
|
department,
|
||||||
total_likes: 0,
|
password_hash,
|
||||||
total_views: 0,
|
role
|
||||||
is_active: true
|
)
|
||||||
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('註冊錯誤:', error)
|
console.error('註冊錯誤:', error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
@@ -130,6 +130,7 @@ export default function RegisterPage() {
|
|||||||
email: formData.email,
|
email: formData.email,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
department: formData.department,
|
department: formData.department,
|
||||||
|
role: isInvitedUser ? invitedRole : 'user', // 如果是邀請用戶,使用邀請的角色
|
||||||
})
|
})
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@@ -33,9 +33,17 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
Copy,
|
Copy,
|
||||||
Link,
|
Link,
|
||||||
|
Heart,
|
||||||
|
ThumbsUp,
|
||||||
|
Star,
|
||||||
|
Plus,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Code,
|
Code,
|
||||||
Users,
|
Users,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Filter,
|
||||||
|
X,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
export function UserManagement() {
|
export function UserManagement() {
|
||||||
@@ -45,6 +53,25 @@ export function UserManagement() {
|
|||||||
const [selectedRole, setSelectedRole] = useState("all")
|
const [selectedRole, setSelectedRole] = useState("all")
|
||||||
const [selectedStatus, setSelectedStatus] = useState("all")
|
const [selectedStatus, setSelectedStatus] = useState("all")
|
||||||
const [selectedUser, setSelectedUser] = useState<any>(null)
|
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 [showUserDetail, setShowUserDetail] = useState(false)
|
||||||
const [showInviteUser, setShowInviteUser] = useState(false)
|
const [showInviteUser, setShowInviteUser] = useState(false)
|
||||||
const [showEditUser, setShowEditUser] = useState(false)
|
const [showEditUser, setShowEditUser] = useState(false)
|
||||||
@@ -71,6 +98,74 @@ export function UserManagement() {
|
|||||||
setIsClient(true)
|
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 () => {
|
const loadUsers = async () => {
|
||||||
if (!isClient) return
|
if (!isClient) return
|
||||||
@@ -133,6 +228,51 @@ export function UserManagement() {
|
|||||||
const handleViewUser = (user: any) => {
|
const handleViewUser = (user: any) => {
|
||||||
setSelectedUser(user)
|
setSelectedUser(user)
|
||||||
setShowUserDetail(true)
|
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) => {
|
const handleEditUser = (user: any) => {
|
||||||
@@ -155,18 +295,51 @@ export function UserManagement() {
|
|||||||
const handleToggleUserStatus = async (userId: string) => {
|
const handleToggleUserStatus = async (userId: string) => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
// 模擬 API 調用
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
// 找到要切換狀態的用戶
|
||||||
|
const user = users.find(u => u.id === userId)
|
||||||
|
if (!user) {
|
||||||
|
setError("用戶不存在")
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setUsers(
|
const newStatus = user.status === "active" ? "inactive" : "active"
|
||||||
users.map((user) =>
|
|
||||||
user.id === userId ? { ...user, status: user.status === "active" ? "inactive" : "active" } : user,
|
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)
|
setIsLoading(false)
|
||||||
setSuccess("用戶狀態更新成功!")
|
|
||||||
setTimeout(() => setSuccess(""), 3000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChangeUserRole = async (userId: string, newRole: string) => {
|
const handleChangeUserRole = async (userId: string, newRole: string) => {
|
||||||
@@ -222,13 +395,13 @@ export function UserManagement() {
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const newInvitation = {
|
const newInvitation = {
|
||||||
id: Date.now().toString(),
|
id: data.data.user.id,
|
||||||
name: "",
|
name: data.data.user.name || "",
|
||||||
email: inviteEmail,
|
email: data.data.user.email,
|
||||||
department: "",
|
department: data.data.user.department || "",
|
||||||
role: "",
|
role: data.data.user.role,
|
||||||
status: "invited",
|
status: "invited",
|
||||||
joinDate: "",
|
joinDate: data.data.user.join_date ? new Date(data.data.user.join_date).toLocaleDateString('zh-TW') : "",
|
||||||
lastLogin: "",
|
lastLogin: "",
|
||||||
totalApps: 0,
|
totalApps: 0,
|
||||||
totalReviews: 0,
|
totalReviews: 0,
|
||||||
@@ -327,15 +500,38 @@ export function UserManagement() {
|
|||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
// 模擬 API 調用
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
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)
|
setIsLoading(false)
|
||||||
setShowEditUser(false)
|
|
||||||
setSuccess("用戶資料更新成功!")
|
|
||||||
setTimeout(() => setSuccess(""), 3000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmDeleteUser = async () => {
|
const confirmDeleteUser = async () => {
|
||||||
@@ -343,16 +539,30 @@ export function UserManagement() {
|
|||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
// 模擬 API 調用
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
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)
|
setIsLoading(false)
|
||||||
setShowDeleteConfirm(false)
|
|
||||||
setUserToDelete(null)
|
|
||||||
setSuccess("用戶刪除成功!")
|
|
||||||
setTimeout(() => setSuccess(""), 3000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRoleColor = (role: string) => {
|
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) => {
|
const getRoleIcon = (role: string) => {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case "admin":
|
case "admin":
|
||||||
@@ -1140,22 +1388,141 @@ export function UserManagement() {
|
|||||||
<p className="text-gray-500">用戶尚未註冊,暫無活動記錄</p>
|
<p className="text-gray-500">用戶尚未註冊,暫無活動記錄</p>
|
||||||
</div>
|
</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 className="space-y-3">
|
||||||
<div>
|
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||||
<p className="text-sm font-medium">登入系統</p>
|
<div className="flex flex-wrap gap-3 items-end">
|
||||||
<p className="text-xs text-gray-500">2024-01-20 16:45</p>
|
<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>
|
</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>
|
{userActivities.length === 0 ? (
|
||||||
<p className="text-sm font-medium">查看應用:智能對話助手</p>
|
<div className="text-center py-8">
|
||||||
<p className="text-xs text-gray-500">2024-01-20 15:30</p>
|
<Calendar className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||||
|
<p className="text-gray-500">暫無活動記錄</p>
|
||||||
</div>
|
</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>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -1164,7 +1531,7 @@ export function UserManagement() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="text-center">
|
<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>
|
<p className="text-sm text-gray-600">創建應用</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -1172,7 +1539,7 @@ export function UserManagement() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="text-center">
|
<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>
|
<p className="text-sm text-gray-600">撰寫評價</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -1180,7 +1547,7 @@ export function UserManagement() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="text-center">
|
<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>
|
<p className="text-sm text-gray-600">獲得讚數</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -1188,9 +1555,7 @@ export function UserManagement() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-2xl font-bold text-orange-600">
|
<p className="text-2xl font-bold text-orange-600">{userStats.loginDays}</p>
|
||||||
{selectedUser.status === "invited" ? 0 : 15}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600">登入天數</p>
|
<p className="text-sm text-gray-600">登入天數</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
@@ -61,6 +61,7 @@ interface RegisterData {
|
|||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
department: string
|
department: string
|
||||||
|
role?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||||
|
@@ -14,7 +14,7 @@ export interface User {
|
|||||||
join_date: string;
|
join_date: string;
|
||||||
total_likes: number;
|
total_likes: number;
|
||||||
total_views: number;
|
total_views: number;
|
||||||
is_active: boolean;
|
status: 'active' | 'inactive' | 'invited';
|
||||||
last_login?: string;
|
last_login?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
@@ -34,7 +34,7 @@ export interface UserProfile {
|
|||||||
join_date: string;
|
join_date: string;
|
||||||
total_likes: number;
|
total_likes: number;
|
||||||
total_views: number;
|
total_views: number;
|
||||||
is_active: boolean;
|
status: 'active' | 'inactive' | 'invited';
|
||||||
last_login?: string;
|
last_login?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
|
@@ -3,6 +3,8 @@
|
|||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
import { db } from '../database';
|
import { db } from '../database';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import crypto from 'crypto';
|
||||||
import type {
|
import type {
|
||||||
User,
|
User,
|
||||||
Judge,
|
Judge,
|
||||||
@@ -37,7 +39,7 @@ export class UserService {
|
|||||||
// 創建用戶
|
// 創建用戶
|
||||||
async create(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
|
async create(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
|
||||||
const sql = `
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`;
|
`;
|
||||||
const params = [
|
const params = [
|
||||||
@@ -51,7 +53,7 @@ export class UserService {
|
|||||||
userData.join_date,
|
userData.join_date,
|
||||||
userData.total_likes,
|
userData.total_likes,
|
||||||
userData.total_views,
|
userData.total_views,
|
||||||
userData.is_active,
|
userData.status,
|
||||||
userData.last_login || null
|
userData.last_login || null
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -61,13 +63,13 @@ export class UserService {
|
|||||||
|
|
||||||
// 根據郵箱獲取用戶
|
// 根據郵箱獲取用戶
|
||||||
async findByEmail(email: string): Promise<User | null> {
|
async findByEmail(email: string): Promise<User | null> {
|
||||||
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<User>(sql, [email]);
|
return await db.queryOne<User>(sql, [email]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根據ID獲取用戶
|
// 根據ID獲取用戶
|
||||||
async findById(id: string): Promise<User | null> {
|
async findById(id: string): Promise<User | null> {
|
||||||
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<User>(sql, [id]);
|
return await db.queryOne<User>(sql, [id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +113,7 @@ export class UserService {
|
|||||||
const { search, department, role, status, page = 1, limit = 10 } = filters;
|
const { search, department, role, status, page = 1, limit = 10 } = filters;
|
||||||
|
|
||||||
// 構建查詢條件
|
// 構建查詢條件
|
||||||
let whereConditions = ['is_active = TRUE'];
|
let whereConditions: string[] = [];
|
||||||
let params: any[] = [];
|
let params: any[] = [];
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
@@ -131,9 +133,14 @@ export class UserService {
|
|||||||
|
|
||||||
if (status && status !== 'all') {
|
if (status && status !== 'all') {
|
||||||
if (status === 'active') {
|
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') {
|
} 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 = `
|
const usersSql = `
|
||||||
SELECT
|
SELECT
|
||||||
id, name, email, avatar, department, role, join_date,
|
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
|
FROM users
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
@@ -166,19 +173,20 @@ export class UserService {
|
|||||||
activeUsers: number;
|
activeUsers: number;
|
||||||
adminCount: number;
|
adminCount: number;
|
||||||
developerCount: number;
|
developerCount: number;
|
||||||
|
invitedUsers: number;
|
||||||
inactiveUsers: number;
|
inactiveUsers: number;
|
||||||
newThisMonth: number;
|
newThisMonth: number;
|
||||||
}> {
|
}> {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_users,
|
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 = 'admin' THEN 1 END) as admin_count,
|
||||||
COUNT(CASE WHEN role = 'developer' THEN 1 END) as developer_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 status = 'invited' THEN 1 END) as invited_users,
|
||||||
COUNT(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as new_this_month
|
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
|
FROM users
|
||||||
WHERE is_active = TRUE
|
|
||||||
`;
|
`;
|
||||||
const result = await this.query(sql);
|
const result = await this.query(sql);
|
||||||
const stats = result[0] || {};
|
const stats = result[0] || {};
|
||||||
@@ -188,16 +196,544 @@ export class UserService {
|
|||||||
activeUsers: stats.active_users || 0,
|
activeUsers: stats.active_users || 0,
|
||||||
adminCount: stats.admin_count || 0,
|
adminCount: stats.admin_count || 0,
|
||||||
developerCount: stats.developer_count || 0,
|
developerCount: stats.developer_count || 0,
|
||||||
|
invitedUsers: stats.invited_users || 0,
|
||||||
inactiveUsers: stats.inactive_users || 0,
|
inactiveUsers: stats.inactive_users || 0,
|
||||||
newThisMonth: stats.new_this_month || 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<any> {
|
||||||
|
// 獲取創建應用數量
|
||||||
|
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<any | null> {
|
||||||
|
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<T = any>(sql: string, params: any[] = []): Promise<T[]> {
|
async query<T = any>(sql: string, params: any[] = []): Promise<T[]> {
|
||||||
return await db.query<T>(sql, params);
|
return await db.query<T>(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<any[]> {
|
||||||
|
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<any[]> {
|
||||||
|
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<T = any>(sql: string, params: any[] = []): Promise<T | null> {
|
async queryOne<T = any>(sql: string, params: any[] = []): Promise<T | null> {
|
||||||
return await db.queryOne<T>(sql, params);
|
return await db.queryOne<T>(sql, params);
|
||||||
|
Reference in New Issue
Block a user