修正團體管理的 BUG

This commit is contained in:
2025-09-19 18:36:35 +08:00
parent 95c0c4cb23
commit 8ec5ead183
11 changed files with 367 additions and 137 deletions

View File

@@ -517,7 +517,8 @@ export function AppManagement() {
type: newApp.type,
app_url: newApp.appUrl,
icon: newApp.icon,
icon_color: newApp.iconColor
icon_color: newApp.iconColor,
department: newApp.department
})
})

View File

@@ -563,7 +563,13 @@ export function CompetitionManagement() {
contactEmail: teamDetails.contact_email || '',
leaderPhone: teamDetails.leader_phone || '',
description: teamDetails.description || '',
members: teamDetails.members || [],
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 || '成員'
})),
apps: teamDetails.apps ? teamDetails.apps.map((app: any) => app.id || app) : [],
submittedAppCount: teamDetails.apps?.length || 0,
}
@@ -1142,7 +1148,7 @@ export function CompetitionManagement() {
contact_email: newTeam.contactEmail,
description: newTeam.description,
members: newTeam.members.map(member => ({
user_id: member.id, // 現在 member.id 就是 user_id
user_id: member.user_id || member.id, // 確保使用正確的 user_id
role: member.role || 'member'
})),
apps: newTeam.apps // 添加應用 ID 列表
@@ -1164,7 +1170,7 @@ export function CompetitionManagement() {
contact_email: newTeam.contactEmail,
description: newTeam.description,
members: newTeam.members.map(member => ({
user_id: member.id, // 現在 member.id 就是 user_id
user_id: member.user_id || member.id, // 確保使用正確的 user_id
role: member.role || 'member'
})),
apps: newTeam.apps // 添加應用 ID 列表
@@ -2136,6 +2142,19 @@ export function CompetitionManagement() {
}
}
const getCompetitionTypeColor = (type: string) => {
switch (type) {
case "individual":
return "bg-blue-100 text-blue-800 border-blue-200"
case "team":
return "bg-purple-100 text-purple-800 border-purple-200"
case "mixed":
return "bg-orange-100 text-orange-800 border-orange-200"
default:
return "bg-gray-100 text-gray-800 border-gray-200"
}
}
const getScoreLabelText = (key: string) => {
switch (key) {
case "innovation":
@@ -2464,13 +2483,13 @@ export function CompetitionManagement() {
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-[300px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -2506,9 +2525,9 @@ export function CompetitionManagement() {
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 min-w-[100px]">
{getCompetitionTypeIcon(competition.type)}
<Badge variant="outline" className="text-xs">
<Badge variant="outline" className={`text-xs px-2 py-1 whitespace-nowrap ${getCompetitionTypeColor(competition.type)}`}>
{getCompetitionTypeText(competition.type)}
</Badge>
</div>
@@ -2527,34 +2546,37 @@ export function CompetitionManagement() {
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className={getStatusColor(competition.status)}>
{getStatusText(competition.status)}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center space-x-1">
{getCompetitionTypeIcon(competition.type)}
<span>{participantCount} </span>
<div className="min-w-[80px]">
<Badge variant="outline" className={`text-xs px-2 py-1 whitespace-nowrap ${getStatusColor(competition.status)}`}>
{getStatusText(competition.status)}
</Badge>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="flex items-center space-x-1 min-w-[100px]">
{getCompetitionTypeIcon(competition.type)}
<span className="text-sm whitespace-nowrap">{participantCount} </span>
</div>
</TableCell>
<TableCell>
<div className="space-y-1 min-w-[140px]">
<div className="flex items-center space-x-2">
<Progress value={scoringProgress.percentage} className="w-16 h-2" />
<span className="text-xs text-gray-500">
<span className="text-xs text-gray-500 whitespace-nowrap">
{scoringProgress.completed}/{scoringProgress.total}
</span>
</div>
<p className="text-xs text-gray-500">{scoringProgress.percentage}% </p>
<p className="text-xs text-gray-500 whitespace-nowrap">{scoringProgress.percentage}% </p>
</div>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" disabled={isLoading}>
<MoreHorizontal className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<div className="flex justify-center min-w-[60px]">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" disabled={isLoading}>
<MoreHorizontal className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleViewCompetition(competition)}>
<Eye className="w-4 h-4 mr-2" />
@@ -2598,7 +2620,8 @@ export function CompetitionManagement() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</DropdownMenu>
</div>
</TableCell>
</TableRow>
)
@@ -2771,7 +2794,7 @@ export function CompetitionManagement() {
</div>
<div className="flex items-center space-x-2 text-gray-600">
<Trophy className="w-3 h-3" />
<span>{team.submittedAppCount} </span>
<span>{team.app_count || 0} </span>
</div>
</div>
@@ -6603,7 +6626,7 @@ export function CompetitionManagement() {
<div className="flex-1">
<div className="flex items-center space-x-2">
<p className="font-medium text-gray-900">{member.name}</p>
{member.user_id === selectedTeam.leader_id && (
{member.role === '隊長' && (
<Badge variant="default" className="text-xs bg-orange-100 text-orange-800">
</Badge>

View File

@@ -1,6 +1,6 @@
"use client"
import { useState } from "react"
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"
@@ -43,6 +43,10 @@ export function TeamManagement() {
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: "",
@@ -60,10 +64,35 @@ export function TeamManagement() {
role: "成員",
})
const filteredTeams = teams.filter((team) => {
// 獲取團隊數據
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 {
console.error('獲取團隊數據失敗:', data.message)
setError('獲取團隊數據失敗')
}
} catch (error) {
console.error('獲取團隊數據失敗:', error)
setError('獲取團隊數據失敗')
} finally {
setIsLoadingTeams(false)
}
}
useEffect(() => {
fetchTeams()
}, [])
const filteredTeams = apiTeams.filter((team) => {
const matchesSearch =
team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
team.members.some((member) => member.name.toLowerCase().includes(searchTerm.toLowerCase()))
team.leader_name?.toLowerCase().includes(searchTerm.toLowerCase())
const matchesDepartment = selectedDepartment === "all" || team.department === selectedDepartment
return matchesSearch && matchesDepartment
})
@@ -73,16 +102,29 @@ export function TeamManagement() {
setShowTeamDetail(true)
}
const handleEditTeam = (team: Team) => {
const handleEditTeam = (team: any) => {
setSelectedTeam(team)
// 確保成員數據結構正確
const members = team.members && Array.isArray(team.members)
? team.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: team.name,
department: team.department,
contactEmail: team.contactEmail,
members: [...team.members],
leader: team.leader,
apps: [...team.apps],
totalLikes: team.totalLikes,
contactEmail: team.contact_email || team.contactEmail,
description: team.description || '',
members: members,
leader: team.leader_id || team.leader,
apps: team.apps || [],
totalLikes: team.total_likes || team.totalLikes || 0,
})
setShowEditTeam(true)
}
@@ -205,14 +247,49 @@ export function TeamManagement() {
}
setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 1000))
updateTeam(selectedTeam.id, newTeam)
setShowEditTeam(false)
setSelectedTeam(null)
setSuccess("團隊更新成功!")
setIsLoading(false)
setTimeout(() => setSuccess(""), 3000)
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
}))
}
// 調用 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 (
@@ -254,7 +331,7 @@ export function TeamManagement() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{teams.length}</p>
<p className="text-2xl font-bold">{apiTeams.length}</p>
</div>
<Users className="w-8 h-8 text-green-600" />
</div>
@@ -266,7 +343,7 @@ export function TeamManagement() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{teams.reduce((sum, team) => sum + team.members.length, 0)}</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>
@@ -279,8 +356,8 @@ export function TeamManagement() {
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">
{teams.length > 0
? Math.round((teams.reduce((sum, team) => sum + team.members.length, 0) / teams.length) * 10) / 10
{apiTeams.length > 0
? Math.round((apiTeams.reduce((sum, team) => sum + (team.member_count || 0), 0) / apiTeams.length) * 10) / 10
: 0}
</p>
</div>
@@ -352,8 +429,6 @@ export function TeamManagement() {
</TableHeader>
<TableBody>
{filteredTeams.map((team) => {
const leader = team.members.find((m) => m.id === team.leader)
return (
<TableRow key={team.id}>
<TableCell>
@@ -363,7 +438,7 @@ export function TeamManagement() {
</div>
<div>
<p className="font-medium">{team.name}</p>
<p className="text-sm text-gray-500">{team.contactEmail}</p>
<p className="text-sm text-gray-500">{team.contact_email}</p>
</div>
</div>
</TableCell>
@@ -371,10 +446,10 @@ export function TeamManagement() {
<div className="flex items-center space-x-2">
<Avatar className="w-6 h-6">
<AvatarFallback className="bg-green-100 text-green-700 text-xs">
{leader?.name[0] || "?"}
{team.leader_name?.[0] || "?"}
</AvatarFallback>
</Avatar>
<span className="text-sm">{leader?.name || "未設定"}</span>
<span className="text-sm">{team.leader_name || "未設定"}</span>
</div>
</TableCell>
<TableCell>
@@ -385,11 +460,11 @@ export function TeamManagement() {
<TableCell>
<div className="flex items-center space-x-1">
<UserPlus className="w-4 h-4 text-blue-500" />
<span>{team.members.length}</span>
<span>{team.member_count || 0}</span>
</div>
</TableCell>
<TableCell>
<span className="font-medium">{team.apps.length}</span>
<span className="font-medium">{team.app_count || 0}</span>
</TableCell>
<TableCell>
<span className="font-medium text-red-600">{team.totalLikes}</span>
@@ -807,16 +882,16 @@ export function TeamManagement() {
</div>
<div className="flex-1">
<h3 className="text-xl font-semibold">{selectedTeam.name}</h3>
<p className="text-gray-600 mb-2">{selectedTeam.contactEmail}</p>
<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.members.length}
{selectedTeam.member_count || 0}
</Badge>
<Badge variant="outline" className="bg-green-100 text-green-700">
{selectedTeam.apps.length}
{selectedTeam.app_count || 0}
</Badge>
</div>
</div>

View File

@@ -372,7 +372,7 @@ export function CompetitionDetailDialog({
<div
key={member.id}
className={`border rounded-lg p-4 ${
member.id === team.leader ? "border-green-300 bg-green-50" : "border-gray-200"
member.role === '隊長' ? "border-green-300 bg-green-50" : "border-gray-200"
}`}
>
<div className="flex items-center space-x-3">
@@ -383,7 +383,7 @@ export function CompetitionDetailDialog({
<div className="flex-1">
<div className="flex items-center space-x-2">
<h4 className="font-semibold">{member.name}</h4>
{member.id === team.leader && (
{member.role === '隊長' && (
<Badge variant="secondary" className="bg-green-100 text-green-800">
</Badge>

View File

@@ -453,7 +453,7 @@ export function PopularityRankings() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 px-8">
{currentTeams.map((team, index) => {
const globalRank = startIndex + index + 1
const leader = team.members.find((m: any) => m.id === team.leader)
const leader = team.members.find((m: any) => m.role === '隊長')
return (
<Card
@@ -474,7 +474,7 @@ export function PopularityRankings() {
<div className="flex-1 min-w-0">
<h4 className="font-bold text-gray-900 mb-1 truncate">{team.name}</h4>
<p className="text-sm text-gray-600 mb-2">{leader?.name}</p>
<p className="text-sm text-gray-600 mb-2">{leader?.name || '未知隊長'}</p>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="bg-green-100 text-green-800 border-green-200 text-xs">
@@ -496,7 +496,13 @@ export function PopularityRankings() {
{member.name[0]}
</div>
<span className="text-gray-600">{member.name}</span>
<span className="text-gray-400">({member.role})</span>
<span className={`text-xs px-1 py-0.5 rounded ${
member.role === '隊長'
? 'bg-yellow-100 text-yellow-800'
: 'text-gray-400'
}`}>
{member.role}
</span>
</div>
))}
{team.members.length > 3 && (

View File

@@ -71,7 +71,13 @@ const getIconComponent = (iconName: string) => {
// App data for team apps - get from team data
const getAppDetails = (appId: string, team: any) => {
const appDetail = team.appsDetails?.find((app: any) => app.id === appId);
// 首先嘗試從 appsDetails 獲取
let appDetail = team.appsDetails?.find((app: any) => app.id === appId);
// 如果沒有找到,嘗試從 apps 獲取
if (!appDetail) {
appDetail = team.apps?.find((app: any) => app.id === appId);
}
if (appDetail) {
return {
@@ -311,7 +317,7 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
<div className="flex-1">
<div className="flex items-center space-x-2">
<h4 className="font-medium">{member.name}</h4>
{member.id === team.leader && (
{member.role === '隊長' && (
<Badge variant="default" className="bg-yellow-100 text-yellow-800 text-xs">
</Badge>
@@ -334,19 +340,36 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{team.apps.map((appId: string) => {
const app = getAppDetails(appId, team)
{team.apps.map((app: any) => {
// 如果 app 是字符串 ID使用 getAppDetails 獲取詳情
// 如果 app 是對象,直接使用
const appData = typeof app === 'string' ? getAppDetails(app, team) : {
id: app.id,
name: app.name || "未命名應用",
type: app.type || "未知類型",
description: app.description || "無描述",
icon: getIconComponent(app.icon) || Brain,
iconColor: app.icon_color || "from-blue-500 to-purple-500",
author: app.creator_name || "未知作者",
department: app.creator_department || "未知部門",
category: app.category || "未分類",
likes: app.likes_count || 0,
views: app.views_count || 0,
rating: Number(app.rating) || 0,
createdAt: app.created_at
}
// 如果沒有圖標,使用默認的 Brain 圖標
const IconComponent = app.icon || Brain
const likes = app.likes || 0
const views = app.views || 0
const rating = app.rating || 0
const IconComponent = appData.icon || Brain
const likes = appData.likes || 0
const views = appData.views || 0
const rating = appData.rating || 0
return (
<Card
key={appId}
key={appData.id}
className="hover:shadow-md transition-all duration-200 cursor-pointer group"
onClick={() => handleAppClick(appId)}
onClick={() => handleAppClick(appData.id)}
>
<CardContent className="p-4 flex flex-col h-full">
<div className="flex items-start space-x-3 mb-3">
@@ -356,16 +379,16 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
<div className="flex-1">
<div className="flex items-center space-x-2">
<h4 className="font-medium text-gray-900 group-hover:text-blue-600 transition-colors">
{app.name}
{appData.name}
</h4>
<ExternalLink className="w-3 h-3 text-gray-400 group-hover:text-blue-500 opacity-0 group-hover:opacity-100 transition-all" />
</div>
<Badge variant="outline" className={`${getTypeColor(app.type)} text-xs mt-1`}>
{app.type}
<Badge variant="outline" className={`${getTypeColor(appData.type)} text-xs mt-1`}>
{appData.type}
</Badge>
</div>
</div>
<p className="text-sm text-gray-600 mb-3 line-clamp-2">{app.description}</p>
<p className="text-sm text-gray-600 mb-3 line-clamp-2">{appData.description}</p>
<div className="flex items-center justify-between mt-auto">
<div className="flex items-center space-x-3 text-xs text-gray-500">
<div className="flex items-center space-x-1">
@@ -379,7 +402,7 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
</div>
<div onClick={(e) => e.stopPropagation()}>
<LikeButton
appId={appId}
appId={appData.id}
size="sm"
likeCount={likes}
showCount={true}