1009 lines
38 KiB
TypeScript
1009 lines
38 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import { useCompetition } from "@/contexts/competition-context"
|
||
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 { Badge } from "@/components/ui/badge"
|
||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||
import {
|
||
Users,
|
||
Plus,
|
||
MoreHorizontal,
|
||
Edit,
|
||
Trash2,
|
||
Eye,
|
||
UserPlus,
|
||
UserMinus,
|
||
Crown,
|
||
CheckCircle,
|
||
AlertTriangle,
|
||
Loader2,
|
||
} from "lucide-react"
|
||
import type { Team, TeamMember } from "@/types/competition"
|
||
|
||
export function TeamManagement() {
|
||
const { teams, addTeam, updateTeam, getTeamById } = useCompetition()
|
||
|
||
const [searchTerm, setSearchTerm] = useState("")
|
||
const [selectedDepartment, setSelectedDepartment] = useState("all")
|
||
const [selectedTeam, setSelectedTeam] = useState<Team | null>(null)
|
||
const [showTeamDetail, setShowTeamDetail] = useState(false)
|
||
const [showAddTeam, setShowAddTeam] = useState(false)
|
||
const [showEditTeam, setShowEditTeam] = useState(false)
|
||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||
const [isLoading, setIsLoading] = useState(false)
|
||
const [success, setSuccess] = useState("")
|
||
const [error, setError] = useState("")
|
||
|
||
// 從 API 獲取的團隊數據
|
||
const [apiTeams, setApiTeams] = useState<any[]>([])
|
||
const [isLoadingTeams, setIsLoadingTeams] = useState(true)
|
||
|
||
const [newTeam, setNewTeam] = useState({
|
||
name: "",
|
||
department: "HQBU",
|
||
contactEmail: "",
|
||
members: [] as TeamMember[],
|
||
leader: "",
|
||
apps: [] as string[],
|
||
totalLikes: 0,
|
||
})
|
||
|
||
const [newMember, setNewMember] = useState({
|
||
name: "",
|
||
user_id: "",
|
||
department: "HQBU",
|
||
role: "成員",
|
||
})
|
||
|
||
// 可用用戶狀態
|
||
const [availableUsers, setAvailableUsers] = useState<any[]>([])
|
||
const [isLoadingUsers, setIsLoadingUsers] = useState(false)
|
||
|
||
// 獲取團隊數據
|
||
const fetchTeams = async () => {
|
||
try {
|
||
setIsLoadingTeams(true)
|
||
const response = await fetch('/api/admin/teams')
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
setApiTeams(data.data)
|
||
} else {
|
||
setError('獲取團隊數據失敗')
|
||
}
|
||
} catch (error) {
|
||
setError('獲取團隊數據失敗')
|
||
} finally {
|
||
setIsLoadingTeams(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
fetchTeams()
|
||
fetchAvailableUsers()
|
||
}, [])
|
||
|
||
// 獲取可用用戶列表
|
||
const fetchAvailableUsers = async () => {
|
||
try {
|
||
setIsLoadingUsers(true)
|
||
const response = await fetch('/api/admin/users/available')
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
setAvailableUsers(data.data)
|
||
} else {
|
||
setError('獲取用戶列表失敗')
|
||
}
|
||
} catch (error) {
|
||
setError('獲取用戶列表失敗')
|
||
} finally {
|
||
setIsLoadingUsers(false)
|
||
}
|
||
}
|
||
|
||
const filteredTeams = apiTeams.filter((team) => {
|
||
const matchesSearch =
|
||
team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
team.leader_name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||
const matchesDepartment = selectedDepartment === "all" || team.department === selectedDepartment
|
||
return matchesSearch && matchesDepartment
|
||
})
|
||
|
||
const handleViewTeam = (team: Team) => {
|
||
setSelectedTeam(team)
|
||
setShowTeamDetail(true)
|
||
}
|
||
|
||
const handleEditTeam = async (team: any) => {
|
||
setSelectedTeam(team)
|
||
|
||
try {
|
||
// 獲取團隊的詳細信息,包括成員
|
||
const response = await fetch(`/api/admin/teams/${team.id}`)
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
const teamDetails = data.data
|
||
|
||
// 確保成員數據結構正確
|
||
const members = teamDetails.members && Array.isArray(teamDetails.members)
|
||
? teamDetails.members.map((member: any) => ({
|
||
id: member.user_id || member.id,
|
||
user_id: member.user_id || member.id,
|
||
name: member.name,
|
||
department: member.department,
|
||
role: member.role || '成員'
|
||
}))
|
||
: []
|
||
|
||
setNewTeam({
|
||
name: teamDetails.name,
|
||
department: teamDetails.department,
|
||
contactEmail: teamDetails.contact_email || teamDetails.contactEmail,
|
||
description: teamDetails.description || '',
|
||
members: members,
|
||
leader: teamDetails.leader_id || teamDetails.leader,
|
||
apps: teamDetails.apps || [],
|
||
totalLikes: teamDetails.total_likes || teamDetails.totalLikes || 0,
|
||
})
|
||
setShowEditTeam(true)
|
||
} else {
|
||
setError('獲取團隊詳情失敗')
|
||
}
|
||
} catch (error) {
|
||
console.error('獲取團隊詳情失敗:', error)
|
||
setError('獲取團隊詳情失敗')
|
||
}
|
||
}
|
||
|
||
const handleDeleteTeam = (team: Team) => {
|
||
setSelectedTeam(team)
|
||
setShowDeleteConfirm(true)
|
||
}
|
||
|
||
const confirmDeleteTeam = () => {
|
||
if (selectedTeam) {
|
||
// In a real app, you would call a delete function here
|
||
setShowDeleteConfirm(false)
|
||
setSelectedTeam(null)
|
||
setSuccess("團隊刪除成功!")
|
||
setTimeout(() => setSuccess(""), 3000)
|
||
}
|
||
}
|
||
|
||
const handleAddMember = () => {
|
||
if (!newMember.user_id || !newMember.name.trim()) {
|
||
setError("請選擇成員")
|
||
return
|
||
}
|
||
|
||
// 檢查是否已經添加過這個成員
|
||
if (newTeam.members.some(member => member.user_id === newMember.user_id)) {
|
||
setError("該成員已經在團隊中")
|
||
return
|
||
}
|
||
|
||
const member: TeamMember = {
|
||
id: newMember.user_id, // 使用真實的用戶 ID
|
||
user_id: newMember.user_id,
|
||
name: newMember.name,
|
||
department: newMember.department,
|
||
role: newMember.role,
|
||
}
|
||
|
||
setNewTeam({
|
||
...newTeam,
|
||
members: [...newTeam.members, member],
|
||
})
|
||
|
||
// Set as leader if it's the first member
|
||
if (newTeam.members.length === 0) {
|
||
setNewTeam((prev) => ({
|
||
...prev,
|
||
leader: member.id,
|
||
members: [...prev.members, { ...member, role: "隊長" }],
|
||
}))
|
||
}
|
||
|
||
setNewMember({
|
||
name: "",
|
||
user_id: "",
|
||
department: "HQBU",
|
||
role: "成員",
|
||
})
|
||
setError("")
|
||
}
|
||
|
||
const handleRemoveMember = (memberId: string) => {
|
||
const updatedMembers = newTeam.members.filter((m) => m.id !== memberId)
|
||
let newLeader = newTeam.leader
|
||
|
||
// If removing the leader, assign leadership to the first remaining member
|
||
if (memberId === newTeam.leader && updatedMembers.length > 0) {
|
||
newLeader = updatedMembers[0].id
|
||
updatedMembers[0].role = "隊長"
|
||
}
|
||
|
||
setNewTeam({
|
||
...newTeam,
|
||
members: updatedMembers,
|
||
leader: newLeader,
|
||
})
|
||
}
|
||
|
||
const handleSetLeader = (memberId: string) => {
|
||
const updatedMembers = newTeam.members.map((member) => ({
|
||
...member,
|
||
role: member.id === memberId ? "隊長" : "成員",
|
||
}))
|
||
|
||
setNewTeam({
|
||
...newTeam,
|
||
members: updatedMembers,
|
||
leader: memberId,
|
||
})
|
||
}
|
||
|
||
const handleAddTeam = async () => {
|
||
setError("")
|
||
|
||
if (!newTeam.name || !newTeam.contactEmail || newTeam.members.length === 0) {
|
||
setError("請填寫所有必填欄位並至少添加一名成員")
|
||
return
|
||
}
|
||
|
||
setIsLoading(true)
|
||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||
|
||
addTeam(newTeam)
|
||
setShowAddTeam(false)
|
||
setNewTeam({
|
||
name: "",
|
||
department: "HQBU",
|
||
contactEmail: "",
|
||
members: [],
|
||
leader: "",
|
||
apps: [],
|
||
totalLikes: 0,
|
||
})
|
||
setSuccess("團隊創建成功!")
|
||
setIsLoading(false)
|
||
setTimeout(() => setSuccess(""), 3000)
|
||
}
|
||
|
||
const handleUpdateTeam = async () => {
|
||
if (!selectedTeam) return
|
||
|
||
setError("")
|
||
|
||
if (!newTeam.name || !newTeam.contactEmail || newTeam.members.length === 0) {
|
||
setError("請填寫所有必填欄位並至少添加一名成員")
|
||
return
|
||
}
|
||
|
||
setIsLoading(true)
|
||
|
||
try {
|
||
// 準備更新數據
|
||
const updateData = {
|
||
name: newTeam.name,
|
||
department: newTeam.department,
|
||
contact_email: newTeam.contactEmail,
|
||
description: newTeam.description || null,
|
||
leader_id: newTeam.leader,
|
||
members: newTeam.members.map(member => ({
|
||
user_id: member.user_id || member.id,
|
||
role: member.role
|
||
}))
|
||
}
|
||
|
||
console.log('🔍 準備更新的團隊數據:', updateData)
|
||
console.log('🔍 成員數據:', newTeam.members)
|
||
|
||
// 調用 API 更新團隊
|
||
const response = await fetch(`/api/admin/teams/${selectedTeam.id}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(updateData)
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
// 更新成功,重新載入團隊數據
|
||
await fetchTeams()
|
||
setShowEditTeam(false)
|
||
setSelectedTeam(null)
|
||
setSuccess("團隊更新成功!")
|
||
} else {
|
||
setError(data.message || "更新團隊失敗")
|
||
}
|
||
} catch (error) {
|
||
console.error('更新團隊失敗:', error)
|
||
setError("更新團隊失敗")
|
||
} finally {
|
||
setIsLoading(false)
|
||
setTimeout(() => setError(""), 3000)
|
||
setTimeout(() => setSuccess(""), 3000)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Success/Error Messages */}
|
||
{success && (
|
||
<Alert className="border-green-200 bg-green-50">
|
||
<CheckCircle className="h-4 w-4 text-green-600" />
|
||
<AlertDescription className="text-green-800">{success}</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{error && (
|
||
<Alert variant="destructive">
|
||
<AlertTriangle className="h-4 w-4" />
|
||
<AlertDescription>{error}</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-3xl font-bold text-gray-900">團隊管理</h1>
|
||
<p className="text-gray-600">管理競賽團隊和成員資訊</p>
|
||
</div>
|
||
<Button
|
||
onClick={() => setShowAddTeam(true)}
|
||
className="bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700"
|
||
>
|
||
<Plus className="w-4 h-4 mr-2" />
|
||
新增團隊
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Stats Cards */}
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">總團隊數</p>
|
||
<p className="text-2xl font-bold">{apiTeams.length}</p>
|
||
</div>
|
||
<Users className="w-8 h-8 text-green-600" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">總成員數</p>
|
||
<p className="text-2xl font-bold">{apiTeams.reduce((sum, team) => sum + (team.member_count || 0), 0)}</p>
|
||
</div>
|
||
<UserPlus className="w-8 h-8 text-blue-600" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">平均團隊規模</p>
|
||
<p className="text-2xl font-bold">
|
||
{apiTeams.length > 0
|
||
? Math.round((apiTeams.reduce((sum, team) => sum + (team.member_count || 0), 0) / apiTeams.length) * 10) / 10
|
||
: 0}
|
||
</p>
|
||
</div>
|
||
<Crown className="w-8 h-8 text-yellow-600" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Filters */}
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="flex flex-col lg:flex-row gap-4 items-center">
|
||
<div className="flex-1 relative">
|
||
<Input
|
||
placeholder="搜尋團隊名稱或成員姓名..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<Select value={selectedDepartment} onValueChange={setSelectedDepartment}>
|
||
<SelectTrigger className="w-32">
|
||
<SelectValue placeholder="部門" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">全部部門</SelectItem>
|
||
<SelectItem value="ACBU">ACBU</SelectItem>
|
||
<SelectItem value="AUBU">AUBU</SelectItem>
|
||
<SelectItem value="FAB3">FAB3</SelectItem>
|
||
<SelectItem value="FNBU">FNBU</SelectItem>
|
||
<SelectItem value="HQBU">HQBU</SelectItem>
|
||
<SelectItem value="HRBU">HRBU</SelectItem>
|
||
<SelectItem value="IBU">IBU</SelectItem>
|
||
<SelectItem value="ITBU">ITBU</SelectItem>
|
||
<SelectItem value="MBU1">MBU1</SelectItem>
|
||
<SelectItem value="PBU">PBU</SelectItem>
|
||
<SelectItem value="SBG">SBG</SelectItem>
|
||
<SelectItem value="SBU">SBU</SelectItem>
|
||
<SelectItem value="法務室">法務室</SelectItem>
|
||
<SelectItem value="關係企業發展">關係企業發展</SelectItem>
|
||
<SelectItem value="稽核室">稽核室</SelectItem>
|
||
<SelectItem value="總經理室">總經理室</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Teams Table */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>團隊列表 ({filteredTeams.length})</CardTitle>
|
||
<CardDescription>管理所有競賽團隊</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>團隊名稱</TableHead>
|
||
<TableHead>隊長</TableHead>
|
||
<TableHead>部門</TableHead>
|
||
<TableHead>成員數</TableHead>
|
||
<TableHead>應用數</TableHead>
|
||
<TableHead>總按讚數</TableHead>
|
||
<TableHead>操作</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{filteredTeams.map((team) => {
|
||
return (
|
||
<TableRow key={team.id}>
|
||
<TableCell>
|
||
<div className="flex items-center space-x-3">
|
||
<div className="w-8 h-8 bg-gradient-to-r from-green-500 to-blue-500 rounded-lg flex items-center justify-center">
|
||
<Users className="w-4 h-4 text-white" />
|
||
</div>
|
||
<div>
|
||
<p className="font-medium">{team.name}</p>
|
||
<p className="text-sm text-gray-500">{team.contact_email}</p>
|
||
</div>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="flex items-center space-x-2">
|
||
<Avatar className="w-6 h-6">
|
||
<AvatarFallback className="bg-green-100 text-green-700 text-xs">
|
||
{team.leader_name?.[0] || "?"}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<span className="text-sm">{team.leader_name || "未設定"}</span>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Badge variant="outline" className="bg-gray-100 text-gray-700">
|
||
{team.department}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="flex items-center space-x-1">
|
||
<UserPlus className="w-4 h-4 text-blue-500" />
|
||
<span>{team.member_count || 0}</span>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<span className="font-medium">{team.app_count || 0}</span>
|
||
</TableCell>
|
||
<TableCell>
|
||
<span className="font-medium text-red-600">{team.totalLikes}</span>
|
||
</TableCell>
|
||
<TableCell>
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" size="sm">
|
||
<MoreHorizontal className="w-4 h-4" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end">
|
||
<DropdownMenuItem onClick={() => handleViewTeam(team)}>
|
||
<Eye className="w-4 h-4 mr-2" />
|
||
查看詳情
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => handleEditTeam(team)}>
|
||
<Edit className="w-4 h-4 mr-2" />
|
||
編輯團隊
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem className="text-red-600" onClick={() => handleDeleteTeam(team)}>
|
||
<Trash2 className="w-4 h-4 mr-2" />
|
||
刪除團隊
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</TableCell>
|
||
</TableRow>
|
||
)
|
||
})}
|
||
</TableBody>
|
||
</Table>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Add Team Dialog */}
|
||
<Dialog open={showAddTeam} onOpenChange={setShowAddTeam}>
|
||
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>新增團隊</DialogTitle>
|
||
<DialogDescription>創建一個新的競賽團隊</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<Tabs defaultValue="basic" className="w-full">
|
||
<TabsList className="grid w-full grid-cols-2">
|
||
<TabsTrigger value="basic">基本資訊</TabsTrigger>
|
||
<TabsTrigger value="members">團隊成員</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="basic" className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="teamName">團隊名稱 *</Label>
|
||
<Input
|
||
id="teamName"
|
||
value={newTeam.name}
|
||
onChange={(e) => setNewTeam({ ...newTeam, name: e.target.value })}
|
||
placeholder="輸入團隊名稱"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="teamDepartment">主要部門</Label>
|
||
<Select
|
||
value={newTeam.department}
|
||
onValueChange={(value) => setNewTeam({ ...newTeam, department: value })}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="HQBU">HQBU</SelectItem>
|
||
<SelectItem value="ITBU">ITBU</SelectItem>
|
||
<SelectItem value="MBU1">MBU1</SelectItem>
|
||
<SelectItem value="SBU">SBU</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="contactEmail">聯絡信箱 *</Label>
|
||
<Input
|
||
id="contactEmail"
|
||
type="email"
|
||
value={newTeam.contactEmail}
|
||
onChange={(e) => setNewTeam({ ...newTeam, contactEmail: e.target.value })}
|
||
placeholder="team@company.com"
|
||
/>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="members" className="space-y-4">
|
||
<div className="space-y-4">
|
||
<h4 className="font-semibold">新增成員</h4>
|
||
<div className="grid grid-cols-3 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="memberName">成員姓名</Label>
|
||
<Input
|
||
id="memberName"
|
||
value={newMember.name}
|
||
onChange={(e) => setNewMember({ ...newMember, name: e.target.value })}
|
||
placeholder="輸入成員姓名"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="memberDepartment">部門</Label>
|
||
<Select
|
||
value={newMember.department}
|
||
onValueChange={(value) => setNewMember({ ...newMember, department: value })}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="HQBU">HQBU</SelectItem>
|
||
<SelectItem value="ITBU">ITBU</SelectItem>
|
||
<SelectItem value="MBU1">MBU1</SelectItem>
|
||
<SelectItem value="SBU">SBU</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="memberRole">角色</Label>
|
||
<Input
|
||
id="memberRole"
|
||
value={newMember.role}
|
||
onChange={(e) => setNewMember({ ...newMember, role: e.target.value })}
|
||
placeholder="例如:開發工程師"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<Button onClick={handleAddMember} variant="outline" className="w-full bg-transparent">
|
||
<UserPlus className="w-4 h-4 mr-2" />
|
||
新增成員
|
||
</Button>
|
||
</div>
|
||
|
||
{newTeam.members.length > 0 && (
|
||
<div className="space-y-4">
|
||
<h4 className="font-semibold">團隊成員 ({newTeam.members.length})</h4>
|
||
<div className="space-y-2">
|
||
{newTeam.members.map((member) => (
|
||
<div key={member.id} className="flex items-center justify-between p-3 border rounded-lg">
|
||
<div className="flex items-center space-x-3">
|
||
<Avatar className="w-8 h-8">
|
||
<AvatarFallback className="bg-green-100 text-green-700 text-sm">
|
||
{member.name[0]}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<div>
|
||
<div className="flex items-center space-x-2">
|
||
<span className="font-medium">{member.name}</span>
|
||
{member.id === newTeam.leader && (
|
||
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
|
||
隊長
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
<div className="text-sm text-gray-600">
|
||
{member.department} • {member.role}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
{member.id !== newTeam.leader && (
|
||
<Button variant="outline" size="sm" onClick={() => handleSetLeader(member.id)}>
|
||
<Crown className="w-4 h-4 mr-1" />
|
||
設為隊長
|
||
</Button>
|
||
)}
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleRemoveMember(member.id)}
|
||
className="text-red-600 border-red-300 hover:bg-red-50"
|
||
>
|
||
<UserMinus className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
<div className="flex justify-end space-x-3">
|
||
<Button variant="outline" onClick={() => setShowAddTeam(false)}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleAddTeam} disabled={isLoading}>
|
||
{isLoading ? (
|
||
<>
|
||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||
創建中...
|
||
</>
|
||
) : (
|
||
"創建團隊"
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Edit Team Dialog */}
|
||
<Dialog open={showEditTeam} onOpenChange={setShowEditTeam}>
|
||
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>編輯團隊</DialogTitle>
|
||
<DialogDescription>修改團隊資訊和成員</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<Tabs defaultValue="basic" className="w-full">
|
||
<TabsList className="grid w-full grid-cols-2">
|
||
<TabsTrigger value="basic">基本資訊</TabsTrigger>
|
||
<TabsTrigger value="members">團隊成員</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="basic" className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="editTeamName">團隊名稱 *</Label>
|
||
<Input
|
||
id="editTeamName"
|
||
value={newTeam.name}
|
||
onChange={(e) => setNewTeam({ ...newTeam, name: e.target.value })}
|
||
placeholder="輸入團隊名稱"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="editTeamDepartment">主要部門</Label>
|
||
<Select
|
||
value={newTeam.department}
|
||
onValueChange={(value) => setNewTeam({ ...newTeam, department: value })}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="HQBU">HQBU</SelectItem>
|
||
<SelectItem value="ITBU">ITBU</SelectItem>
|
||
<SelectItem value="MBU1">MBU1</SelectItem>
|
||
<SelectItem value="SBU">SBU</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="editContactEmail">聯絡信箱 *</Label>
|
||
<Input
|
||
id="editContactEmail"
|
||
type="email"
|
||
value={newTeam.contactEmail}
|
||
onChange={(e) => setNewTeam({ ...newTeam, contactEmail: e.target.value })}
|
||
placeholder="team@company.com"
|
||
/>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="members" className="space-y-4">
|
||
<div className="space-y-4">
|
||
<h4 className="font-semibold">新增成員</h4>
|
||
<div className="grid grid-cols-3 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="editMemberSelect">選擇成員</Label>
|
||
<Select
|
||
value={newMember.user_id}
|
||
onValueChange={(value) => {
|
||
const selectedUser = availableUsers.find(user => user.id === value)
|
||
setNewMember({
|
||
...newMember,
|
||
user_id: value,
|
||
name: selectedUser?.name || "",
|
||
department: selectedUser?.department || "HQBU"
|
||
})
|
||
}}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="選擇團隊成員" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{availableUsers.map((user) => (
|
||
<SelectItem key={user.id} value={user.id}>
|
||
{user.name} ({user.department})
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="editMemberDepartment">部門</Label>
|
||
<Select
|
||
value={newMember.department}
|
||
onValueChange={(value) => setNewMember({ ...newMember, department: value })}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="HQBU">HQBU</SelectItem>
|
||
<SelectItem value="ITBU">ITBU</SelectItem>
|
||
<SelectItem value="MBU1">MBU1</SelectItem>
|
||
<SelectItem value="SBU">SBU</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="editMemberRole">角色</Label>
|
||
<Input
|
||
id="editMemberRole"
|
||
value={newMember.role}
|
||
onChange={(e) => setNewMember({ ...newMember, role: e.target.value })}
|
||
placeholder="例如:開發工程師"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<Button onClick={handleAddMember} variant="outline" className="w-full bg-transparent">
|
||
<UserPlus className="w-4 h-4 mr-2" />
|
||
新增成員
|
||
</Button>
|
||
</div>
|
||
|
||
{newTeam.members.length > 0 && (
|
||
<div className="space-y-4">
|
||
<h4 className="font-semibold">團隊成員 ({newTeam.members.length})</h4>
|
||
<div className="space-y-2">
|
||
{newTeam.members.map((member) => (
|
||
<div key={member.id} className="flex items-center justify-between p-3 border rounded-lg">
|
||
<div className="flex items-center space-x-3">
|
||
<Avatar className="w-8 h-8">
|
||
<AvatarFallback className="bg-green-100 text-green-700 text-sm">
|
||
{member.name[0]}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<div>
|
||
<div className="flex items-center space-x-2">
|
||
<span className="font-medium">{member.name}</span>
|
||
{member.id === newTeam.leader && (
|
||
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
|
||
隊長
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
<div className="text-sm text-gray-600">
|
||
{member.department} • {member.role}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
{member.id !== newTeam.leader && (
|
||
<Button variant="outline" size="sm" onClick={() => handleSetLeader(member.id)}>
|
||
<Crown className="w-4 h-4 mr-1" />
|
||
設為隊長
|
||
</Button>
|
||
)}
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleRemoveMember(member.id)}
|
||
className="text-red-600 border-red-300 hover:bg-red-50"
|
||
>
|
||
<UserMinus className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
<div className="flex justify-end space-x-3">
|
||
<Button variant="outline" onClick={() => setShowEditTeam(false)}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleUpdateTeam} disabled={isLoading}>
|
||
{isLoading ? (
|
||
<>
|
||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||
更新中...
|
||
</>
|
||
) : (
|
||
"更新團隊"
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Delete Confirmation Dialog */}
|
||
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center space-x-2">
|
||
<AlertTriangle className="w-5 h-5 text-red-500" />
|
||
<span>確認刪除團隊</span>
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
您確定要刪除團隊「{selectedTeam?.name}」嗎?
|
||
<br />
|
||
<span className="text-red-600 font-medium">此操作無法復原,將會永久刪除團隊的所有資料。</span>
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="flex justify-end space-x-3 mt-6">
|
||
<Button variant="outline" onClick={() => setShowDeleteConfirm(false)}>
|
||
取消
|
||
</Button>
|
||
<Button variant="destructive" onClick={confirmDeleteTeam}>
|
||
<Trash2 className="w-4 h-4 mr-2" />
|
||
確認刪除
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Team Detail Dialog */}
|
||
<Dialog open={showTeamDetail} onOpenChange={setShowTeamDetail}>
|
||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>團隊詳情</DialogTitle>
|
||
<DialogDescription>查看團隊的詳細資訊</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
{selectedTeam && (
|
||
<div className="space-y-6">
|
||
<div className="flex items-start space-x-4">
|
||
<div className="w-16 h-16 bg-gradient-to-r from-green-500 to-blue-500 rounded-xl flex items-center justify-center">
|
||
<Users className="w-8 h-8 text-white" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h3 className="text-xl font-semibold">{selectedTeam.name}</h3>
|
||
<p className="text-gray-600 mb-2">{selectedTeam.contact_email}</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Badge variant="outline" className="bg-gray-100 text-gray-700">
|
||
{selectedTeam.department}
|
||
</Badge>
|
||
<Badge variant="outline" className="bg-blue-100 text-blue-700">
|
||
{selectedTeam.member_count || 0} 名成員
|
||
</Badge>
|
||
<Badge variant="outline" className="bg-green-100 text-green-700">
|
||
{selectedTeam.app_count || 0} 個應用
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<p className="text-sm text-gray-500">團隊ID</p>
|
||
<p className="font-medium">{selectedTeam.id}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-500">總按讚數</p>
|
||
<p className="font-medium text-red-600">{selectedTeam.totalLikes}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h4 className="font-semibold mb-3">團隊成員</h4>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{selectedTeam.members.map((member) => (
|
||
<div key={member.id} className="flex items-center space-x-3 p-3 border rounded-lg">
|
||
<Avatar className="w-10 h-10">
|
||
<AvatarFallback className="bg-green-100 text-green-700">{member.name[0]}</AvatarFallback>
|
||
</Avatar>
|
||
<div className="flex-1">
|
||
<div className="flex items-center space-x-2">
|
||
<span className="font-medium">{member.name}</span>
|
||
{member.id === selectedTeam.leader && (
|
||
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
|
||
隊長
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
<div className="text-sm text-gray-600">
|
||
{member.department} • {member.role}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
)
|
||
}
|