Files

771 lines
29 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect } from "react"
import { ProtectedRoute } from "@/components/protected-route"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Plus, Edit, Trash2, ArrowLeft, Eye, EyeOff, ChevronLeft, ChevronRight } from "lucide-react"
import Link from "next/link"
import { useAuth, type User } from "@/lib/hooks/use-auth"
export default function UsersManagementPage() {
return (
<ProtectedRoute adminOnly>
<UsersManagementContent />
</ProtectedRoute>
)
}
function UsersManagementContent() {
const { user: currentUser } = useAuth()
const [users, setUsers] = useState<(User & { password?: string })[]>([])
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [editingUser, setEditingUser] = useState<User | null>(null)
const [deletingUser, setDeletingUser] = useState<User | null>(null)
const [showPassword, setShowPassword] = useState(false)
const [newUser, setNewUser] = useState({
name: "",
email: "",
password: "",
department: "",
role: "user" as "user" | "admin",
})
const [error, setError] = useState("")
// 分頁相關狀態
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [totalUsers, setTotalUsers] = useState(0)
const [adminCount, setAdminCount] = useState(0)
const [userCount, setUserCount] = useState(0)
const usersPerPage = 5
const departments = ["人力資源部", "資訊技術部", "財務部", "行銷部", "業務部", "研發部", "客服部", "其他"]
useEffect(() => {
loadUsers()
}, [])
const loadUsers = async (page: number = 1) => {
try {
const response = await fetch(`/api/admin/users?page=${page}&limit=${usersPerPage}`)
const data = await response.json()
if (data.success) {
setUsers(data.data.users)
setTotalPages(data.data.totalPages)
setTotalUsers(data.data.totalUsers)
setCurrentPage(page)
// 更新統計數據
setAdminCount(data.data.adminCount)
setUserCount(data.data.userCount)
} else {
setError(data.error || '載入用戶列表失敗')
}
} catch (err) {
console.error('載入用戶列表錯誤:', err)
setError('載入用戶列表時發生錯誤')
}
}
const handleAddUser = async () => {
setError("")
if (!newUser.name || !newUser.email || !newUser.password || !newUser.department) {
setError("請填寫所有必填欄位")
return
}
try {
const response = await fetch('/api/admin/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newUser),
})
const data = await response.json()
if (data.success) {
// 重新載入用戶列表
await loadUsers(currentPage)
setNewUser({
name: "",
email: "",
password: "",
department: "",
role: "user",
})
setIsAddDialogOpen(false)
} else {
setError(data.error || '創建用戶失敗')
}
} catch (err) {
console.error('創建用戶錯誤:', err)
setError('創建用戶時發生錯誤')
}
}
const handleEditUser = (user: User) => {
setEditingUser(user)
setNewUser({
name: user.name,
email: user.email,
password: "",
department: user.department,
role: user.role,
})
}
const handleUpdateUser = async () => {
if (!editingUser) return
setError("")
if (!newUser.name || !newUser.email || !newUser.department) {
setError("請填寫所有必填欄位")
return
}
try {
const updateData: any = {
id: editingUser.id,
name: newUser.name,
email: newUser.email,
department: newUser.department,
role: newUser.role,
}
const response = await fetch('/api/admin/users', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updateData),
})
const data = await response.json()
if (data.success) {
// 重新載入用戶列表
await loadUsers(currentPage)
setEditingUser(null)
setNewUser({
name: "",
email: "",
password: "",
department: "",
role: "user",
})
} else {
setError(data.error || '更新用戶失敗')
}
} catch (err) {
console.error('更新用戶錯誤:', err)
setError('更新用戶時發生錯誤')
}
}
const handleDeleteUser = (user: User) => {
if (user.id === currentUser?.id) {
setError("無法刪除自己的帳戶")
return
}
setDeletingUser(user)
}
const confirmDeleteUser = async () => {
if (!deletingUser) return
try {
const response = await fetch(`/api/admin/users?id=${deletingUser.id}`, {
method: 'DELETE',
})
const data = await response.json()
if (data.success) {
// 重新載入用戶列表
await loadUsers(currentPage)
setDeletingUser(null)
} else {
setError(data.error || '刪除用戶失敗')
}
} catch (err) {
console.error('刪除用戶錯誤:', err)
setError('刪除用戶時發生錯誤')
}
}
// 分頁控制函數
const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages) {
loadUsers(page)
}
}
const handlePreviousPage = () => {
if (currentPage > 1) {
handlePageChange(currentPage - 1)
}
}
const handleNextPage = () => {
if (currentPage < totalPages) {
handlePageChange(currentPage + 1)
}
}
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b bg-card/50 backdrop-blur-sm">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild>
<Link href="/dashboard">
<ArrowLeft className="w-4 h-4 mr-2" />
<span className="hidden sm:inline"></span>
</Link>
</Button>
<div>
<h1 className="text-xl font-bold text-foreground"></h1>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
</div>
</header>
<div className="container mx-auto px-4 py-8">
<div className="max-w-6xl mx-auto">
{/* Stats */}
<div className="grid grid-cols-3 md:grid-cols-3 gap-3 md:gap-6 mb-6 md:mb-8">
<Card className="p-3 md:p-6">
<CardHeader className="pb-1 md:pb-2 p-0">
<CardTitle className="text-xs md:text-sm font-medium text-muted-foreground text-center md:text-left"></CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="text-lg md:text-2xl font-bold text-center md:text-left">{totalUsers}</div>
</CardContent>
</Card>
<Card className="p-3 md:p-6">
<CardHeader className="pb-1 md:pb-2 p-0">
<CardTitle className="text-xs md:text-sm font-medium text-muted-foreground text-center md:text-left"></CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="text-lg md:text-2xl font-bold text-center md:text-left">{adminCount}</div>
</CardContent>
</Card>
<Card className="p-3 md:p-6">
<CardHeader className="pb-1 md:pb-2 p-0">
<CardTitle className="text-xs md:text-sm font-medium text-muted-foreground text-center md:text-left"></CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="text-lg md:text-2xl font-bold text-center md:text-left">{userCount}</div>
</CardContent>
</Card>
</div>
{/* Users Table */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</div>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="w-4 h-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input
id="name"
value={newUser.name}
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
placeholder="請輸入姓名"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={newUser.email}
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
placeholder="請輸入電子郵件"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password"></Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="password"
type={showPassword ? "text" : "password"}
value={newUser.password}
onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
placeholder="請輸入密碼或使用預設密碼"
className="pr-10"
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-muted-foreground" />
) : (
<Eye className="h-4 w-4 text-muted-foreground" />
)}
</Button>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setNewUser({ ...newUser, password: "password123" })}
className="whitespace-nowrap"
>
使
</Button>
</div>
<p className="text-xs text-muted-foreground">
password123
</p>
</div>
<div className="space-y-2">
<Label htmlFor="department"></Label>
<Select
value={newUser.department}
onValueChange={(value) => setNewUser({ ...newUser, department: value })}
>
<SelectTrigger>
<SelectValue placeholder="請選擇部門" />
</SelectTrigger>
<SelectContent>
{departments.map((dept) => (
<SelectItem key={dept} value={dept}>
{dept}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="role"></Label>
<Select
value={newUser.role}
onValueChange={(value: "user" | "admin") => setNewUser({ ...newUser, role: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user"></SelectItem>
<SelectItem value="admin"></SelectItem>
</SelectContent>
</Select>
</div>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="flex gap-2">
<Button onClick={handleAddUser} className="flex-1">
</Button>
<Button variant="outline" onClick={() => setIsAddDialogOpen(false)} className="flex-1">
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.department}</TableCell>
<TableCell>
<Badge variant={user.role === "admin" ? "default" : "secondary"}>
{user.role === "admin" ? "管理員" : "一般用戶"}
</Badge>
</TableCell>
<TableCell>{new Date(user.createdAt).toLocaleDateString("zh-TW")}</TableCell>
<TableCell>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => handleEditUser(user)}>
<Edit className="w-4 h-4" />
</Button>
{user.id !== currentUser?.id && (
<Button variant="ghost" size="sm" onClick={() => handleDeleteUser(user)}>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex flex-col sm:flex-row items-center justify-between mt-6 gap-4">
<div className="text-sm text-muted-foreground text-center sm:text-left">
{((currentPage - 1) * usersPerPage) + 1} - {Math.min(currentPage * usersPerPage, totalUsers)} {totalUsers}
</div>
{/* Desktop Pagination */}
<div className="hidden sm:flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={handlePreviousPage}
disabled={currentPage === 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="flex items-center space-x-1">
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<Button
key={page}
variant={currentPage === page ? "default" : "outline"}
size="sm"
onClick={() => handlePageChange(page)}
className="w-8 h-8 p-0"
>
{page}
</Button>
))}
</div>
<Button
variant="outline"
size="sm"
onClick={handleNextPage}
disabled={currentPage === totalPages}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
{/* Mobile Pagination */}
<div className="flex sm:hidden items-center space-x-2 w-full justify-center">
<Button
variant="outline"
size="sm"
onClick={handlePreviousPage}
disabled={currentPage === 1}
className="flex-1 max-w-[80px]"
>
<ChevronLeft className="h-4 w-4 mr-1" />
</Button>
<div className="flex items-center space-x-1 px-2">
{(() => {
const maxVisiblePages = 3
const startPage = Math.max(1, currentPage - 1)
const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1)
const pages = []
// 如果不在第一頁,顯示第一頁和省略號
if (startPage > 1) {
pages.push(
<Button
key={1}
variant="outline"
size="sm"
onClick={() => handlePageChange(1)}
className="w-8 h-8 p-0"
>
1
</Button>
)
if (startPage > 2) {
pages.push(
<span key="ellipsis1" className="text-muted-foreground px-1">
...
</span>
)
}
}
// 顯示當前頁附近的頁碼
for (let i = startPage; i <= endPage; i++) {
pages.push(
<Button
key={i}
variant={currentPage === i ? "default" : "outline"}
size="sm"
onClick={() => handlePageChange(i)}
className="w-8 h-8 p-0"
>
{i}
</Button>
)
}
// 如果不在最後一頁,顯示省略號和最後一頁
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
pages.push(
<span key="ellipsis2" className="text-muted-foreground px-1">
...
</span>
)
}
pages.push(
<Button
key={totalPages}
variant="outline"
size="sm"
onClick={() => handlePageChange(totalPages)}
className="w-8 h-8 p-0"
>
{totalPages}
</Button>
)
}
return pages
})()}
</div>
<Button
variant="outline"
size="sm"
onClick={handleNextPage}
disabled={currentPage === totalPages}
className="flex-1 max-w-[80px]"
>
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
</div>
)}
{/* Edit User Dialog */}
<Dialog open={!!editingUser} onOpenChange={() => setEditingUser(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="edit-name"></Label>
<Input
id="edit-name"
value={newUser.name}
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
placeholder="請輸入姓名"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-email"></Label>
<Input
id="edit-email"
type="email"
value={newUser.email}
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
placeholder="請輸入電子郵件"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-department"></Label>
<Select
value={newUser.department}
onValueChange={(value) => setNewUser({ ...newUser, department: value })}
>
<SelectTrigger>
<SelectValue placeholder="請選擇部門" />
</SelectTrigger>
<SelectContent>
{departments.map((dept) => (
<SelectItem key={dept} value={dept}>
{dept}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="edit-role"></Label>
<Select
value={newUser.role}
onValueChange={(value: "user" | "admin") => setNewUser({ ...newUser, role: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user"></SelectItem>
<SelectItem value="admin"></SelectItem>
</SelectContent>
</Select>
</div>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="flex gap-2">
<Button onClick={handleUpdateUser} className="flex-1">
</Button>
<Button variant="outline" onClick={() => setEditingUser(null)} className="flex-1">
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={!!deletingUser} onOpenChange={() => setDeletingUser(null)}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-destructive/10 flex items-center justify-center">
<Trash2 className="w-4 h-4 text-destructive" />
</div>
</DialogTitle>
<DialogDescription className="text-left">
</DialogDescription>
</DialogHeader>
{deletingUser && (
<div className="space-y-4">
<div className="p-4 bg-muted/50 rounded-lg border">
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground"></span>
<span className="text-sm font-medium">{deletingUser.name}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground"></span>
<span className="text-sm">{deletingUser.email}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground"></span>
<span className="text-sm">{deletingUser.department}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground"></span>
<Badge variant={deletingUser.role === "admin" ? "default" : "secondary"} className="text-xs">
{deletingUser.role === "admin" ? "管理員" : "一般用戶"}
</Badge>
</div>
</div>
</div>
<div className="bg-destructive/5 border border-destructive/20 rounded-lg p-3">
<div className="flex items-start gap-2">
<div className="w-4 h-4 rounded-full bg-destructive/20 flex items-center justify-center mt-0.5 flex-shrink-0">
<div className="w-2 h-2 rounded-full bg-destructive"></div>
</div>
<div className="text-sm text-destructive/80">
<p className="font-medium"></p>
<p></p>
</div>
</div>
</div>
</div>
)}
<div className="flex gap-3 pt-4">
<Button
variant="outline"
onClick={() => setDeletingUser(null)}
className="flex-1"
>
</Button>
<Button
variant="destructive"
onClick={confirmDeleteUser}
className="flex-1"
>
<Trash2 className="w-4 h-4 mr-2" />
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</div>
)
}