init2
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -22,53 +22,23 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
|||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Users, Settings, Target, MessageSquare, Plus, Edit, Trash2, Shield, Database, Bell } from "lucide-react"
|
import { Users, Settings, Target, MessageSquare, Plus, Edit, Trash2, Shield, Database, Bell } from "lucide-react"
|
||||||
|
|
||||||
// Mock data with colors
|
// 用戶介面
|
||||||
const users = [
|
interface User {
|
||||||
{
|
id: string
|
||||||
id: 1,
|
name: string
|
||||||
name: "陳雅雯",
|
email: string
|
||||||
email: "sarah.chen@company.com",
|
role: "executive" | "manager" | "hr"
|
||||||
role: "executive",
|
avatar_url?: string
|
||||||
status: "active",
|
created_at: string
|
||||||
lastLogin: "2024-02-10",
|
updated_at: string
|
||||||
department: "業務部",
|
}
|
||||||
position: "業務副總",
|
|
||||||
color: "emerald",
|
// 用戶顏色映射
|
||||||
},
|
const getUserColor = (id: string) => {
|
||||||
{
|
const colors = ["emerald", "blue", "purple", "orange", "cyan", "pink", "indigo", "yellow"]
|
||||||
id: 2,
|
const index = id.charCodeAt(id.length - 1) % colors.length
|
||||||
name: "王志明",
|
return colors[index]
|
||||||
email: "mike.wang@company.com",
|
}
|
||||||
role: "manager",
|
|
||||||
status: "active",
|
|
||||||
lastLogin: "2024-02-09",
|
|
||||||
department: "技術部",
|
|
||||||
position: "技術長",
|
|
||||||
color: "blue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "李美玲",
|
|
||||||
email: "lisa.lee@company.com",
|
|
||||||
role: "executive",
|
|
||||||
status: "inactive",
|
|
||||||
lastLogin: "2024-01-28",
|
|
||||||
department: "行銷部",
|
|
||||||
position: "行銷長",
|
|
||||||
color: "purple",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "張建國",
|
|
||||||
email: "john.chang@company.com",
|
|
||||||
role: "manager",
|
|
||||||
status: "active",
|
|
||||||
lastLogin: "2024-02-08",
|
|
||||||
department: "研發部",
|
|
||||||
position: "研發總監",
|
|
||||||
color: "orange",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const kpiTemplates = [
|
const kpiTemplates = [
|
||||||
{ id: 1, name: "營收成長率", category: "財務", defaultWeight: 30, isActive: true, usageCount: 15, color: "emerald" },
|
{ id: 1, name: "營收成長率", category: "財務", defaultWeight: 30, isActive: true, usageCount: 15, color: "emerald" },
|
||||||
@@ -106,15 +76,103 @@ export default function AdminPanel() {
|
|||||||
const [activeTab, setActiveTab] = useState("users")
|
const [activeTab, setActiveTab] = useState("users")
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||||
const [dialogType, setDialogType] = useState("")
|
const [dialogType, setDialogType] = useState("")
|
||||||
|
const [users, setUsers] = useState<User[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [editingUser, setEditingUser] = useState<User | null>(null)
|
||||||
|
const [editForm, setEditForm] = useState({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
role: "manager" as "executive" | "manager" | "hr"
|
||||||
|
})
|
||||||
|
|
||||||
|
// 載入用戶數據
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsers()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const response = await fetch('/api/users')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('載入用戶數據失敗')
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
setUsers(data)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : '載入用戶數據失敗')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleOpenDialog = (type: string) => {
|
const handleOpenDialog = (type: string) => {
|
||||||
setDialogType(type)
|
setDialogType(type)
|
||||||
setIsDialogOpen(true)
|
setIsDialogOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleEditUser = (user: User) => {
|
||||||
|
setEditingUser(user)
|
||||||
|
setEditForm({
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role
|
||||||
|
})
|
||||||
|
setDialogType("edit-user")
|
||||||
|
setIsDialogOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateUser = async () => {
|
||||||
|
if (!editingUser) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/users/${editingUser.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(editForm),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('更新用戶失敗')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新載入用戶數據
|
||||||
|
await fetchUsers()
|
||||||
|
setIsDialogOpen(false)
|
||||||
|
setEditingUser(null)
|
||||||
|
setEditForm({ name: "", email: "", role: "manager" })
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : '更新用戶失敗')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteUser = async (userId: string) => {
|
||||||
|
if (!confirm('確定要刪除此用戶嗎?此操作無法復原。')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/users/${userId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('刪除用戶失敗')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新載入用戶數據
|
||||||
|
await fetchUsers()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : '刪除用戶失敗')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const adminStats = {
|
const adminStats = {
|
||||||
totalUsers: users.length,
|
totalUsers: users.length,
|
||||||
activeUsers: users.filter((u) => u.status === "active").length,
|
activeUsers: users.length, // 所有用戶都視為活躍
|
||||||
totalKPIs: kpiTemplates.length,
|
totalKPIs: kpiTemplates.length,
|
||||||
activeKPIs: kpiTemplates.filter((k) => k.isActive).length,
|
activeKPIs: kpiTemplates.filter((k) => k.isActive).length,
|
||||||
totalQuestions: questionTemplates.length,
|
totalQuestions: questionTemplates.length,
|
||||||
@@ -242,74 +300,97 @@ export default function AdminPanel() {
|
|||||||
新增用戶
|
新增用戶
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
{error && (
|
||||||
<TableRow>
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
||||||
<TableHead>用戶</TableHead>
|
{error}
|
||||||
<TableHead>部門/職位</TableHead>
|
</div>
|
||||||
<TableHead>角色</TableHead>
|
)}
|
||||||
<TableHead>狀態</TableHead>
|
|
||||||
<TableHead>最後登入</TableHead>
|
{loading ? (
|
||||||
<TableHead>操作</TableHead>
|
<div className="text-center py-8">
|
||||||
</TableRow>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto"></div>
|
||||||
</TableHeader>
|
<p className="mt-2 text-gray-600">載入用戶數據中...</p>
|
||||||
<TableBody>
|
</div>
|
||||||
{users.map((user) => (
|
) : (
|
||||||
<TableRow key={user.id}>
|
<Table>
|
||||||
<TableCell>
|
<TableHeader>
|
||||||
<div className="flex items-center space-x-3">
|
<TableRow>
|
||||||
<Avatar className="h-8 w-8">
|
<TableHead>用戶</TableHead>
|
||||||
<AvatarFallback
|
<TableHead>角色</TableHead>
|
||||||
className={`bg-gradient-to-br ${
|
<TableHead>建立時間</TableHead>
|
||||||
user.color === "emerald"
|
<TableHead>操作</TableHead>
|
||||||
? "from-emerald-400 to-green-500"
|
|
||||||
: user.color === "blue"
|
|
||||||
? "from-blue-400 to-cyan-500"
|
|
||||||
: user.color === "purple"
|
|
||||||
? "from-purple-400 to-violet-500"
|
|
||||||
: "from-orange-400 to-amber-500"
|
|
||||||
} text-white`}
|
|
||||||
>
|
|
||||||
{user.name.charAt(0)}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">{user.name}</div>
|
|
||||||
<div className="text-sm text-gray-500">{user.email}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">{user.department}</div>
|
|
||||||
<div className="text-sm text-gray-500">{user.position}</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant={user.role === "executive" ? "default" : "secondary"}>
|
|
||||||
{user.role === "executive" ? "高階主管" : user.role === "manager" ? "經理" : "HR"}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant={user.status === "active" ? "default" : "secondary"}>
|
|
||||||
{user.status === "active" ? "啟用" : "停用"}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-sm text-gray-500">{user.lastLogin}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button variant="ghost" size="sm">
|
|
||||||
<Edit className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" className="text-red-500">
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{users.map((user) => {
|
||||||
|
const userColor = getUserColor(user.id)
|
||||||
|
return (
|
||||||
|
<TableRow key={user.id}>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Avatar className="h-8 w-8">
|
||||||
|
<AvatarFallback
|
||||||
|
className={`bg-gradient-to-br ${
|
||||||
|
userColor === "emerald"
|
||||||
|
? "from-emerald-400 to-green-500"
|
||||||
|
: userColor === "blue"
|
||||||
|
? "from-blue-400 to-cyan-500"
|
||||||
|
: userColor === "purple"
|
||||||
|
? "from-purple-400 to-violet-500"
|
||||||
|
: userColor === "orange"
|
||||||
|
? "from-orange-400 to-amber-500"
|
||||||
|
: userColor === "cyan"
|
||||||
|
? "from-cyan-400 to-teal-500"
|
||||||
|
: userColor === "pink"
|
||||||
|
? "from-pink-400 to-rose-500"
|
||||||
|
: userColor === "indigo"
|
||||||
|
? "from-indigo-400 to-blue-500"
|
||||||
|
: "from-yellow-400 to-amber-500"
|
||||||
|
} text-white`}
|
||||||
|
>
|
||||||
|
{user.name.charAt(0)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{user.name}</div>
|
||||||
|
<div className="text-sm text-gray-500">{user.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={user.role === "executive" ? "default" : "secondary"}>
|
||||||
|
{user.role === "executive" ? "高階主管" : user.role === "manager" ? "經理" : "HR"}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm text-gray-500">
|
||||||
|
{new Date(user.created_at).toLocaleDateString('zh-TW')}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEditUser(user)}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-red-500"
|
||||||
|
onClick={() => handleDeleteUser(user.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* KPI Templates Tab */}
|
{/* KPI Templates Tab */}
|
||||||
@@ -474,36 +555,56 @@ export default function AdminPanel() {
|
|||||||
<DialogTitle className="bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">
|
<DialogTitle className="bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">
|
||||||
{dialogType === "user"
|
{dialogType === "user"
|
||||||
? "新增用戶"
|
? "新增用戶"
|
||||||
: dialogType === "kpi"
|
: dialogType === "edit-user"
|
||||||
? "新增 KPI 模板"
|
? "編輯用戶"
|
||||||
: dialogType === "question"
|
: dialogType === "kpi"
|
||||||
? "新增審查問題"
|
? "新增 KPI 模板"
|
||||||
: "備份資料"}
|
: dialogType === "question"
|
||||||
|
? "新增審查問題"
|
||||||
|
: "備份資料"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{dialogType === "user"
|
{dialogType === "user"
|
||||||
? "建立新的系統用戶帳號"
|
? "建立新的系統用戶帳號"
|
||||||
: dialogType === "kpi"
|
: dialogType === "edit-user"
|
||||||
? "建立新的 KPI 模板"
|
? "修改用戶資訊"
|
||||||
: dialogType === "question"
|
: dialogType === "kpi"
|
||||||
? "建立新的審查問題"
|
? "建立新的 KPI 模板"
|
||||||
: "備份系統資料到安全位置"}
|
: dialogType === "question"
|
||||||
|
? "建立新的審查問題"
|
||||||
|
: "備份系統資料到安全位置"}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{dialogType === "user" && (
|
{(dialogType === "user" || dialogType === "edit-user") && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="userName">姓名</Label>
|
<Label htmlFor="userName">姓名</Label>
|
||||||
<Input id="userName" placeholder="輸入用戶姓名" />
|
<Input
|
||||||
|
id="userName"
|
||||||
|
placeholder="輸入用戶姓名"
|
||||||
|
value={editForm.name}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="userEmail">電子郵件</Label>
|
<Label htmlFor="userEmail">電子郵件</Label>
|
||||||
<Input id="userEmail" type="email" placeholder="輸入電子郵件" />
|
<Input
|
||||||
|
id="userEmail"
|
||||||
|
type="email"
|
||||||
|
placeholder="輸入電子郵件"
|
||||||
|
value={editForm.email}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, email: e.target.value })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="userRole">角色</Label>
|
<Label htmlFor="userRole">角色</Label>
|
||||||
<Select>
|
<Select
|
||||||
|
value={editForm.role}
|
||||||
|
onValueChange={(value: "executive" | "manager" | "hr") =>
|
||||||
|
setEditForm({ ...editForm, role: value })
|
||||||
|
}
|
||||||
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="選擇角色" />
|
<SelectValue placeholder="選擇角色" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -514,10 +615,6 @@ export default function AdminPanel() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Label htmlFor="department">部門</Label>
|
|
||||||
<Input id="department" placeholder="輸入部門名稱" />
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{dialogType === "kpi" && (
|
{dialogType === "kpi" && (
|
||||||
@@ -593,8 +690,9 @@ export default function AdminPanel() {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white"
|
className="bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white"
|
||||||
|
onClick={dialogType === "edit-user" ? handleUpdateUser : undefined}
|
||||||
>
|
>
|
||||||
{dialogType === "backup" ? "開始備份" : "建立"}
|
{dialogType === "backup" ? "開始備份" : dialogType === "edit-user" ? "更新" : "建立"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
62
app/api/users/[id]/route.ts
Normal file
62
app/api/users/[id]/route.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { userService } from '@/lib/database'
|
||||||
|
|
||||||
|
// 更新用戶
|
||||||
|
export async function PUT(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { name, email, role, avatar_url } = body
|
||||||
|
|
||||||
|
// 驗證必填欄位
|
||||||
|
if (!name || !email || !role) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '姓名、電子郵件和角色為必填欄位' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 驗證角色
|
||||||
|
const validRoles = ['executive', 'manager', 'hr']
|
||||||
|
if (!validRoles.includes(role)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '無效的角色類型' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = await userService.updateUser(params.id, {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
role,
|
||||||
|
avatar_url
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json(updatedUser)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新用戶失敗:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '更新用戶失敗' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刪除用戶
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await userService.deleteUser(params.id)
|
||||||
|
return NextResponse.json({ message: '用戶刪除成功' })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刪除用戶失敗:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '刪除用戶失敗' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user