Files
ai-showcase-platform/components/admin/competition-management.tsx
2025-09-16 10:38:23 +08:00

7679 lines
338 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 React, { 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 { Textarea } from "@/components/ui/textarea"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } 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 { Separator } from "@/components/ui/separator"
import { Progress } from "@/components/ui/progress"
import { Checkbox } from "@/components/ui/checkbox"
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination"
import {
Trophy,
Plus,
Award,
MoreHorizontal,
Edit,
Play,
CheckCircle,
AlertTriangle,
Crown,
UserPlus,
Loader2,
Star,
StarOff,
Users,
Settings,
ClipboardList,
Link,
UserCheck,
UserX,
Upload,
Filter,
User,
Mail,
Trash2,
Eye,
Search,
X,
} from "lucide-react"
import type { CompetitionRule, CompetitionAwardType } from "@/types/competition"
import { ScoringManagement } from "./scoring-management"
// Competition data - empty for production
const mockIndividualApps: any[] = []
// Teams data - empty for production
const initialTeams: any[] = []
export function CompetitionManagement() {
const {
competitions,
currentCompetition,
setCurrentCompetition,
addCompetition,
updateCompetition,
deleteCompetition,
judges,
addJudge,
updateJudge,
deleteJudge,
awards,
addAward,
getAppDetailedScores,
judgeScores,
getAppJudgeScores,
submitJudgeScore,
} = useCompetition()
// Teams state - managed locally for now
const [teams, setTeams] = useState(initialTeams)
// 資料庫整合狀態
const [dbCompetitions, setDbCompetitions] = useState<any[]>([])
const [isLoadingDb, setIsLoadingDb] = useState(false)
const [dbStats, setDbStats] = useState<any>(null)
// 評審資料庫整合狀態
const [dbJudges, setDbJudges] = useState<any[]>([])
const [isLoadingJudges, setIsLoadingJudges] = useState(false)
const [judgeStats, setJudgeStats] = useState<any>(null)
// 團隊資料庫整合狀態
const [dbTeams, setDbTeams] = useState<any[]>([])
const [isLoadingTeams, setIsLoadingTeams] = useState(false)
const [teamStats, setTeamStats] = useState<any>(null)
// 可用應用狀態
const [availableApps, setAvailableApps] = useState<any[]>([])
const [isLoadingApps, setIsLoadingApps] = useState(false)
// 可用用戶狀態
const [availableUsers, setAvailableUsers] = useState<any[]>([])
const [isLoadingUsers, setIsLoadingUsers] = useState(false)
const [showCreateCompetition, setShowCreateCompetition] = useState(false)
const [showAddJudge, setShowAddJudge] = useState(false)
const [showCreateAward, setShowCreateAward] = useState(false)
const [showScoringManagement, setShowScoringManagement] = useState(false)
const [showJudgeLinks, setShowJudgeLinks] = useState(false)
const [showManualScoring, setShowManualScoring] = useState(false)
const [showCreateTeam, setShowCreateTeam] = useState(false)
const [showTeamDetail, setShowTeamDetail] = useState(false)
const [showDeleteTeamConfirm, setShowDeleteTeamConfirm] = useState(false)
const [selectedCompetition, setSelectedCompetition] = useState<any>(null)
const [selectedTeam, setSelectedTeam] = useState<any>(null)
const [teamToDelete, setTeamToDelete] = useState<any>(null)
const [isLoading, setIsLoading] = useState(false)
const [success, setSuccess] = useState("")
const [error, setError] = useState("")
const [showCompetitionDetail, setShowCompetitionDetail] = useState(false)
const [showDeleteCompetitionConfirm, setShowDeleteCompetitionConfirm] = useState(false)
const [showChangeStatusDialog, setShowChangeStatusDialog] = useState(false)
const [selectedCompetitionForAction, setSelectedCompetitionForAction] = useState<any>(null)
const [newStatus, setNewStatus] = useState("")
// 奖项搜索和筛选状态
const [awardSearchQuery, setAwardSearchQuery] = useState("")
const [awardYearFilter, setAwardYearFilter] = useState("all")
// 資料庫 API 調用函數
const fetchCompetitions = async () => {
setIsLoadingDb(true)
try {
const response = await fetch('/api/admin/competitions')
const data = await response.json()
if (data.success) {
setDbCompetitions(data.data)
} else {
setError('獲取競賽列表失敗: ' + data.message)
}
} catch (error) {
console.error('獲取競賽列表失敗:', error)
setError('獲取競賽列表失敗')
} finally {
setIsLoadingDb(false)
}
}
const fetchCompetitionStats = async () => {
try {
const response = await fetch('/api/admin/competitions/stats')
const data = await response.json()
if (data.success) {
setDbStats(data.data)
}
} catch (error) {
console.error('獲取競賽統計失敗:', error)
}
}
const createCompetitionInDb = async (competitionData: any) => {
try {
const response = await fetch('/api/admin/competitions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(competitionData)
})
const data = await response.json()
if (data.success) {
setSuccess('競賽創建成功!')
await fetchCompetitions() // 重新獲取列表
await fetchCompetitionStats() // 重新獲取統計
return data.data
} else {
setError('創建競賽失敗: ' + data.message)
return null
}
} catch (error) {
console.error('創建競賽失敗:', error)
setError('創建競賽失敗')
return null
}
}
const updateCompetitionInDb = async (id: string, updates: any) => {
try {
const response = await fetch(`/api/admin/competitions/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updates)
})
const data = await response.json()
if (data.success) {
setSuccess('競賽更新成功!')
await fetchCompetitions() // 重新獲取列表
await fetchCompetitionStats() // 重新獲取統計
return data.data
} else {
setError('更新競賽失敗: ' + data.message)
return null
}
} catch (error) {
console.error('更新競賽失敗:', error)
setError('更新競賽失敗')
return null
}
}
const deleteCompetitionInDb = async (id: string) => {
try {
const response = await fetch(`/api/admin/competitions/${id}`, {
method: 'DELETE'
})
const data = await response.json()
if (data.success) {
setSuccess('競賽刪除成功!')
await fetchCompetitions() // 重新獲取列表
await fetchCompetitionStats() // 重新獲取統計
return true
} else {
setError('刪除競賽失敗: ' + data.message)
return false
}
} catch (error) {
console.error('刪除競賽失敗:', error)
setError('刪除競賽失敗')
return false
}
}
// 評審資料庫 API 調用函數
const fetchJudges = async () => {
setIsLoadingJudges(true)
try {
// 添加時間戳避免緩存
const response = await fetch(`/api/admin/judges?t=${Date.now()}`, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache'
}
})
const data = await response.json()
console.log('🔍 fetchJudges 響應:', data)
if (data.success) {
console.log('📊 獲取到的評審數據:', data.data)
setDbJudges(data.data)
} else {
setError('獲取評審列表失敗: ' + data.message)
}
} catch (error) {
console.error('獲取評審列表失敗:', error)
setError('獲取評審列表失敗')
} finally {
setIsLoadingJudges(false)
}
}
const fetchJudgeStats = async () => {
try {
const response = await fetch('/api/admin/judges/stats')
const data = await response.json()
console.log('評審統計 API 響應:', data)
if (data.success) {
setJudgeStats(data.data)
console.log('評審統計設置成功:', data.data)
} else {
console.error('評審統計 API 失敗:', data.message)
}
} catch (error) {
console.error('獲取評審統計失敗:', error)
}
}
// 團隊資料庫 API 調用函數
const fetchTeams = async () => {
setIsLoadingTeams(true)
try {
// 添加時間戳避免緩存
const response = await fetch(`/api/admin/teams?t=${Date.now()}`, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache'
}
})
const data = await response.json()
console.log('🔍 fetchTeams 響應:', data)
if (data.success) {
console.log('📊 獲取到的團隊數據:', data.data)
setDbTeams(data.data)
} else {
setError('獲取團隊列表失敗: ' + data.message)
}
} catch (error) {
console.error('獲取團隊列表失敗:', error)
setError('獲取團隊列表失敗')
} finally {
setIsLoadingTeams(false)
}
}
const fetchTeamStats = async () => {
try {
const response = await fetch('/api/admin/teams/stats')
const data = await response.json()
console.log('團隊統計 API 響應:', data)
if (data.success) {
setTeamStats(data.data)
console.log('團隊統計設置成功:', data.data)
}
} catch (error) {
console.error('獲取團隊統計失敗:', error)
}
}
const createTeamInDb = async (teamData: any) => {
try {
const response = await fetch('/api/admin/teams', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(teamData)
})
const data = await response.json()
if (data.success) {
setSuccess('團隊創建成功!')
await fetchTeams() // 重新獲取列表
await fetchTeamStats() // 重新獲取統計
return data.data
} else {
setError('創建團隊失敗: ' + data.message)
return null
}
} catch (error) {
console.error('創建團隊失敗:', error)
setError('創建團隊失敗')
return null
}
}
const updateTeamInDb = async (teamId: string, updates: any) => {
try {
const response = await fetch(`/api/admin/teams/${teamId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updates)
})
const data = await response.json()
if (data.success) {
setSuccess('團隊更新成功!')
await fetchTeams() // 重新獲取列表
await fetchTeamStats() // 重新獲取統計
return true // 返回 true 表示成功
} else {
setError('更新團隊失敗: ' + data.message)
return false
}
} catch (error) {
console.error('更新團隊失敗:', error)
setError('更新團隊失敗')
return false
}
}
const deleteTeamInDb = async (teamId: string, hard: boolean = false, skipRefresh: boolean = false) => {
try {
const response = await fetch(`/api/admin/teams/${teamId}?hard=${hard}`, {
method: 'DELETE'
})
const data = await response.json()
if (data.success) {
setSuccess(data.message || '團隊刪除成功!')
if (!skipRefresh) {
await fetchTeams() // 重新獲取列表
await fetchTeamStats() // 重新獲取統計
}
return true
} else {
setError('刪除團隊失敗: ' + data.message)
return false
}
} catch (error) {
console.error('刪除團隊失敗:', error)
setError('刪除團隊失敗')
return false
}
}
// 獲取可用應用列表
const fetchAvailableApps = async (teamId?: string) => {
console.log('🔍 開始獲取可用應用列表, teamId:', teamId)
setIsLoadingApps(true)
try {
const url = teamId
? `/api/admin/apps/available?teamId=${teamId}`
: '/api/admin/apps/available'
console.log('📡 請求 URL:', url)
const response = await fetch(url)
const data = await response.json()
console.log('📊 應用 API 響應:', data)
if (data.success) {
console.log('✅ 應用數據設置成功:', data.data.length, '個應用')
setAvailableApps(data.data)
return data.data // 返回數據
} else {
console.error('❌ 獲取應用列表失敗:', data.message)
setError('獲取可用應用列表失敗: ' + data.message)
return []
}
} catch (error) {
console.error('❌ 獲取可用應用列表失敗:', error)
setError('獲取可用應用列表失敗')
return []
} finally {
setIsLoadingApps(false)
}
}
// 獲取可用用戶列表
const fetchAvailableUsers = async () => {
console.log('🔍 開始獲取可用用戶列表')
setIsLoadingUsers(true)
try {
const response = await fetch('/api/admin/users/available')
const data = await response.json()
console.log('📊 用戶 API 響應:', data)
if (data.success) {
console.log('✅ 用戶數據設置成功:', data.data.length, '個用戶')
setAvailableUsers(data.data)
return data.data // 返回數據
} else {
console.error('❌ 獲取用戶列表失敗:', data.message)
setError('獲取可用用戶列表失敗: ' + data.message)
return []
}
} catch (error) {
console.error('❌ 獲取可用用戶列表失敗:', error)
setError('獲取可用用戶列表失敗')
return []
} finally {
setIsLoadingUsers(false)
}
}
// 獲取完整團隊信息
const fetchTeamDetails = async (teamId: string) => {
try {
const response = await fetch(`/api/admin/teams/${teamId}`)
const data = await response.json()
if (data.success) {
return data.data
} else {
setError('獲取團隊詳情失敗: ' + data.message)
return null
}
} catch (error) {
console.error('獲取團隊詳情失敗:', error)
setError('獲取團隊詳情失敗')
return null
}
}
// 編輯團隊
const handleEditTeam = async (team: any) => {
try {
console.log('🔍 開始編輯團隊:', team.id)
// 先加載用戶和應用數據
const users = await fetchAvailableUsers()
const apps = await fetchAvailableApps(team.id)
console.log('📊 實際載入的用戶數據:', users.length)
console.log('📊 實際載入的應用數據:', apps.length)
// 獲取完整的團隊信息
const teamDetails = await fetchTeamDetails(team.id)
console.log('📋 團隊詳情:', teamDetails)
if (teamDetails) {
// 填充表單數據
const teamData = {
name: teamDetails.name,
leader: teamDetails.leader_name || '',
leader_id: teamDetails.leader_id || '',
department: teamDetails.department,
contactEmail: teamDetails.contact_email || '',
leaderPhone: teamDetails.leader_phone || '',
description: teamDetails.description || '',
members: teamDetails.members || [],
apps: teamDetails.apps ? teamDetails.apps.map((app: any) => app.id || app) : [],
submittedAppCount: teamDetails.apps?.length || 0,
}
console.log('📝 填充的表單數據:', teamData)
setNewTeam(teamData)
setSelectedTeam(teamDetails)
setShowCreateTeam(true)
}
} catch (error) {
console.error('編輯團隊失敗:', error)
setError('編輯團隊失敗')
}
}
// 刪除團隊
const handleDeleteTeam = async (team: any) => {
setTeamToDelete(team)
setShowDeleteTeamConfirm(true)
}
// 確認刪除團隊
const confirmDeleteTeam = async () => {
if (!teamToDelete) return
setIsLoading(true)
try {
const success = await deleteTeamInDb(teamToDelete.id)
if (success) {
setSuccess("團隊刪除成功!")
setShowDeleteTeamConfirm(false)
setTeamToDelete(null)
// 重新獲取團隊列表
await fetchTeams()
await fetchTeamStats()
}
} catch (error) {
console.error('刪除團隊失敗:', error)
setError('刪除團隊失敗')
} finally {
setIsLoading(false)
}
}
const createJudgeInDb = async (judgeData: any) => {
try {
const response = await fetch('/api/admin/judges', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(judgeData)
})
const data = await response.json()
if (data.success) {
setSuccess('評審創建成功!')
await fetchJudges() // 重新獲取列表
await fetchJudgeStats() // 重新獲取統計
return data.data
} else {
setError('創建評審失敗: ' + data.message)
return null
}
} catch (error) {
console.error('創建評審失敗:', error)
setError('創建評審失敗')
return null
}
}
const updateJudgeInDb = async (id: string, updates: any) => {
try {
const response = await fetch(`/api/admin/judges/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updates)
})
const data = await response.json()
if (data.success) {
setSuccess('評審更新成功!')
await fetchJudges() // 重新獲取列表
await fetchJudgeStats() // 重新獲取統計
return true // 返回 true 表示成功
} else {
setError('更新評審失敗: ' + data.message)
return false
}
} catch (error) {
console.error('更新評審失敗:', error)
setError('更新評審失敗')
return false
}
}
const deleteJudgeInDb = async (id: string, hardDelete: boolean = false, skipRefresh: boolean = false) => {
try {
const url = hardDelete ? `/api/admin/judges/${id}?hard=true` : `/api/admin/judges/${id}`
const response = await fetch(url, {
method: 'DELETE'
})
const data = await response.json()
if (data.success) {
setSuccess(data.message || '評審刪除成功!')
if (!skipRefresh) {
await fetchJudges() // 重新獲取列表
await fetchJudgeStats() // 重新獲取統計
}
return true
} else {
setError('刪除評審失敗: ' + data.message)
return false
}
} catch (error) {
console.error('刪除評審失敗:', error)
setError('刪除評審失敗')
return false
}
}
const [awardMonthFilter, setAwardMonthFilter] = useState("all")
const [awardTypeFilter, setAwardTypeFilter] = useState("all")
const [awardCompetitionTypeFilter, setAwardCompetitionTypeFilter] = useState("all")
// 組件載入時獲取資料
useEffect(() => {
fetchCompetitions()
fetchCompetitionStats()
fetchJudges()
fetchJudgeStats()
fetchTeams()
fetchTeamStats()
}, [])
// 当筛选条件改变时重置分页
const resetAwardPagination = () => {
setAwardCurrentPage(1)
}
// Manual scoring states
const [manualScoring, setManualScoring] = useState({
judgeId: "",
participantId: "",
participantType: "individual" as "individual" | "team",
scores: {} as Record<string, number>,
comments: "",
})
// 混合賽的參賽者類型選擇
const [selectedParticipantType, setSelectedParticipantType] = useState<"individual" | "team">("individual")
// Form states - Updated for mixed competition support
const [newCompetition, setNewCompetition] = useState({
name: "",
type: "individual" as "individual" | "team" | "mixed",
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
startDate: "",
endDate: "",
description: "",
status: "upcoming" as const,
// For individual and team competitions
judges: [] as string[],
participatingApps: [] as string[],
participatingTeams: [] as string[],
evaluationFocus: "",
rules: [] as CompetitionRule[],
awardTypes: [] as CompetitionAwardType[],
// For mixed competitions - separate configurations
individualConfig: {
judges: [] as string[],
rules: [] as CompetitionRule[],
awardTypes: [] as CompetitionAwardType[],
evaluationFocus: "",
},
teamConfig: {
judges: [] as string[],
rules: [] as CompetitionRule[],
awardTypes: [] as CompetitionAwardType[],
evaluationFocus: "",
},
})
const [newJudge, setNewJudge] = useState({
name: "",
title: "",
department: "",
expertise: "",
})
const [newAward, setNewAward] = useState({
competitionId: "",
participantId: "",
participantType: "individual" as "individual" | "team",
awardType: "custom" as const,
awardName: "",
customAwardTypeId: "",
description: "",
score: 0,
category: "innovation" as const,
rank: 0,
applicationLinks: {
production: "",
demo: "",
github: "",
},
documents: [] as { id: string; name: string; type: string; size: string; uploadDate: string; url: string }[],
judgeComments: "",
photos: [] as { id: string; name: string; url: string; caption: string; uploadDate: string; size: string }[],
})
// Team form states
const [newTeam, setNewTeam] = useState({
name: "",
leader: "",
leader_id: "", // 添加隊長 ID
department: "HQBU",
contactEmail: "",
leaderPhone: "",
description: "",
members: [] as Array<{ id: string; name: string; department: string; role: string }>,
apps: [] as string[], // 改為存儲應用 ID
submittedAppCount: 0,
})
const [newMember, setNewMember] = useState({
name: "",
user_id: "", // 添加用戶 ID
department: "HQBU",
role: "成員",
})
const [newApp, setNewApp] = useState({
name: "",
link: "",
})
const [createError, setCreateError] = useState("")
// Participant selection states - separate for mixed competitions
const [participantSearchTerm, setParticipantSearchTerm] = useState("")
const [departmentFilter, setDepartmentFilter] = useState<string>("all")
const [individualParticipantSearchTerm, setIndividualParticipantSearchTerm] = useState("")
const [teamParticipantSearchTerm, setTeamParticipantSearchTerm] = useState("")
const [individualDepartmentFilter, setIndividualDepartmentFilter] = useState<string>("all")
const [teamDepartmentFilter, setTeamDepartmentFilter] = useState<string>("all")
// Get participants based on competition type
const [selectedJudge, setSelectedJudge] = useState<any>(null)
const [showJudgeDetail, setShowJudgeDetail] = useState(false)
const [showDeleteJudgeConfirm, setShowDeleteJudgeConfirm] = useState(false)
const [showDisableJudgeConfirm, setShowDisableJudgeConfirm] = useState(false)
// 獎項相關狀態
const [showAwardDetail, setShowAwardDetail] = useState(false)
const [selectedAward, setSelectedAward] = useState<any>(null)
const [showDeleteAwardConfirm, setShowDeleteAwardConfirm] = useState(false)
const [awardToDelete, setAwardToDelete] = useState<any>(null)
// 評審分頁和篩選狀態
const [judgeCurrentPage, setJudgeCurrentPage] = useState(1)
const [judgeSearchTerm, setJudgeSearchTerm] = useState("")
const [judgeDepartmentFilter, setJudgeDepartmentFilter] = useState<string>("all")
const [judgeExpertiseFilter, setJudgeExpertiseFilter] = useState<string>("all")
const [judgeStatusFilter, setJudgeStatusFilter] = useState<string>("all")
const judgesPerPage = 6
// 團隊分頁和篩選狀態
const [teamCurrentPage, setTeamCurrentPage] = useState(1)
const [teamSearchTerm, setTeamSearchTerm] = useState("")
const teamsPerPage = 6
// 獎項分頁狀態
const [awardCurrentPage, setAwardCurrentPage] = useState(1)
const awardsPerPage = 6
// Get participants based on competition type
const getParticipants = (competitionType: string) => {
switch (competitionType) {
case "individual":
return mockIndividualApps
case "team":
return dbTeams.length > 0 ? dbTeams : teams
default:
return []
}
}
// Filter participants - updated for mixed competitions
const getFilteredParticipants = (competitionType: string) => {
const participants = getParticipants(competitionType)
let searchTerm = participantSearchTerm
let departmentFilterValue = departmentFilter
// Use separate search terms for mixed competitions
if (newCompetition.type === "mixed") {
searchTerm = competitionType === "individual" ? individualParticipantSearchTerm : teamParticipantSearchTerm
departmentFilterValue = competitionType === "individual" ? individualDepartmentFilter : teamDepartmentFilter
}
return participants.filter((participant) => {
const searchField = competitionType === "team" ? participant.name : participant.name
const creatorField = competitionType === "team" ? participant.leader : participant.creator
const matchesSearch =
searchField.toLowerCase().includes(searchTerm.toLowerCase()) ||
creatorField.toLowerCase().includes(searchTerm.toLowerCase())
const matchesDepartment = departmentFilterValue === "all" || participant.department === departmentFilterValue
return matchesSearch && matchesDepartment
})
}
const resetForm = () => {
setNewCompetition({
name: "",
type: "individual",
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
startDate: "",
endDate: "",
description: "",
status: "upcoming",
judges: [],
participatingApps: [],
participatingTeams: [],
evaluationFocus: "",
rules: [],
awardTypes: [],
individualConfig: {
judges: [],
rules: [],
awardTypes: [],
evaluationFocus: "",
},
teamConfig: {
judges: [],
rules: [],
awardTypes: [],
evaluationFocus: "",
},
})
// Reset search terms
setParticipantSearchTerm("")
setDepartmentFilter("all")
setIndividualParticipantSearchTerm("")
setTeamParticipantSearchTerm("")
setIndividualDepartmentFilter("all")
setTeamDepartmentFilter("all")
}
const resetTeamForm = () => {
setNewTeam({
name: "",
leader: "",
department: "HQBU",
contactEmail: "",
leaderPhone: "",
description: "",
members: [],
apps: [],
submittedAppCount: 0,
})
setNewMember({
name: "",
department: "HQBU",
role: "成員",
})
setNewApp({
name: "",
link: "",
})
}
const handleCreateCompetition = async () => {
setCreateError("")
if (!newCompetition.name || !newCompetition.startDate || !newCompetition.endDate) {
setCreateError("請填寫所有必填欄位")
return
}
// Validation for mixed competitions
if (newCompetition.type === "mixed") {
if (newCompetition.individualConfig.judges.length === 0 && newCompetition.teamConfig.judges.length === 0) {
setCreateError("混合賽至少需要為個人賽或團體賽選擇評審")
return
}
// Check if at least one competition type has participants
const hasParticipants =
newCompetition.participatingApps.length > 0 || newCompetition.participatingTeams.length > 0
if (!hasParticipants) {
setCreateError("請至少選擇一個個人賽應用或團隊賽團隊")
return
}
// Validate individual rules if there are individual participants and judges
if (newCompetition.participatingApps.length > 0 && newCompetition.individualConfig.judges.length > 0) {
if (newCompetition.individualConfig.rules.length > 0) {
const individualTotalWeight = calculateTotalWeight(newCompetition.individualConfig.rules)
if (Math.abs(individualTotalWeight - 100) > 0.01) {
setCreateError("個人賽評比標準權重總和必須為 100%")
return
}
}
}
// Validate team rules if there are team participants and judges
if (newCompetition.participatingTeams.length > 0 && newCompetition.teamConfig.judges.length > 0) {
if (newCompetition.teamConfig.rules.length > 0) {
const teamTotalWeight = calculateTotalWeight(newCompetition.teamConfig.rules)
if (Math.abs(teamTotalWeight - 100) > 0.01) {
setCreateError("團體賽評比標準權重總和必須為 100%")
return
}
}
}
} else {
// Validation for single type competitions
if (newCompetition.judges.length === 0) {
setCreateError("請至少選擇一位評審")
return
}
const hasParticipants =
(newCompetition.type === "individual" && newCompetition.participatingApps.length > 0) ||
(newCompetition.type === "team" && newCompetition.participatingTeams.length > 0)
if (!hasParticipants) {
setCreateError("請至少選擇一個參賽項目")
return
}
if (newCompetition.rules.length > 0) {
const totalWeight = calculateTotalWeight(newCompetition.rules)
if (Math.abs(totalWeight - 100) > 0.01) {
setCreateError("評比標準權重總和必須為 100%")
return
}
const hasEmptyRule = newCompetition.rules.some((rule) => !rule.name.trim() || !rule.description.trim())
if (hasEmptyRule) {
setCreateError("請填寫所有評比標準的名稱和描述")
return
}
}
if (newCompetition.awardTypes.length > 0) {
const hasEmptyAwardType = newCompetition.awardTypes.some(
(awardType) => !awardType.name.trim() || !awardType.description.trim(),
)
if (hasEmptyAwardType) {
setCreateError("請填寫所有獎項類型的名稱和描述")
return
}
}
}
setIsLoading(true)
try {
if (selectedCompetitionForAction) {
// 編輯模式 - 更新現有競賽
const updates = {
name: newCompetition.name,
year: newCompetition.year,
month: newCompetition.month,
startDate: newCompetition.startDate,
endDate: newCompetition.endDate,
status: newCompetition.status,
description: newCompetition.description,
type: newCompetition.type,
evaluationFocus: newCompetition.evaluationFocus,
maxTeamSize: newCompetition.maxTeamSize || null,
// 關聯數據
judges: newCompetition.judges || [],
teams: newCompetition.participatingTeams || [],
awardTypes: newCompetition.awardTypes || [],
rules: newCompetition.rules || []
}
const updatedCompetition = await updateCompetitionInDb(selectedCompetitionForAction.id, updates)
if (updatedCompetition) {
// 同時更新 context
updateCompetition(selectedCompetitionForAction.id, newCompetition)
}
} else {
// 創建模式 - 新增競賽到資料庫
const competitionData = {
name: newCompetition.name,
year: newCompetition.year,
month: newCompetition.month,
startDate: newCompetition.startDate,
endDate: newCompetition.endDate,
status: newCompetition.status,
description: newCompetition.description,
type: newCompetition.type,
evaluationFocus: newCompetition.evaluationFocus,
maxTeamSize: newCompetition.maxTeamSize || null,
isActive: true,
// 關聯數據
judges: newCompetition.judges || [],
teams: newCompetition.participatingTeams || [],
awardTypes: newCompetition.awardTypes || [],
rules: newCompetition.rules || []
}
const createdCompetition = await createCompetitionInDb(competitionData)
if (createdCompetition) {
// 同時添加到 context保持向後兼容
const competitionWithId = {
...newCompetition,
id: createdCompetition.id,
createdAt: createdCompetition.created_at,
}
addCompetition(competitionWithId)
}
}
} catch (error) {
console.error("處理競賽失敗:", error)
setCreateError("處理競賽時發生錯誤")
}
setShowCreateCompetition(false)
setSelectedCompetitionForAction(null)
setCreateError("")
resetForm()
setIsLoading(false)
setTimeout(() => setSuccess(""), 3000)
}
const handleCreateTeam = async () => {
setCreateError("")
if (!newTeam.name || !newTeam.leader || !newTeam.contactEmail) {
setCreateError("請填寫團隊名稱、隊長和聯絡信箱")
return
}
if (newTeam.members.length === 0) {
setCreateError("請至少添加一名團隊成員")
return
}
// Check if leader is in members list
const leaderInMembers = newTeam.members.some((member) => member.name === newTeam.leader)
if (!leaderInMembers) {
setCreateError("隊長必須在團隊成員列表中")
return
}
setIsLoading(true)
try {
if (selectedTeam) {
// 編輯模式 - 更新現有團隊
// 使用選擇的隊長 ID如果沒有選擇則保持原有的隊長 ID
const leaderId = newTeam.leader_id || (selectedTeam as any).leader_id || '0b844fb6-1a63-4e0c-a15a-416e9b0ec8c7'
const teamData = {
name: newTeam.name,
leader_id: leaderId,
department: newTeam.department,
contact_email: newTeam.contactEmail,
description: newTeam.description,
members: newTeam.members.map(member => ({
user_id: member.id, // 現在 member.id 就是 user_id
role: member.role || 'member'
})),
apps: newTeam.apps // 添加應用 ID 列表
}
const success = await updateTeamInDb(selectedTeam.id, teamData)
if (success) {
setSuccess("團隊更新成功!")
}
} else {
// 創建模式 - 新增團隊
// 使用選擇的隊長 ID如果沒有選擇則使用預設的用戶 ID
const leaderId = newTeam.leader_id || '0b844fb6-1a63-4e0c-a15a-416e9b0ec8c7'
const teamData = {
name: newTeam.name,
leader_id: leaderId,
department: newTeam.department,
contact_email: newTeam.contactEmail,
description: newTeam.description,
members: newTeam.members.map(member => ({
user_id: member.id, // 現在 member.id 就是 user_id
role: member.role || 'member'
})),
apps: newTeam.apps // 添加應用 ID 列表
}
const createdTeam = await createTeamInDb(teamData)
if (createdTeam) {
setSuccess("團隊創建成功!")
}
}
} catch (error) {
console.error("處理團隊失敗:", error)
setCreateError("處理團隊時發生錯誤")
}
setShowCreateTeam(false)
setSelectedTeam(null)
resetTeamForm()
setIsLoading(false)
setTimeout(() => setSuccess(""), 3000)
}
const handleConfirmDeleteTeam = async () => {
if (!teamToDelete) return
setIsLoading(true)
try {
const success = await deleteTeamInDb(teamToDelete.id, true) // 硬刪除
if (success) {
setShowDeleteTeamConfirm(false)
setTeamToDelete(null)
setSuccess("團隊刪除成功!")
} else {
setError("刪除團隊失敗")
}
} catch (error) {
console.error('刪除團隊失敗:', error)
setError('刪除團隊失敗')
} finally {
setIsLoading(false)
}
}
const handleAddMember = () => {
if (!newMember.user_id || !newMember.name.trim()) {
setCreateError("請選擇成員")
return
}
// 檢查是否已經添加過這個成員
if (newTeam.members.some(member => member.id === newMember.user_id)) {
setCreateError("該成員已經在團隊中")
return
}
const member = {
id: newMember.user_id, // 使用用戶 ID
name: newMember.name,
department: newMember.department,
role: newMember.role,
}
setNewTeam({
...newTeam,
members: [...newTeam.members, member],
})
setNewMember({
name: "",
user_id: "",
department: "HQBU",
role: "成員",
})
setCreateError("")
}
const handleRemoveMember = (memberId: string) => {
setNewTeam({
...newTeam,
members: newTeam.members.filter((m) => m.id !== memberId),
})
}
const handleAddApp = (appId: string) => {
if (newTeam.apps.includes(appId)) {
setCreateError("此應用已經加入團隊")
return
}
setNewTeam({
...newTeam,
apps: [...newTeam.apps, appId],
})
setCreateError("")
}
const handleRemoveApp = (appId: string) => {
setNewTeam({
...newTeam,
apps: newTeam.apps.filter(id => id !== appId),
})
}
const handleAddJudge = async () => {
setError("")
if (!newJudge.name || !newJudge.title || !newJudge.department) {
setError("請填寫所有必填欄位")
return
}
setIsLoading(true)
try {
const expertiseArray = newJudge.expertise
.split(",")
.map((s) => s.trim())
.filter(Boolean)
if (selectedJudge) {
// 編輯模式 - 更新現有評審
const updates = {
name: newJudge.name,
title: newJudge.title,
department: newJudge.department,
expertise: expertiseArray,
avatar: newJudge.avatar || null,
isActive: true
}
const updatedJudge = await updateJudgeInDb(selectedJudge.id, updates)
if (updatedJudge) {
// 同時更新 context
updateJudge(selectedJudge.id, {
...newJudge,
expertise: expertiseArray,
})
}
} else {
// 新增模式 - 新增評審到資料庫
const judgeData = {
name: newJudge.name,
title: newJudge.title,
department: newJudge.department,
expertise: expertiseArray,
avatar: newJudge.avatar || null,
isActive: true
}
const createdJudge = await createJudgeInDb(judgeData)
if (createdJudge) {
// 同時添加到 context保持向後兼容
addJudge({
...newJudge,
expertise: expertiseArray,
})
// 重新獲取評審列表和統計
await fetchJudges()
await fetchJudgeStats()
}
}
} catch (error) {
console.error("處理評審失敗:", error)
setError("處理評審時發生錯誤")
}
setShowAddJudge(false)
setSelectedJudge(null)
setNewJudge({
name: "",
title: "",
department: "",
expertise: "",
})
setIsLoading(false)
setTimeout(() => setSuccess(""), 3000)
}
const handleEditJudge = (judge: any) => {
setSelectedJudge(judge)
setNewJudge({
name: judge.name,
title: judge.title,
department: judge.department,
expertise: judge.expertise.join(", "),
})
setShowAddJudge(true) // 使用新增評審對話框
}
const handleDisableJudge = (judge: any) => {
setSelectedJudge(judge)
setShowDisableJudgeConfirm(true)
}
const handleEnableJudge = async (judge: any) => {
setIsLoading(true)
try {
const success = await updateJudgeInDb(judge.id, { is_active: true })
if (success) {
setSuccess('評審已啟用!')
// 不需要再次調用 fetchJudges 和 fetchJudgeStats因為 updateJudgeInDb 已經調用了
} else {
setError('啟用評審失敗')
}
} catch (error) {
console.error('啟用評審失敗:', error)
setError('啟用評審失敗')
} finally {
setIsLoading(false)
}
}
const handleDeleteJudge = (judge: any) => {
setSelectedJudge(judge)
setShowDeleteJudgeConfirm(true)
}
const confirmDisableJudge = async () => {
if (!selectedJudge) return
setIsLoading(true)
try {
const success = await updateJudgeInDb(selectedJudge.id, { is_active: false })
if (success) {
setShowDisableJudgeConfirm(false)
setSelectedJudge(null)
setSuccess('評審已停用!')
// 不需要再次調用 fetchJudges 和 fetchJudgeStats因為 updateJudgeInDb 已經調用了
} else {
setError('停用評審失敗')
}
} catch (error) {
console.error('停用評審失敗:', error)
setError('停用評審失敗')
} finally {
setIsLoading(false)
}
}
const confirmDeleteJudge = async () => {
if (!selectedJudge) return
const judgeId = selectedJudge.id // 保存 ID避免後續變為 null
const judgeName = selectedJudge.name // 保存名稱,避免後續變為 null
setIsLoading(true)
try {
const success = await deleteJudgeInDb(judgeId, true, true) // 直接硬刪除,跳過內部刷新
if (success) {
setShowDeleteJudgeConfirm(false)
setSelectedJudge(null)
// 立即從 context 中移除(如果存在)
console.log('🗑️ 調用 deleteJudge 移除評審:', judgeId)
console.log('🗑️ 評審名稱:', judgeName)
console.log('🗑️ 當前 context 中的評審:', judges.map(j => ({ id: j.id, name: j.name })))
// 根據名稱匹配 context 中的評審,因為 ID 可能不匹配
const contextJudge = judges.find(j => j.name === judgeName)
if (contextJudge) {
console.log('🗑️ 找到匹配的 context 評審:', contextJudge)
deleteJudge(contextJudge.id)
} else {
console.log('🗑️ 沒有找到匹配的 context 評審,嘗試使用原始 ID')
deleteJudge(judgeId)
}
// 強制從 dbJudges 中移除,確保立即更新
setDbJudges(prev => {
console.log('🗑️ 從 dbJudges 中移除評審:', judgeId)
console.log('🗑️ 移除前的評審列表:', prev.map(j => ({ id: j.id, name: j.name, is_active: j.is_active })))
const filtered = prev.filter(judge => judge.id !== judgeId)
console.log('🗑️ 移除後的評審列表:', filtered.map(j => ({ id: j.id, name: j.name, is_active: j.is_active })))
return filtered
})
// 等待一個微任務,確保 context 狀態更新完成
await new Promise(resolve => setTimeout(resolve, 0))
// 確保列表和統計數據立即更新
console.log('🔄 調用 fetchJudges 重新獲取數據')
await fetchJudges()
await fetchJudgeStats()
// 再次檢查 context 狀態
console.log('🔍 刪除後檢查 context 狀態')
}
} catch (error) {
console.error('刪除評審失敗:', error)
setError('刪除評審失敗')
} finally {
setIsLoading(false)
}
}
const handleCreateAward = async () => {
setError("")
if (!newAward.competitionId || !newAward.participantId || !newAward.awardName) {
setError("請填寫所有必填欄位")
return
}
setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 1000))
const competition = competitions.find((c) => c.id === newAward.competitionId)
let participant: any = null
let participantName = ""
let creatorName = ""
// Get participant based on type
if (newAward.participantType === "individual") {
// 示例個人應用數據
const mockIndividualApps = [
{ id: "app1", name: "智能客服系統", creator: "張小明", department: "ITBU" },
{ id: "app2", name: "數據分析平台", creator: "李美華", department: "研發部" },
]
participant = mockIndividualApps.find((a) => a.id === newAward.participantId)
participantName = participant?.name || ""
creatorName = participant?.creator || ""
} else if (newAward.participantType === "team") {
participant = (dbTeams.length > 0 ? dbTeams : teams).find((t) => t.id === newAward.participantId)
participantName = participant?.name || ""
creatorName = participant?.leader || ""
}
if (competition && participant) {
// 根據獎項類型設定圖標
let icon = "🏆"
switch (newAward.awardType) {
case "gold": icon = "🥇"; break;
case "silver": icon = "🥈"; break;
case "bronze": icon = "🥉"; break;
case "popular": icon = "👥"; break;
case "innovation": icon = "💡"; break;
case "technical": icon = "⚙️"; break;
default: icon = "🏆"; break;
}
const award = {
id: `award_${Date.now()}`,
competitionId: newAward.competitionId,
appId: newAward.participantType === "individual" ? newAward.participantId : undefined,
teamId: newAward.participantType === "team" ? newAward.participantId : undefined,
appName: newAward.participantType === "individual" ? participantName : undefined,
creator: creatorName,
awardType: newAward.awardType,
awardName: newAward.awardName,
score: newAward.score,
year: competition.year,
month: competition.month,
icon,
rank: newAward.rank,
category: newAward.category,
competitionType: newAward.participantType,
description: newAward.description,
judgeComments: newAward.judgeComments,
applicationLinks: newAward.applicationLinks,
documents: newAward.documents,
photos: newAward.photos,
}
addAward(award)
}
setShowCreateAward(false)
setNewAward({
competitionId: "",
participantId: "",
participantType: "individual",
awardType: "custom",
awardName: "",
customAwardTypeId: "",
description: "",
score: 0,
category: "innovation",
rank: 0,
applicationLinks: {
production: "",
demo: "",
github: "",
},
documents: [],
judgeComments: "",
photos: [],
})
setSuccess(selectedAward ? "獎項更新成功!" : "獎項創建成功!")
setIsLoading(false)
setTimeout(() => setSuccess(""), 3000)
}
const handleViewAward = (award: any) => {
setSelectedAward(award)
setShowAwardDetail(true)
}
const handleEditAward = (award: any) => {
setSelectedAward(award)
setNewAward({
competitionId: award.competitionId,
participantId: award.appId || award.teamId || "",
participantType: award.competitionType,
awardType: award.awardType,
awardName: award.awardName,
customAwardTypeId: award.customAwardTypeId || "",
description: (award as any).description || "",
score: award.score,
category: award.category,
rank: award.rank,
applicationLinks: (award as any).applicationLinks || {
production: "",
demo: "",
github: "",
},
documents: (award as any).documents || [],
judgeComments: (award as any).judgeComments || "",
photos: (award as any).photos || [],
})
setShowCreateAward(true)
}
const handleDeleteAward = (award: any) => {
setAwardToDelete(award)
setShowDeleteAwardConfirm(true)
}
const confirmDeleteAward = async () => {
if (!awardToDelete) return
setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 500))
// 這裡應該調用 context 中的刪除函數
// deleteAward(awardToDelete.id)
setShowDeleteAwardConfirm(false)
setAwardToDelete(null)
setSuccess("獎項刪除成功!")
setIsLoading(false)
setTimeout(() => setSuccess(""), 3000)
}
const handleManualScoring = (competition: any) => {
setSelectedCompetition(competition)
// 設定初始參賽者類型
if (competition.type === "mixed") {
setSelectedParticipantType("individual") // 混合賽預設從個人賽開始
} else {
setSelectedParticipantType(competition.type)
}
// 初始化評分項目
const initialScores = getInitialScores(competition, competition.type === "mixed" ? "individual" : competition.type)
setManualScoring({
judgeId: "",
participantId: "",
participantType: competition.type || "individual",
scores: initialScores,
comments: "",
})
setShowManualScoring(true)
}
// 獲取初始評分項目的輔助函數
const getInitialScores = (competition: any, participantType: "individual" | "team") => {
const initialScores: Record<string, number> = {}
if (competition.type === "mixed") {
// 混合賽:根據參賽者類型選擇對應的評分規則
const config = participantType === "individual" ? competition.individualConfig : competition.teamConfig
if (config && config.rules && config.rules.length > 0) {
config.rules.forEach((rule: any) => {
initialScores[rule.name] = 0
})
} else {
// 預設評分項目
getDefaultScoringItems(participantType).forEach(item => {
initialScores[item.name] = 0
})
}
} else {
// 單一類型競賽
if (competition.rules && competition.rules.length > 0) {
competition.rules.forEach((rule: any) => {
initialScores[rule.name] = 0
})
} else {
// 預設評分項目
getDefaultScoringItems(participantType).forEach(item => {
initialScores[item.name] = 0
})
}
}
return initialScores
}
// 獲取預設評分項目
const getDefaultScoringItems = (participantType: "individual" | "team") => {
if (participantType === "team") {
return [
{ name: '團隊合作', description: '團隊協作和溝通能力' },
{ name: '創新性', description: '創新程度和獨特性' },
{ name: '技術性', description: '技術實現的複雜度和品質' },
{ name: '實用性', description: '實際應用價值和用戶體驗' },
{ name: '展示效果', description: '團隊展示的清晰度和吸引力' }
]
} else {
return [
{ name: '創新性', description: '創新程度和獨特性' },
{ name: '技術性', description: '技術實現的複雜度和品質' },
{ name: '實用性', description: '實際應用價值和用戶體驗' },
{ name: '展示效果', description: '展示的清晰度和吸引力' },
{ name: '影響力', description: '對行業或社會的潛在影響' }
]
}
}
// 處理參賽者類型變更(僅針對混合賽)
const handleParticipantTypeChange = (newType: "individual" | "team") => {
setSelectedParticipantType(newType)
// 重新初始化評分項目
const newScores = getInitialScores(selectedCompetition, newType)
setManualScoring({
...manualScoring,
participantId: "", // 清空選擇的參賽者
scores: newScores,
})
}
const handleSubmitManualScore = async () => {
setError("")
if (!manualScoring.judgeId || !manualScoring.participantId) {
setError("請選擇評審和參賽項目")
return
}
const hasAllScores = Object.values(manualScoring.scores).every((score) => score > 0)
if (!hasAllScores) {
setError("請為所有評分項目打分")
return
}
if (!manualScoring.comments.trim()) {
setError("請填寫評審意見")
return
}
setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 1000))
submitJudgeScore({
judgeId: manualScoring.judgeId,
appId: manualScoring.participantId, // Using appId field for all participant types
scores: manualScoring.scores,
comments: manualScoring.comments.trim(),
})
setManualScoring({
judgeId: "",
participantId: "",
participantType: "individual",
scores: {
innovation: 0,
technical: 0,
usability: 0,
presentation: 0,
impact: 0,
},
comments: "",
})
setSuccess("評分提交成功!")
setIsLoading(false)
setTimeout(() => setSuccess(""), 3000)
}
const handleViewCompetition = (competition: any) => {
setSelectedCompetitionForAction(competition)
setShowCompetitionDetail(true)
}
const handleEditCompetition = (competition: any) => {
setSelectedCompetitionForAction(competition)
// 調試信息
console.log('🔍 handleEditCompetition 調試信息:');
console.log('競賽數據:', competition);
console.log('startDate:', competition.startDate);
console.log('endDate:', competition.endDate);
console.log('start_date:', competition.start_date);
console.log('end_date:', competition.end_date);
// 將 ISO 日期字符串轉換為 YYYY-MM-DD 格式
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
try {
const date = new Date(dateString);
return date.toISOString().split('T')[0];
} catch (error) {
console.error('日期格式轉換錯誤:', error);
return '';
}
};
const startDate = competition.startDate || competition.start_date;
const endDate = competition.endDate || competition.end_date;
console.log('轉換後的 startDate:', formatDateForInput(startDate));
console.log('轉換後的 endDate:', formatDateForInput(endDate));
setNewCompetition({
name: competition.name,
type: competition.type,
year: competition.year,
month: competition.month,
startDate: formatDateForInput(startDate),
endDate: formatDateForInput(endDate),
description: competition.description,
status: competition.status,
// 將評審對象數組轉換為 ID 數組
judges: competition.judges ? competition.judges.map((judge: any) => judge.id) : [],
// 將團隊對象數組轉換為 ID 數組
participatingTeams: competition.teams ? competition.teams.map((team: any) => team.id) : [],
// 將應用對象數組轉換為 ID 數組
participatingApps: competition.apps ? competition.apps.map((app: any) => app.id) : [],
evaluationFocus: competition.evaluationFocus || "",
rules: competition.rules || [],
awardTypes: competition.awardTypes || [],
individualConfig: competition.individualConfig || {
judges: [],
rules: [],
awardTypes: [],
evaluationFocus: "",
},
teamConfig: competition.teamConfig || {
judges: [],
rules: [],
awardTypes: [],
evaluationFocus: "",
},
})
setShowCreateCompetition(true) // 使用創建競賽對話框
}
const handleDeleteCompetition = (competition: any) => {
setSelectedCompetitionForAction(competition)
setShowDeleteCompetitionConfirm(true)
}
const handleChangeStatus = (competition: any) => {
setSelectedCompetitionForAction(competition)
setNewStatus(competition.status)
setShowChangeStatusDialog(true)
}
const confirmDeleteCompetition = async () => {
if (!selectedCompetitionForAction) return
setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 500))
deleteCompetition(selectedCompetitionForAction.id)
setShowDeleteCompetitionConfirm(false)
setSelectedCompetitionForAction(null)
setSuccess("競賽刪除成功!")
setIsLoading(false)
setTimeout(() => setSuccess(""), 3000)
}
const handleUpdateStatus = async () => {
if (!selectedCompetitionForAction) return
setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 500))
updateCompetition(selectedCompetitionForAction.id, {
...selectedCompetitionForAction,
status: newStatus,
})
setShowChangeStatusDialog(false)
setSelectedCompetitionForAction(null)
setSuccess("競賽狀態更新成功!")
setIsLoading(false)
setTimeout(() => setSuccess(""), 3000)
}
const getCompetitionTypeIcon = (type: string) => {
switch (type) {
case "individual":
return <User className="w-4 h-4" />
case "team":
return <Users className="w-4 h-4" />
case "mixed":
return <Trophy className="w-4 h-4" />
default:
return <Trophy className="w-4 h-4" />
}
}
const getScoreLabelText = (key: string) => {
switch (key) {
case "innovation":
return "創新性"
case "technical":
return "技術性"
case "usability":
return "實用性"
case "presentation":
return "展示性"
case "impact":
return "影響力"
default:
return key
}
}
const getCompetitionTypeText = (type: string) => {
switch (type) {
case "individual":
return "個人賽"
case "team":
return "團體賽"
case "mixed":
return "混合賽"
default:
return "未知類型"
}
}
// 格式化時間顯示
const formatDateRange = (startDate: string, endDate: string) => {
try {
const start = new Date(startDate)
const end = new Date(endDate)
const formatDate = (date: Date) => {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}
return `${formatDate(start)} ~ ${formatDate(end)}`
} catch (error) {
return `${startDate} ~ ${endDate}`
}
}
// 計算權重百分比
const calculateWeightPercentage = (weight: any) => {
const weightNum = parseFloat(weight) || 0;
// 如果權重已經是百分比格式大於1直接使用
if (weightNum > 1) {
return weightNum;
}
// 如果權重是小數格式0-1轉換為百分比
return weightNum * 100;
}
// 計算總權重
const calculateTotalWeight = (rules: any[]) => {
return rules.reduce((sum, rule) => {
return sum + calculateWeightPercentage(rule.weight);
}, 0);
}
const getParticipantCount = (competition: any) => {
// 如果是從資料庫載入的競賽,使用 teams 屬性
if (competition.teams && Array.isArray(competition.teams)) {
return competition.teams.length
}
// 如果是本地競賽數據,使用原來的邏輯
switch (competition.type) {
case "individual":
return competition.participatingApps?.length || 0
case "team":
return competition.participatingTeams?.length || 0
case "mixed":
return (competition.participatingApps?.length || 0) + (competition.participatingTeams?.length || 0)
default:
return 0
}
}
const getScoringProgress = (competitionId: string) => {
const competition = competitions.find((c) => c.id === competitionId)
if (!competition) return { completed: 0, total: 0, percentage: 0 }
const participantCount = getParticipantCount(competition)
const totalExpected = competition.judges.length * participantCount
const completed = judgeScores.filter((score) => {
const individualParticipants = competition.participatingApps || []
const teamParticipants = competition.participatingTeams || []
const allParticipants = [...individualParticipants, ...teamParticipants]
return allParticipants.includes(score.appId) && competition.judges.includes(score.judgeId)
}).length
return {
completed,
total: totalExpected,
percentage: totalExpected > 0 ? Math.round((completed / totalExpected) * 100) : 0,
}
}
const getStatusColor = (status: string) => {
switch (status) {
case "completed":
return "bg-green-100 text-green-800 border-green-200"
case "active":
return "bg-blue-100 text-blue-800 border-blue-200"
case "judging":
return "bg-orange-100 text-orange-800 border-orange-200"
case "upcoming":
return "bg-gray-100 text-gray-800 border-gray-200"
default:
return "bg-gray-100 text-gray-800 border-gray-200"
}
}
const getStatusText = (status: string) => {
switch (status) {
case "completed":
return "已完成"
case "active":
return "進行中"
case "judging":
return "評審中"
case "upcoming":
return "即將開始"
default:
return status
}
}
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text)
setSuccess("連結已複製到剪貼簿!")
setTimeout(() => setSuccess(""), 3000)
}
// 获取筛选后的奖项
const getFilteredAwards = () => {
let filteredAwards = [...awards]
// 搜索功能 - 按应用名称、创作者或奖项名称搜索
if (awardSearchQuery.trim()) {
const query = awardSearchQuery.toLowerCase().trim()
filteredAwards = filteredAwards.filter((award) => {
return (
award.appName?.toLowerCase().includes(query) ||
award.creator?.toLowerCase().includes(query) ||
award.awardName?.toLowerCase().includes(query)
)
})
}
// 年份筛选
if (awardYearFilter !== "all") {
filteredAwards = filteredAwards.filter((award) => award.year === Number.parseInt(awardYearFilter))
}
// 月份筛选
if (awardMonthFilter !== "all") {
filteredAwards = filteredAwards.filter((award) => award.month === Number.parseInt(awardMonthFilter))
}
// 奖项类型筛选
if (awardTypeFilter !== "all") {
if (awardTypeFilter === "ranking") {
filteredAwards = filteredAwards.filter((award) => award.rank > 0 && award.rank <= 3)
} else if (awardTypeFilter === "popular") {
filteredAwards = filteredAwards.filter((award) => award.awardType === "popular")
} else {
filteredAwards = filteredAwards.filter((award) => award.awardType === awardTypeFilter)
}
}
// 竞赛类型筛选
if (awardCompetitionTypeFilter !== "all") {
filteredAwards = filteredAwards.filter((award) => award.competitionType === awardCompetitionTypeFilter)
}
return filteredAwards.sort((a, b) => {
// 按年份、月份、排名排序
if (a.year !== b.year) return b.year - a.year
if (a.month !== b.month) return b.month - a.month
if (a.rank !== b.rank) {
if (a.rank === 0) return 1
if (b.rank === 0) return -1
return a.rank - b.rank
}
return 0
})
}
const judgeScoringUrl = typeof window !== "undefined" ? `${window.location.origin}/judge-scoring` : "/judge-scoring"
// Filter out proposal competitions from display
const displayCompetitions = competitions.filter((competition) => competition.type !== "proposal")
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>
{currentCompetition && (
<div className="flex items-center mt-2 text-sm text-purple-600">
<Star className="w-4 h-4 mr-1" />
{currentCompetition.name} ({getCompetitionTypeText(currentCompetition.type)})
</div>
)}
</div>
<div className="flex space-x-3">
<Button
onClick={() => setShowCreateCompetition(true)}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700"
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 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">
{isLoadingDb ? (
<Loader2 className="w-6 h-6 animate-spin" />
) : (
dbStats?.total || displayCompetitions.length
)}
</p>
</div>
<Trophy className="w-8 h-8 text-purple-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">
{isLoadingDb ? (
<Loader2 className="w-6 h-6 animate-spin" />
) : (
dbStats?.active || displayCompetitions.filter((c) => c.status === "active").length
)}
</p>
</div>
<Play 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">{judgeStats?.totalJudges || dbJudges.length || judges.length}</p>
</div>
<Crown className="w-8 h-8 text-yellow-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">{awards.length}</p>
</div>
<Award className="w-8 h-8 text-orange-600" />
</div>
</CardContent>
</Card>
</div>
<Tabs defaultValue="competitions" className="w-full">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="competitions"></TabsTrigger>
<TabsTrigger value="teams"></TabsTrigger>
<TabsTrigger value="judges"></TabsTrigger>
<TabsTrigger value="scoring"></TabsTrigger>
<TabsTrigger value="awards"></TabsTrigger>
</TabsList>
<TabsContent value="competitions" className="space-y-4">
<Card>
<CardHeader>
<CardTitle></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>
{isLoadingDb ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8">
<Loader2 className="w-6 h-6 animate-spin mx-auto mb-2" />
<p className="text-gray-500">...</p>
</TableCell>
</TableRow>
) : (dbCompetitions.length > 0 ? dbCompetitions : displayCompetitions).length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8">
<Trophy className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500"></p>
<p className="text-sm text-gray-400"></p>
</TableCell>
</TableRow>
) : (dbCompetitions.length > 0 ? dbCompetitions : displayCompetitions).map((competition) => {
const isCurrentCompetition = currentCompetition?.id === competition.id
const scoringProgress = getScoringProgress(competition.id)
const participantCount = getParticipantCount(competition)
return (
<TableRow key={competition.id} className={isCurrentCompetition ? "bg-purple-50" : ""}>
<TableCell>
<div className="flex items-center space-x-2">
{isCurrentCompetition && <Star className="w-4 h-4 text-purple-600 fill-current" />}
<div>
<p className="font-medium">{competition.name}</p>
<p className="text-sm text-gray-500">{competition.description}</p>
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
{getCompetitionTypeIcon(competition.type)}
<Badge variant="outline" className="text-xs">
{getCompetitionTypeText(competition.type)}
</Badge>
</div>
</TableCell>
<TableCell>
<div className="text-sm">
<p>
{competition.year}{competition.month}
</p>
<p className="text-gray-500">
{formatDateRange(
competition.start_date || competition.startDate,
competition.end_date || competition.endDate
)}
</p>
</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>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Progress value={scoringProgress.percentage} className="w-16 h-2" />
<span className="text-xs text-gray-500">
{scoringProgress.completed}/{scoringProgress.total}
</span>
</div>
<p className="text-xs text-gray-500">{scoringProgress.percentage}% </p>
</div>
</TableCell>
<TableCell>
<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" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEditCompetition(competition)}>
<Edit className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleChangeStatus(competition)}>
<Settings className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleManualScoring(competition)}>
<ClipboardList className="w-4 h-4 mr-2" />
</DropdownMenuItem>
{!isCurrentCompetition && (
<DropdownMenuItem onClick={() => setCurrentCompetition(competition)}>
<Star className="w-4 h-4 mr-2" />
</DropdownMenuItem>
)}
{isCurrentCompetition && (
<DropdownMenuItem onClick={() => setCurrentCompetition(null)}>
<StarOff className="w-4 h-4 mr-2" />
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => handleDeleteCompetition(competition)}
className="text-red-600 focus:text-red-600"
>
<Trash2 className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="teams" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold"></h3>
<Button
onClick={() => {
setShowCreateTeam(true)
fetchAvailableApps()
fetchAvailableUsers()
}}
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>
{/* 搜索和篩選區域 */}
<div className="bg-gray-50 p-4 rounded-lg space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* 搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="搜索團隊名稱、隊長姓名..."
value={teamSearchTerm}
onChange={(e) => {
setTeamSearchTerm(e.target.value)
setTeamCurrentPage(1) // 重置到第一頁
}}
className="pl-10"
/>
</div>
{/* 部門篩選 */}
<Select value={teamDepartmentFilter} onValueChange={(value) => {
setTeamDepartmentFilter(value)
setTeamCurrentPage(1)
}}>
<SelectTrigger>
<SelectValue placeholder="部門篩選" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="HQBU">HQBU</SelectItem>
<SelectItem value="ITBU">ITBU</SelectItem>
<SelectItem value="MBU1">MBU1</SelectItem>
<SelectItem value="MBU2">MBU2</SelectItem>
<SelectItem value="SBU">SBU</SelectItem>
<SelectItem value="研發部"></SelectItem>
<SelectItem value="產品部"></SelectItem>
<SelectItem value="技術部"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
{/* 清除篩選按鈕 */}
<Button
variant="outline"
onClick={() => {
setTeamSearchTerm("")
setTeamDepartmentFilter("all")
setTeamCurrentPage(1)
}}
className="flex items-center space-x-2"
>
<Filter className="w-4 h-4" />
<span></span>
</Button>
</div>
{/* 結果統計 */}
<div className="text-sm text-gray-600">
{(() => {
const filtered = dbTeams.filter(team => {
const matchesSearch = teamSearchTerm === "" ||
team.name.toLowerCase().includes(teamSearchTerm.toLowerCase()) ||
(team.leader_name && team.leader_name.toLowerCase().includes(teamSearchTerm.toLowerCase()))
const matchesDepartment = teamDepartmentFilter === "all" ||
(teamDepartmentFilter === "其他"
? !["HQBU", "ITBU", "MBU1", "MBU2", "SBU", "研發部", "產品部", "技術部"].includes(team.department)
: team.department === teamDepartmentFilter)
return matchesSearch && matchesDepartment
})
return filtered.length
})()}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 min-h-[400px]" style={{ gridAutoRows: 'max-content' }}>
{(() => {
// 篩選邏輯
const filtered = dbTeams.filter(team => {
const matchesSearch = teamSearchTerm === "" ||
team.name.toLowerCase().includes(teamSearchTerm.toLowerCase()) ||
(team.leader_name && team.leader_name.toLowerCase().includes(teamSearchTerm.toLowerCase()))
const matchesDepartment = teamDepartmentFilter === "all" ||
(teamDepartmentFilter === "其他"
? !["HQBU", "ITBU", "MBU1", "MBU2", "SBU", "研發部", "產品部", "技術部"].includes(team.department)
: team.department === teamDepartmentFilter)
return matchesSearch && matchesDepartment
})
// 分頁邏輯
const startIndex = (teamCurrentPage - 1) * teamsPerPage
const endIndex = startIndex + teamsPerPage
const paginatedTeams = filtered.slice(startIndex, endIndex)
// 如果沒有找到任何團隊
if (filtered.length === 0) {
return (
<div className="col-span-full flex flex-col items-center justify-center py-12 text-gray-500 min-h-[300px]">
<Users className="w-12 h-12 mb-4 text-gray-300" />
<p className="text-lg font-medium mb-2"></p>
<p className="text-sm">
{teamSearchTerm || teamDepartmentFilter !== "all"
? "請調整搜索條件或篩選條件"
: "點擊「創建團隊」按鈕來添加第一個團隊"}
</p>
</div>
)
}
return paginatedTeams.map((team) => (
<Card key={team.id} className="relative h-fit">
<CardContent className="p-4">
<div className="space-y-3">
<div className="flex items-start justify-between">
<div>
<h4 className="font-semibold text-lg">{team.name}</h4>
<div className="flex items-center space-x-2 text-sm text-gray-600">
<User className="w-3 h-3" />
<span>{team.leader_name || team.leader}</span>
</div>
</div>
<Badge variant="outline" className="text-xs">
{team.department}
</Badge>
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center space-x-2 text-gray-600">
<Mail className="w-3 h-3" />
<span>{team.contact_email}</span>
</div>
<div className="flex items-center space-x-2 text-gray-600">
<Users className="w-3 h-3" />
<span>{team.member_count || 0} </span>
</div>
<div className="flex items-center space-x-2 text-gray-600">
<Trophy className="w-3 h-3" />
<span>{team.submittedAppCount} </span>
</div>
</div>
<p className="text-sm text-gray-500 line-clamp-2">{team.description}</p>
<div className="flex justify-end space-x-2">
<Button
variant="outline"
size="sm"
onClick={async () => {
const teamDetails = await fetchTeamDetails(team.id)
if (teamDetails) {
setSelectedTeam(teamDetails)
setShowTeamDetail(true)
}
}}
>
<Eye className="w-3 h-3 mr-1" />
</Button>
<Button variant="outline" size="sm" onClick={() => handleEditTeam(team)}>
<Edit className="w-3 h-3 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDeleteTeam(team)}
className="text-red-600 border-red-300 hover:bg-red-50"
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
</CardContent>
</Card>
))
})()}
</div>
{/* 分頁組件 */}
{(() => {
const filtered = dbTeams.filter(team => {
const matchesSearch = teamSearchTerm === "" ||
team.name.toLowerCase().includes(teamSearchTerm.toLowerCase()) ||
(team.leader_name && team.leader_name.toLowerCase().includes(teamSearchTerm.toLowerCase()))
const matchesDepartment = teamDepartmentFilter === "all" ||
(teamDepartmentFilter === "其他"
? !["HQBU", "ITBU", "MBU1", "MBU2", "SBU", "研發部", "產品部", "技術部"].includes(team.department)
: team.department === teamDepartmentFilter)
return matchesSearch && matchesDepartment
})
const totalPages = Math.ceil(filtered.length / teamsPerPage)
// 如果當前頁面超出總頁數,重置到第一頁
if (teamCurrentPage > totalPages && totalPages > 0) {
setTeamCurrentPage(1)
}
if (totalPages <= 1) return null
return (
<div className="flex flex-col items-center space-y-4 mt-6">
<div className="text-sm text-gray-600">
{teamCurrentPage} {totalPages}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setTeamCurrentPage(prev => Math.max(1, prev - 1))}
disabled={teamCurrentPage === 1}
className="flex items-center space-x-1"
>
<span></span>
<span></span>
</Button>
<div className="flex items-center space-x-1">
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
let page;
if (totalPages <= 5) {
page = i + 1;
} else if (teamCurrentPage <= 3) {
page = i + 1;
} else if (teamCurrentPage >= totalPages - 2) {
page = totalPages - 4 + i;
} else {
page = teamCurrentPage - 2 + i;
}
return (
<Button
key={page}
variant={teamCurrentPage === page ? "default" : "outline"}
size="sm"
onClick={() => setTeamCurrentPage(page)}
className="w-10 h-10 p-0"
>
{page}
</Button>
)
})}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setTeamCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={teamCurrentPage === totalPages}
className="flex items-center space-x-1"
>
<span></span>
<span></span>
</Button>
</div>
</div>
)
})()}
</TabsContent>
<TabsContent value="judges" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold"></h3>
<Button onClick={() => setShowAddJudge(true)} variant="outline">
<UserPlus className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 搜索和篩選區域 */}
<div className="bg-gray-50 p-4 rounded-lg space-y-4">
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{/* 搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="搜索評審姓名、職稱..."
value={judgeSearchTerm}
onChange={(e) => {
setJudgeSearchTerm(e.target.value)
setJudgeCurrentPage(1) // 重置到第一頁
}}
className="pl-10"
/>
</div>
{/* 部門篩選 */}
<Select value={judgeDepartmentFilter} onValueChange={(value) => {
setJudgeDepartmentFilter(value)
setJudgeCurrentPage(1)
}}>
<SelectTrigger>
<SelectValue placeholder="部門篩選" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="HQBU">HQBU</SelectItem>
<SelectItem value="ITBU">ITBU</SelectItem>
<SelectItem value="MBU1">MBU1</SelectItem>
<SelectItem value="MBU2">MBU2</SelectItem>
<SelectItem value="SBU">SBU</SelectItem>
<SelectItem value="研發部"></SelectItem>
<SelectItem value="產品部"></SelectItem>
<SelectItem value="技術部"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
{/* 專業領域篩選 */}
<Select value={judgeExpertiseFilter} onValueChange={(value) => {
setJudgeExpertiseFilter(value)
setJudgeCurrentPage(1)
}}>
<SelectTrigger>
<SelectValue placeholder="專業領域篩選" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="機器學習"></SelectItem>
<SelectItem value="深度學習"></SelectItem>
<SelectItem value="自然語言處理"></SelectItem>
<SelectItem value="計算機視覺"></SelectItem>
<SelectItem value="數據科學"></SelectItem>
<SelectItem value="人工智能"></SelectItem>
<SelectItem value="雲端計算"></SelectItem>
<SelectItem value="網路安全"></SelectItem>
<SelectItem value="軟體工程"></SelectItem>
<SelectItem value="用戶體驗"></SelectItem>
</SelectContent>
</Select>
{/* 狀態篩選 */}
<Select value={judgeStatusFilter} onValueChange={(value) => {
setJudgeStatusFilter(value)
setJudgeCurrentPage(1)
}}>
<SelectTrigger>
<SelectValue placeholder="所有狀態" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="active"></SelectItem>
<SelectItem value="inactive"></SelectItem>
</SelectContent>
</Select>
{/* 清除篩選按鈕 */}
<Button
variant="outline"
onClick={() => {
setJudgeSearchTerm("")
setJudgeDepartmentFilter("all")
setJudgeExpertiseFilter("all")
setJudgeStatusFilter("all")
setJudgeCurrentPage(1)
}}
className="flex items-center space-x-2"
>
<Filter className="w-4 h-4" />
<span></span>
</Button>
</div>
{/* 結果統計 */}
<div className="text-sm text-gray-600">
{isLoadingJudges ? (
<Loader2 className="w-4 h-4 animate-spin inline" />
) : (() => {
const judgesToShow = dbJudges.length > 0 ? dbJudges : judges;
const filtered = judgesToShow.filter(judge => {
const matchesSearch = judgeSearchTerm === "" ||
judge.name.toLowerCase().includes(judgeSearchTerm.toLowerCase()) ||
judge.title.toLowerCase().includes(judgeSearchTerm.toLowerCase())
const matchesDepartment = judgeDepartmentFilter === "all" ||
(judgeDepartmentFilter === "其他"
? !["HQBU", "ITBU", "MBU1", "MBU2", "SBU", "研發部", "產品部", "技術部"].includes(judge.department)
: judge.department === judgeDepartmentFilter)
const matchesExpertise = judgeExpertiseFilter === "all" ||
judge.expertise.some(exp => exp.includes(judgeExpertiseFilter))
const matchesStatus = judgeStatusFilter === "all" ||
(judgeStatusFilter === "active" && judge.is_active === true) ||
(judgeStatusFilter === "inactive" && judge.is_active === false)
return matchesSearch && matchesDepartment && matchesExpertise && matchesStatus
})
return filtered.length
})()}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 min-h-[400px]" style={{ gridAutoRows: 'max-content' }}>
{isLoadingJudges ? (
<div className="col-span-full flex flex-col items-center justify-center py-12 text-gray-500 min-h-[300px]">
<Loader2 className="w-8 h-8 animate-spin mb-4" />
<p className="text-lg font-medium">...</p>
</div>
) : (() => {
// 使用資料庫數據或 context 數據
const judgesToShow = dbJudges.length > 0 ? dbJudges : judges;
console.log('🔍 評審列表渲染 - dbJudges:', dbJudges.map(j => ({ id: j.id, name: j.name, is_active: j.is_active })))
console.log('🔍 評審列表渲染 - judges (context):', judges.map(j => ({ id: j.id, name: j.name, is_active: j.is_active })))
console.log('🔍 評審列表渲染 - judgesToShow:', judgesToShow.map(j => ({ id: j.id, name: j.name, is_active: j.is_active })))
// 篩選邏輯
const filtered = judgesToShow.filter(judge => {
const matchesSearch = judgeSearchTerm === "" ||
judge.name.toLowerCase().includes(judgeSearchTerm.toLowerCase()) ||
judge.title.toLowerCase().includes(judgeSearchTerm.toLowerCase())
const matchesDepartment = judgeDepartmentFilter === "all" ||
(judgeDepartmentFilter === "其他"
? !["HQBU", "ITBU", "MBU1", "MBU2", "SBU", "研發部", "產品部", "技術部"].includes(judge.department)
: judge.department === judgeDepartmentFilter)
const matchesExpertise = judgeExpertiseFilter === "all" ||
judge.expertise.some(exp => exp.includes(judgeExpertiseFilter))
const matchesStatus = judgeStatusFilter === "all" ||
(judgeStatusFilter === "active" && judge.is_active === true) ||
(judgeStatusFilter === "inactive" && judge.is_active === false)
return matchesSearch && matchesDepartment && matchesExpertise && matchesStatus
})
// 分頁邏輯
const startIndex = (judgeCurrentPage - 1) * judgesPerPage
const endIndex = startIndex + judgesPerPage
const paginatedJudges = filtered.slice(startIndex, endIndex)
// 如果沒有找到任何評審
if (filtered.length === 0) {
return (
<div className="col-span-full flex flex-col items-center justify-center py-12 text-gray-500 min-h-[300px]">
<Users className="w-12 h-12 mb-4 text-gray-300" />
<p className="text-lg font-medium mb-2"></p>
<p className="text-sm">
{judgeSearchTerm || judgeDepartmentFilter !== "all" || judgeExpertiseFilter !== "all"
? "請調整搜索條件或篩選條件"
: "點擊「新增評審」按鈕來添加第一位評審"}
</p>
</div>
)
}
return paginatedJudges.map((judge) => (
<Card key={judge.id} className={`h-fit ${!judge.is_active ? 'opacity-75 border-orange-200 bg-orange-50' : ''}`}>
<CardContent className="p-4">
<div className="flex items-center space-x-3 mb-3">
<Avatar>
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
<AvatarFallback className={`${!judge.is_active ? 'bg-orange-100 text-orange-700' : 'bg-purple-100 text-purple-700'}`}>
{judge.name[0]}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center space-x-2">
<h4 className="font-medium">{judge.name}</h4>
{judge.is_active === false && (
<Badge variant="secondary" className="text-xs bg-orange-100 text-orange-700 border-orange-200">
</Badge>
)}
</div>
<p className="text-sm text-gray-600">{judge.title}</p>
<p className="text-xs text-gray-500">{judge.department}</p>
<p className="text-xs text-blue-600 mt-1">ID: {judge.id}</p>
</div>
</div>
<div className="mb-3">
<div className="flex flex-wrap gap-1">
{judge.expertise.slice(0, 3).map((skill) => (
<Badge key={skill} variant="secondary" className="text-xs">
{skill}
</Badge>
))}
{judge.expertise.length > 3 && (
<Badge variant="secondary" className="text-xs">
+{judge.expertise.length - 3}
</Badge>
)}
</div>
</div>
<div className="flex justify-end space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedJudge(judge)
setShowJudgeDetail(true)
}}
>
<Eye className="w-3 h-3 mr-1" />
</Button>
<Button variant="outline" size="sm" onClick={() => handleEditJudge(judge)}>
<Edit className="w-3 h-3 mr-1" />
</Button>
{judge.is_active !== false ? (
<Button
variant="outline"
size="sm"
onClick={() => handleDisableJudge(judge)}
className="text-orange-600 border-orange-300 hover:bg-orange-50"
>
<UserX className="w-3 h-3 mr-1" />
</Button>
) : (
<Button
variant="outline"
size="sm"
onClick={() => handleEnableJudge(judge)}
className="text-green-600 border-green-300 hover:bg-green-50"
>
<UserCheck className="w-3 h-3 mr-1" />
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={() => handleDeleteJudge(judge)}
className="text-red-600 border-red-300 hover:bg-red-50"
>
<Trash2 className="w-3 h-3 mr-1" />
</Button>
</div>
</CardContent>
</Card>
))
})()}
</div>
{/* 分頁組件 */}
{(() => {
// 使用與主列表相同的數據源
const judgesToShow = dbJudges.length > 0 ? dbJudges : judges;
const filtered = judgesToShow.filter(judge => {
const matchesSearch = judgeSearchTerm === "" ||
judge.name.toLowerCase().includes(judgeSearchTerm.toLowerCase()) ||
judge.title.toLowerCase().includes(judgeSearchTerm.toLowerCase())
const matchesDepartment = judgeDepartmentFilter === "all" ||
(judgeDepartmentFilter === "其他"
? !["HQBU", "ITBU", "MBU1", "MBU2", "SBU", "研發部", "產品部", "技術部"].includes(judge.department)
: judge.department === judgeDepartmentFilter)
const matchesExpertise = judgeExpertiseFilter === "all" ||
judge.expertise.some(exp => exp.includes(judgeExpertiseFilter))
const matchesStatus = judgeStatusFilter === "all" ||
(judgeStatusFilter === "active" && judge.is_active === true) ||
(judgeStatusFilter === "inactive" && judge.is_active === false)
return matchesSearch && matchesDepartment && matchesExpertise && matchesStatus
})
const totalPages = Math.ceil(filtered.length / judgesPerPage)
// 如果當前頁面超出總頁數,重置到第一頁
if (judgeCurrentPage > totalPages && totalPages > 0) {
setJudgeCurrentPage(1)
}
if (totalPages <= 1) return null
return (
<div className="flex flex-col items-center space-y-4 mt-6">
<div className="text-sm text-gray-600">
{judgeCurrentPage} {totalPages}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setJudgeCurrentPage(prev => Math.max(1, prev - 1))}
disabled={judgeCurrentPage === 1}
className="flex items-center space-x-1"
>
<span></span>
<span></span>
</Button>
<div className="flex items-center space-x-1">
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
let page;
if (totalPages <= 5) {
page = i + 1;
} else if (judgeCurrentPage <= 3) {
page = i + 1;
} else if (judgeCurrentPage >= totalPages - 2) {
page = totalPages - 4 + i;
} else {
page = judgeCurrentPage - 2 + i;
}
return (
<Button
key={page}
variant={judgeCurrentPage === page ? "default" : "outline"}
size="sm"
onClick={() => setJudgeCurrentPage(page)}
className="w-10 h-10 p-0"
>
{page}
</Button>
)
})}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setJudgeCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={judgeCurrentPage === totalPages}
className="flex items-center space-x-1"
>
<span></span>
<span></span>
</Button>
</div>
</div>
)
})()}
</TabsContent>
<TabsContent value="scoring" className="space-y-4">
<ScoringManagement />
</TabsContent>
<TabsContent value="awards" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold"></h3>
<Button onClick={() => setShowCreateAward(true)} variant="outline">
<Award className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 搜索和筛选控件 */}
{awards.length > 0 && (
<Card>
<CardContent className="p-4">
<div className="space-y-4">
{/* 搜索栏 */}
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="h-4 w-4 text-gray-400" />
</div>
<Input
type="text"
placeholder="搜尋應用名稱、創作者或獎項名稱..."
value={awardSearchQuery}
onChange={(e) => {
setAwardSearchQuery(e.target.value)
resetAwardPagination()
}}
className="pl-10 pr-10 w-full md:w-96"
/>
{awardSearchQuery && (
<button
onClick={() => setAwardSearchQuery("")}
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
>
<X className="h-4 w-4" />
</button>
)}
</div>
{/* 筛选控件 */}
<div className="flex flex-wrap gap-4 items-center">
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700"></span>
<Select value={awardYearFilter} onValueChange={(value) => {
setAwardYearFilter(value)
resetAwardPagination()
}}>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2023">2023</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700"></span>
<Select value={awardMonthFilter} onValueChange={(value) => {
setAwardMonthFilter(value)
resetAwardPagination()
}}>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
<SelectItem value="5">5</SelectItem>
<SelectItem value="6">6</SelectItem>
<SelectItem value="7">7</SelectItem>
<SelectItem value="8">8</SelectItem>
<SelectItem value="9">9</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="11">11</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700"></span>
<Select value={awardTypeFilter} onValueChange={(value) => {
setAwardTypeFilter(value)
resetAwardPagination()
}}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="ranking"></SelectItem>
<SelectItem value="popular"></SelectItem>
<SelectItem value="gold"></SelectItem>
<SelectItem value="silver"></SelectItem>
<SelectItem value="bronze"></SelectItem>
<SelectItem value="innovation"></SelectItem>
<SelectItem value="technical"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700"></span>
<Select value={awardCompetitionTypeFilter} onValueChange={(value) => {
setAwardCompetitionTypeFilter(value)
resetAwardPagination()
}}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="individual"></SelectItem>
<SelectItem value="team"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 清除筛选按钮 */}
{(awardSearchQuery || awardYearFilter !== "all" || awardMonthFilter !== "all" || awardTypeFilter !== "all" || awardCompetitionTypeFilter !== "all") && (
<div className="flex items-center">
<Button
variant="outline"
size="sm"
onClick={() => {
setAwardSearchQuery("")
setAwardYearFilter("all")
setAwardMonthFilter("all")
setAwardTypeFilter("all")
setAwardCompetitionTypeFilter("all")
}}
className="text-gray-600 hover:text-gray-800"
>
<X className="w-4 h-4 mr-1" />
</Button>
</div>
)}
</div>
{/* 统计信息 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
<div className="text-center p-3 bg-blue-50 rounded-lg">
<div className="text-lg font-bold text-blue-600">{getFilteredAwards().length}</div>
<div className="text-xs text-blue-600"></div>
</div>
<div className="text-center p-3 bg-yellow-50 rounded-lg">
<div className="text-lg font-bold text-yellow-600">
{getFilteredAwards().filter((a) => a.rank > 0 && a.rank <= 3).length}
</div>
<div className="text-xs text-yellow-600"></div>
</div>
<div className="text-center p-3 bg-red-50 rounded-lg">
<div className="text-lg font-bold text-red-600">
{getFilteredAwards().filter((a) => a.awardType === "popular").length}
</div>
<div className="text-xs text-red-600"></div>
</div>
<div className="text-center p-3 bg-green-50 rounded-lg">
<div className="text-lg font-bold text-green-600">
{new Set(getFilteredAwards().map((a) => `${a.year}-${a.month}`)).size}
</div>
<div className="text-xs text-green-600"></div>
</div>
</div>
</div>
</CardContent>
</Card>
)}
{awards.length === 0 ? (
<div className="text-center py-12 text-gray-500">
<Award className="w-16 h-16 mx-auto mb-4 text-gray-300" />
<h3 className="text-xl font-semibold text-gray-600 mb-2"></h3>
<p className="text-gray-500 mb-4"></p>
<Button onClick={() => setShowCreateAward(true)} className="bg-gradient-to-r from-orange-600 to-red-600 hover:from-orange-700 hover:to-red-700">
<Award className="w-4 h-4 mr-2" />
</Button>
</div>
) : (
<>
{(() => {
const filteredAwards = getFilteredAwards()
if (filteredAwards.length === 0) {
return (
<Card>
<CardContent className="text-center py-12">
<div className="space-y-4">
{awardSearchQuery ? (
<Search className="w-16 h-16 text-gray-400 mx-auto" />
) : (
<Award className="w-16 h-16 text-gray-400 mx-auto" />
)}
<div>
<h3 className="text-xl font-semibold text-gray-600 mb-2">
{awardSearchQuery ? (
<>{awardSearchQuery}</>
) : (
<></>
)}
</h3>
<p className="text-gray-500">
{awardSearchQuery
? "嘗試使用其他關鍵字或調整篩選條件"
: "請調整篩選條件查看其他獎項"}
</p>
</div>
<div className="flex justify-center gap-2">
<Button
variant="outline"
className="bg-transparent"
onClick={() => {
setAwardSearchQuery("")
setAwardYearFilter("all")
setAwardMonthFilter("all")
setAwardTypeFilter("all")
setAwardCompetitionTypeFilter("all")
}}
>
<X className="w-4 h-4 mr-1" />
</Button>
{awardSearchQuery && (
<Button
variant="outline"
className="bg-transparent"
onClick={() => setAwardSearchQuery("")}
>
</Button>
)}
</div>
</div>
</CardContent>
</Card>
)
}
const startIndex = (awardCurrentPage - 1) * awardsPerPage
const endIndex = startIndex + awardsPerPage
const paginatedAwards = filteredAwards.slice(startIndex, endIndex)
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 min-h-[400px]" style={{ gridAutoRows: 'max-content' }}>
{paginatedAwards.map((award) => (
<Card key={award.id} className="relative overflow-hidden hover:shadow-lg transition-shadow">
<div className="absolute top-4 right-4 text-2xl">{award.icon}</div>
<CardContent className="p-4">
<div className="space-y-3">
{/* 獎項基本資訊 */}
<div className="space-y-2">
<Badge
variant="secondary"
className={`w-fit ${
award.awardType === "gold"
? "bg-yellow-100 text-yellow-800"
: award.awardType === "silver"
? "bg-gray-100 text-gray-800"
: award.awardType === "bronze"
? "bg-orange-100 text-orange-800"
: award.awardType === "popular"
? "bg-purple-100 text-purple-800"
: award.awardType === "innovation"
? "bg-green-100 text-green-800"
: award.awardType === "technical"
? "bg-indigo-100 text-indigo-800"
: "bg-blue-100 text-blue-800"
}`}
>
{award.awardName}
</Badge>
<h4 className="font-semibold text-lg pr-8">{award.appName || "團隊作品"}</h4>
<p className="text-sm text-gray-600">by {award.creator}</p>
{/* 評分顯示 */}
{award.score > 0 && (
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-1">
<Star className="w-4 h-4 text-yellow-500 fill-current" />
<span className="font-semibold text-orange-600">{award.score.toFixed(1)}</span>
</div>
<span className="text-xs text-gray-500"></span>
</div>
)}
{/* 獎項排名 */}
{award.rank > 0 && (
<Badge variant="outline" className="w-fit">
{award.rank}
</Badge>
)}
</div>
{/* 競賽資訊 */}
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500">
{award.year}{award.month}
</span>
<Badge variant="outline" className="text-xs">
{getCompetitionTypeText(award.competitionType)}
</Badge>
</div>
{/* 應用連結摘要 */}
{(award as any).applicationLinks && (
<div className="space-y-2">
<p className="text-xs font-medium text-gray-700"></p>
<div className="flex items-center space-x-2">
{(award as any).applicationLinks.production && (
<div className="w-2 h-2 bg-green-500 rounded-full" title="生產環境"></div>
)}
{(award as any).applicationLinks.demo && (
<div className="w-2 h-2 bg-blue-500 rounded-full" title="演示版本"></div>
)}
{(award as any).applicationLinks.github && (
<div className="w-2 h-2 bg-gray-800 rounded-full" title="源碼倉庫"></div>
)}
</div>
</div>
)}
{/* 相關文檔摘要 */}
{(award as any).documents && (award as any).documents.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-gray-700"></p>
<div className="flex items-center space-x-1">
<span className="text-xs text-gray-600">
{(award as any).documents.length}
</span>
<Badge variant="outline" className="text-xs">
{(award as any).documents.map((doc: any) => doc.type).join(", ")}
</Badge>
</div>
</div>
)}
{/* 得獎照片摘要 */}
{(award as any).photos && (award as any).photos.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-gray-700"></p>
<div className="flex items-center space-x-2">
<span className="text-xs text-gray-600">
{(award as any).photos.length}
</span>
<div className="flex items-center space-x-1">
{(award as any).photos.slice(0, 3).map((photo: any, index: number) => (
<div key={index} className="w-4 h-4 bg-gray-200 rounded border"></div>
))}
{(award as any).photos.length > 3 && (
<span className="text-xs text-gray-500">+{(award as any).photos.length - 3}</span>
)}
</div>
</div>
</div>
)}
{/* 獎項描述 */}
{(award as any).description && (
<p className="text-xs text-gray-600 line-clamp-2">
{(award as any).description}
</p>
)}
{/* 操作按鈕 */}
<div className="flex items-center justify-between pt-2 border-t">
<div className="flex items-center space-x-2">
<Badge variant="outline" className="text-xs">
{(award as any).category || "innovation"}
</Badge>
</div>
<div className="flex items-center space-x-1">
<Button
variant="outline"
size="sm"
onClick={() => handleViewAward(award)}
>
<Eye className="w-3 h-3 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleEditAward(award)}
>
<Edit className="w-3 h-3 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDeleteAward(award)}
className="text-red-600 border-red-300 hover:bg-red-50"
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)
})()}
{/* 分頁組件 */}
{(() => {
const filteredAwards = getFilteredAwards()
const totalPages = Math.ceil(filteredAwards.length / awardsPerPage)
// 如果當前頁面超出總頁數,重置到第一頁
if (awardCurrentPage > totalPages && totalPages > 0) {
setAwardCurrentPage(1)
}
if (totalPages <= 1) return null
return (
<div className="flex flex-col items-center space-y-4 mt-6">
<div className="text-sm text-gray-600">
{awardCurrentPage} {totalPages} {filteredAwards.length}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setAwardCurrentPage(prev => Math.max(1, prev - 1))}
disabled={awardCurrentPage === 1}
className="flex items-center space-x-1"
>
<span></span>
<span></span>
</Button>
<div className="flex items-center space-x-1">
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
let page;
if (totalPages <= 5) {
page = i + 1;
} else if (awardCurrentPage <= 3) {
page = i + 1;
} else if (awardCurrentPage >= totalPages - 2) {
page = totalPages - 4 + i;
} else {
page = awardCurrentPage - 2 + i;
}
return (
<Button
key={page}
variant={awardCurrentPage === page ? "default" : "outline"}
size="sm"
onClick={() => setAwardCurrentPage(page)}
className="w-10 h-10 p-0"
>
{page}
</Button>
)
})}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setAwardCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={awardCurrentPage === totalPages}
className="flex items-center space-x-1"
>
<span></span>
<span></span>
</Button>
</div>
</div>
)
})()}
</>
)}
</TabsContent>
</Tabs>
{/* Create Competition Dialog - Enhanced for Mixed Competitions */}
<Dialog
open={showCreateCompetition}
onOpenChange={(open) => {
setShowCreateCompetition(open)
if (!open) {
setCreateError("")
setSelectedCompetitionForAction(null) // 清除編輯狀態
resetForm() // 重置表單
}
}}
>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{selectedCompetitionForAction ? '編輯競賽' : '創建新競賽'}</DialogTitle>
<DialogDescription>{selectedCompetitionForAction ? '修改競賽的基本資訊、類型和評比規則' : '設定競賽的基本資訊、類型和評比規則'}</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{createError && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{createError}</AlertDescription>
</Alert>
)}
{/* Basic Information */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center">
<Settings className="w-5 h-5 mr-2" />
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name"> *</Label>
<Input
id="name"
value={newCompetition.name}
onChange={(e) => setNewCompetition({ ...newCompetition, name: e.target.value })}
placeholder="輸入競賽名稱"
/>
</div>
<div className="space-y-2">
<Label htmlFor="type"> *</Label>
<Select
value={newCompetition.type}
onValueChange={(value: any) =>
setNewCompetition({
...newCompetition,
type: value,
participatingApps: [],
participatingTeams: [],
})
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="individual">
<div className="flex items-center space-x-2">
<User className="w-4 h-4" />
<span></span>
</div>
</SelectItem>
<SelectItem value="team">
<div className="flex items-center space-x-2">
<Users className="w-4 h-4" />
<span></span>
</div>
</SelectItem>
<SelectItem value="mixed">
<div className="flex items-center space-x-2">
<Trophy className="w-4 h-4" />
<span></span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="startDate"> *</Label>
<Input
id="startDate"
type="date"
value={newCompetition.startDate}
onChange={(e) => setNewCompetition({ ...newCompetition, startDate: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="endDate"> *</Label>
<Input
id="endDate"
type="date"
value={newCompetition.endDate}
onChange={(e) => setNewCompetition({ ...newCompetition, endDate: e.target.value })}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={newCompetition.description}
onChange={(e) => setNewCompetition({ ...newCompetition, description: e.target.value })}
placeholder="描述競賽的目標和規則"
rows={3}
/>
</div>
</div>
<Separator />
{/* Mixed Competition Configuration */}
{newCompetition.type === "mixed" ? (
<div className="space-y-6">
<h3 className="text-lg font-semibold flex items-center">
<Trophy className="w-5 h-5 mr-2" />
</h3>
<Tabs defaultValue="individual" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="individual" className="flex items-center space-x-2">
<User className="w-4 h-4" />
<span></span>
</TabsTrigger>
<TabsTrigger value="team" className="flex items-center space-x-2">
<Users className="w-4 h-4" />
<span></span>
</TabsTrigger>
</TabsList>
{/* Individual Competition Configuration */}
<TabsContent value="individual" className="space-y-6">
<div className="border rounded-lg p-6 bg-blue-50">
<h4 className="text-lg font-semibold mb-4 flex items-center">
<User className="w-5 h-5 mr-2 text-blue-600" />
</h4>
{/* Individual Judge Selection */}
<div className="space-y-4 mb-6">
<h5 className="font-semibold flex items-center">
<UserCheck className="w-4 h-4 mr-2" />
</h5>
<div className="grid grid-cols-2 gap-4">
{(dbJudges.length > 0 ? dbJudges : judges).map((judge) => (
<div key={judge.id} className="flex items-center space-x-3 p-3 border rounded-lg bg-white">
<Checkbox
id={`individual-judge-${judge.id}`}
checked={newCompetition.individualConfig.judges.includes(judge.id)}
onCheckedChange={(checked) => {
if (checked) {
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
judges: [...newCompetition.individualConfig.judges, judge.id],
},
})
} else {
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
judges: newCompetition.individualConfig.judges.filter((id) => id !== judge.id),
},
})
}
}}
/>
<Avatar className="w-8 h-8">
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
<AvatarFallback className="bg-blue-100 text-blue-700 text-sm">
{judge.name[0]}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-medium text-sm">{judge.name}</p>
<p className="text-xs text-gray-600">{judge.title}</p>
</div>
</div>
))}
</div>
{newCompetition.individualConfig.judges.length > 0 && (
<p className="text-sm text-blue-600">
{newCompetition.individualConfig.judges.length}
</p>
)}
</div>
{/* Individual Evaluation Rules */}
<div className="space-y-4 mb-6">
<div className="flex justify-between items-center">
<h5 className="font-semibold flex items-center">
<ClipboardList className="w-4 h-4 mr-2" />
</h5>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const newRule: CompetitionRule = {
id: `ir${Date.now()}`,
name: "",
description: "",
weight: 0,
}
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
rules: [...newCompetition.individualConfig.rules, newRule],
},
})
}}
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{newCompetition.individualConfig.rules.length > 0 && (
<div className="space-y-3">
{newCompetition.individualConfig.rules.map((rule, index) => (
<div
key={rule.id}
className="grid grid-cols-12 gap-3 items-end p-3 border rounded-lg bg-white"
>
<div className="col-span-4">
<Label className="text-xs"></Label>
<Input
value={rule.name || ""}
onChange={(e) => {
const updatedRules = [...newCompetition.individualConfig.rules]
updatedRules[index] = { ...rule, name: e.target.value }
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
rules: updatedRules,
},
})
}}
placeholder="例如:創新性"
className="text-sm"
/>
</div>
<div className="col-span-5">
<Label className="text-xs"></Label>
<Input
value={rule.description || ""}
onChange={(e) => {
const updatedRules = [...newCompetition.individualConfig.rules]
updatedRules[index] = { ...rule, description: e.target.value }
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
rules: updatedRules,
},
})
}}
placeholder="例如:技術創新程度和獨特性"
className="text-sm"
/>
</div>
<div className="col-span-2">
<Label className="text-xs"> (%)</Label>
<Input
type="number"
min="0"
max="100"
value={rule.weight || 0}
onChange={(e) => {
const updatedRules = [...newCompetition.individualConfig.rules]
updatedRules[index] = { ...rule, weight: Number.parseInt(e.target.value) || 0 }
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
rules: updatedRules,
},
})
}}
className="text-sm"
/>
</div>
<div className="col-span-1">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const updatedRules = newCompetition.individualConfig.rules.filter(
(_, i) => i !== index,
)
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
rules: updatedRules,
},
})
}}
className="text-red-600 border-red-300 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
))}
<div className="flex items-center justify-between p-3 bg-blue-100 rounded-lg">
<span className="text-sm text-blue-700">
{calculateTotalWeight(newCompetition.individualConfig.rules).toFixed(2)}%
</span>
{calculateTotalWeight(newCompetition.individualConfig.rules) !== 100 && <span className="text-sm text-orange-600"> 100%</span>}
</div>
</div>
)}
</div>
{/* Individual Award Types */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<h5 className="font-semibold flex items-center">
<Award className="w-4 h-4 mr-2" />
</h5>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const newAwardType: CompetitionAwardType = {
id: `iat${Date.now()}`,
name: "",
description: "",
icon: "🏆",
color: "text-yellow-600",
}
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
awardTypes: [...newCompetition.individualConfig.awardTypes, newAwardType],
},
})
}}
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{newCompetition.individualConfig.awardTypes.length > 0 && (
<div className="space-y-3">
{newCompetition.individualConfig.awardTypes.map((awardType, index) => (
<div
key={awardType.id}
className="grid grid-cols-12 gap-3 items-end p-3 border rounded-lg bg-white"
>
<div className="col-span-1">
<Label className="text-xs"></Label>
<Select
value={awardType.icon || "🏆"}
onValueChange={(value) => {
const updatedAwardTypes = [...newCompetition.individualConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, icon: value }
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
awardTypes: updatedAwardTypes,
},
})
}}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="🏆">🏆</SelectItem>
<SelectItem value="🥇">🥇</SelectItem>
<SelectItem value="🥈">🥈</SelectItem>
<SelectItem value="🥉">🥉</SelectItem>
<SelectItem value="⭐"></SelectItem>
<SelectItem value="💡">💡</SelectItem>
<SelectItem value="⚙️"></SelectItem>
<SelectItem value="🎯">🎯</SelectItem>
<SelectItem value="❤️"></SelectItem>
<SelectItem value="🧠">🧠</SelectItem>
<SelectItem value="🚀">🚀</SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-3">
<Label className="text-xs"></Label>
<Input
value={awardType.name || ""}
onChange={(e) => {
const updatedAwardTypes = [...newCompetition.individualConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, name: e.target.value }
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
awardTypes: updatedAwardTypes,
},
})
}}
placeholder="例如:最佳創新獎"
className="text-sm"
/>
</div>
<div className="col-span-5">
<Label className="text-xs"></Label>
<Input
value={awardType.description || ""}
onChange={(e) => {
const updatedAwardTypes = [...newCompetition.individualConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, description: e.target.value }
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
awardTypes: updatedAwardTypes,
},
})
}}
placeholder="例如:最具創新性的應用"
className="text-sm"
/>
</div>
<div className="col-span-2">
<Label className="text-xs"></Label>
<Select
value={awardType.color || "text-yellow-600"}
onValueChange={(value) => {
const updatedAwardTypes = [...newCompetition.individualConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, color: value }
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
awardTypes: updatedAwardTypes,
},
})
}}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text-yellow-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-yellow-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-blue-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-green-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-green-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-red-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-red-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-purple-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-purple-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-orange-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-orange-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-1">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const updatedAwardTypes = newCompetition.individualConfig.awardTypes.filter(
(_, i) => i !== index,
)
setNewCompetition({
...newCompetition,
individualConfig: {
...newCompetition.individualConfig,
awardTypes: updatedAwardTypes,
},
})
}}
className="text-red-600 border-red-300 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
))}
<div className="flex items-center justify-between p-3 bg-blue-100 rounded-lg">
<span className="text-sm text-blue-700">
{newCompetition.individualConfig.awardTypes.length}
</span>
<div className="flex space-x-1">
{newCompetition.individualConfig.awardTypes.slice(0, 5).map((awardType, index) => (
<span key={index} className="text-lg">
{awardType.icon}
</span>
))}
{newCompetition.individualConfig.awardTypes.length > 5 && (
<span className="text-sm text-gray-500">
+{newCompetition.individualConfig.awardTypes.length - 5}
</span>
)}
</div>
</div>
</div>
)}
</div>
</div>
</TabsContent>
{/* Team Competition Configuration */}
<TabsContent value="team" className="space-y-6">
<div className="border rounded-lg p-6 bg-green-50">
<h4 className="text-lg font-semibold mb-4 flex items-center">
<Users className="w-5 h-5 mr-2 text-green-600" />
</h4>
{/* Team Judge Selection */}
<div className="space-y-4 mb-6">
<h5 className="font-semibold flex items-center">
<UserCheck className="w-4 h-4 mr-2" />
</h5>
<div className="grid grid-cols-2 gap-4">
{(dbJudges.length > 0 ? dbJudges : judges).map((judge) => (
<div key={judge.id} className="flex items-center space-x-3 p-3 border rounded-lg bg-white">
<Checkbox
id={`team-judge-${judge.id}`}
checked={newCompetition.teamConfig.judges.includes(judge.id)}
onCheckedChange={(checked) => {
if (checked) {
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
judges: [...newCompetition.teamConfig.judges, judge.id],
},
})
} else {
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
judges: newCompetition.teamConfig.judges.filter((id) => id !== judge.id),
},
})
}
}}
/>
<Avatar className="w-8 h-8">
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
<AvatarFallback className="bg-green-100 text-green-700 text-sm">
{judge.name[0]}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-medium text-sm">{judge.name}</p>
<p className="text-xs text-gray-600">{judge.title}</p>
</div>
</div>
))}
</div>
{newCompetition.teamConfig.judges.length > 0 && (
<p className="text-sm text-green-600">
{newCompetition.teamConfig.judges.length}
</p>
)}
</div>
{/* Team Evaluation Rules */}
<div className="space-y-4 mb-6">
<div className="flex justify-between items-center">
<h5 className="font-semibold flex items-center">
<ClipboardList className="w-4 h-4 mr-2" />
</h5>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const newRule: CompetitionRule = {
id: `tr${Date.now()}`,
name: "",
description: "",
weight: 0,
}
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
rules: [...newCompetition.teamConfig.rules, newRule],
},
})
}}
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{newCompetition.teamConfig.rules.length > 0 && (
<div className="space-y-3">
{newCompetition.teamConfig.rules.map((rule, index) => (
<div
key={rule.id}
className="grid grid-cols-12 gap-3 items-end p-3 border rounded-lg bg-white"
>
<div className="col-span-4">
<Label className="text-xs"></Label>
<Input
value={rule.name || ""}
onChange={(e) => {
const updatedRules = [...newCompetition.teamConfig.rules]
updatedRules[index] = { ...rule, name: e.target.value }
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
rules: updatedRules,
},
})
}}
placeholder="例如:團隊合作"
className="text-sm"
/>
</div>
<div className="col-span-5">
<Label className="text-xs"></Label>
<Input
value={rule.description || ""}
onChange={(e) => {
const updatedRules = [...newCompetition.teamConfig.rules]
updatedRules[index] = { ...rule, description: e.target.value }
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
rules: updatedRules,
},
})
}}
placeholder="例如:團隊協作能力和分工效率"
className="text-sm"
/>
</div>
<div className="col-span-2">
<Label className="text-xs"> (%)</Label>
<Input
type="number"
min="0"
max="100"
value={rule.weight || 0}
onChange={(e) => {
const updatedRules = [...newCompetition.teamConfig.rules]
updatedRules[index] = { ...rule, weight: Number.parseInt(e.target.value) || 0 }
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
rules: updatedRules,
},
})
}}
className="text-sm"
/>
</div>
<div className="col-span-1">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const updatedRules = newCompetition.teamConfig.rules.filter((_, i) => i !== index)
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
rules: updatedRules,
},
})
}}
className="text-red-600 border-red-300 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
))}
<div className="flex items-center justify-between p-3 bg-green-100 rounded-lg">
<span className="text-sm text-green-700">
{calculateTotalWeight(newCompetition.teamConfig.rules).toFixed(2)}%
</span>
{calculateTotalWeight(newCompetition.teamConfig.rules) !== 100 && (
<span className="text-sm text-orange-600"> 100%</span>
)}
</div>
</div>
)}
</div>
{/* Team Award Types */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<h5 className="font-semibold flex items-center">
<Award className="w-4 h-4 mr-2" />
</h5>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const newAwardType: CompetitionAwardType = {
id: `tat${Date.now()}`,
name: "",
description: "",
icon: "🏆",
color: "text-yellow-600",
}
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
awardTypes: [...newCompetition.teamConfig.awardTypes, newAwardType],
},
})
}}
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{newCompetition.teamConfig.awardTypes.length > 0 && (
<div className="space-y-3">
{newCompetition.teamConfig.awardTypes.map((awardType, index) => (
<div
key={awardType.id}
className="grid grid-cols-12 gap-3 items-end p-3 border rounded-lg bg-white"
>
<div className="col-span-1">
<Label className="text-xs"></Label>
<Select
value={awardType.icon || "🏆"}
onValueChange={(value) => {
const updatedAwardTypes = [...newCompetition.teamConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, icon: value }
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
awardTypes: updatedAwardTypes,
},
})
}}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="🏆">🏆</SelectItem>
<SelectItem value="🥇">🥇</SelectItem>
<SelectItem value="🥈">🥈</SelectItem>
<SelectItem value="🥉">🥉</SelectItem>
<SelectItem value="⭐"></SelectItem>
<SelectItem value="💡">💡</SelectItem>
<SelectItem value="⚙️"></SelectItem>
<SelectItem value="🎯">🎯</SelectItem>
<SelectItem value="❤️"></SelectItem>
<SelectItem value="👥">👥</SelectItem>
<SelectItem value="🧠">🧠</SelectItem>
<SelectItem value="🚀">🚀</SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-3">
<Label className="text-xs"></Label>
<Input
value={awardType.name || ""}
onChange={(e) => {
const updatedAwardTypes = [...newCompetition.teamConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, name: e.target.value }
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
awardTypes: updatedAwardTypes,
},
})
}}
placeholder="例如:最佳團隊合作獎"
className="text-sm"
/>
</div>
<div className="col-span-5">
<Label className="text-xs"></Label>
<Input
value={awardType.description || ""}
onChange={(e) => {
const updatedAwardTypes = [...newCompetition.teamConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, description: e.target.value }
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
awardTypes: updatedAwardTypes,
},
})
}}
placeholder="例如:團隊協作最佳的團隊"
className="text-sm"
/>
</div>
<div className="col-span-2">
<Label className="text-xs"></Label>
<Select
value={awardType.color || "text-yellow-600"}
onValueChange={(value) => {
const updatedAwardTypes = [...newCompetition.teamConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, color: value }
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
awardTypes: updatedAwardTypes,
},
})
}}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text-yellow-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-yellow-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-blue-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-green-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-green-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-red-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-red-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-purple-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-purple-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-orange-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-orange-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-1">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const updatedAwardTypes = newCompetition.teamConfig.awardTypes.filter(
(_, i) => i !== index,
)
setNewCompetition({
...newCompetition,
teamConfig: {
...newCompetition.teamConfig,
awardTypes: updatedAwardTypes,
},
})
}}
className="text-red-600 border-red-300 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
))}
<div className="flex items-center justify-between p-3 bg-green-100 rounded-lg">
<span className="text-sm text-green-700">
{newCompetition.teamConfig.awardTypes.length}
</span>
<div className="flex space-x-1">
{newCompetition.teamConfig.awardTypes.slice(0, 5).map((awardType, index) => (
<span key={index} className="text-lg">
{awardType.icon}
</span>
))}
{newCompetition.teamConfig.awardTypes.length > 5 && (
<span className="text-sm text-gray-500">
+{newCompetition.teamConfig.awardTypes.length - 5}
</span>
)}
</div>
</div>
</div>
)}
</div>
</div>
</TabsContent>
</Tabs>
</div>
) : (
// Single Competition Type Configuration
<>
{/* Judge Selection */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center">
<UserCheck className="w-5 h-5 mr-2" />
*
</h3>
<div className="grid grid-cols-2 gap-4">
{(dbJudges.length > 0 ? dbJudges : judges).map((judge) => (
<div key={judge.id} className="flex items-center space-x-3 p-3 border rounded-lg">
<Checkbox
id={`judge-${judge.id}`}
checked={newCompetition.judges.includes(judge.id)}
onCheckedChange={(checked) => {
if (checked) {
setNewCompetition({
...newCompetition,
judges: [...newCompetition.judges, judge.id],
})
} else {
setNewCompetition({
...newCompetition,
judges: newCompetition.judges.filter((id) => id !== judge.id),
})
}
}}
/>
<Avatar className="w-8 h-8">
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
<AvatarFallback className="bg-purple-100 text-purple-700 text-sm">
{judge.name[0]}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-medium text-sm">{judge.name}</p>
<p className="text-xs text-gray-600">{judge.title}</p>
</div>
</div>
))}
</div>
{newCompetition.judges.length > 0 && (
<p className="text-sm text-green-600"> {newCompetition.judges.length} </p>
)}
</div>
<Separator />
{/* Evaluation Rules */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center">
<ClipboardList className="w-5 h-5 mr-2" />
</h3>
<div className="space-y-4">
<div className="flex justify-between items-center">
<p className="text-sm text-gray-600"></p>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const newRule: CompetitionRule = {
id: `r${Date.now()}`,
name: "",
description: "",
weight: 0,
}
setNewCompetition({
...newCompetition,
rules: [...newCompetition.rules, newRule],
})
}}
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{newCompetition.rules.length > 0 && (
<div className="space-y-3">
{newCompetition.rules.map((rule, index) => (
<div key={rule.id} className="grid grid-cols-12 gap-3 items-end p-3 border rounded-lg">
<div className="col-span-4">
<Label className="text-xs"></Label>
<Input
value={rule.name || ""}
onChange={(e) => {
const updatedRules = [...newCompetition.rules]
updatedRules[index] = { ...rule, name: e.target.value }
setNewCompetition({ ...newCompetition, rules: updatedRules })
}}
placeholder="例如:創新性"
className="text-sm"
/>
</div>
<div className="col-span-5">
<Label className="text-xs"></Label>
<Input
value={rule.description || ""}
onChange={(e) => {
const updatedRules = [...newCompetition.rules]
updatedRules[index] = { ...rule, description: e.target.value }
setNewCompetition({ ...newCompetition, rules: updatedRules })
}}
placeholder="例如:技術創新程度和獨特性"
className="text-sm"
/>
</div>
<div className="col-span-2">
<Label className="text-xs"> (%)</Label>
<Input
type="number"
min="0"
max="100"
value={rule.weight || 0}
onChange={(e) => {
const updatedRules = [...newCompetition.rules]
updatedRules[index] = { ...rule, weight: Number.parseInt(e.target.value) || 0 }
setNewCompetition({ ...newCompetition, rules: updatedRules })
}}
className="text-sm"
/>
</div>
<div className="col-span-1">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const updatedRules = newCompetition.rules.filter((_, i) => i !== index)
setNewCompetition({ ...newCompetition, rules: updatedRules })
}}
className="text-red-600 border-red-300 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
))}
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<span className="text-sm text-blue-700">
{calculateTotalWeight(newCompetition.rules).toFixed(2)}%
</span>
{calculateTotalWeight(newCompetition.rules) !== 100 && (
<span className="text-sm text-orange-600"> 100%</span>
)}
</div>
</div>
)}
{newCompetition.rules.length === 0 && (
<div className="text-center py-8 text-gray-500">
<ClipboardList className="w-12 h-12 mx-auto mb-2 text-gray-400" />
<p></p>
<p className="text-sm"></p>
</div>
)}
</div>
</div>
<Separator />
{/* Award Types */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center">
<Award className="w-5 h-5 mr-2" />
</h3>
<div className="space-y-4">
<div className="flex justify-between items-center">
<p className="text-sm text-gray-600"></p>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const newAwardType: CompetitionAwardType = {
id: `at${Date.now()}`,
name: "",
description: "",
icon: "🏆",
color: "text-yellow-600",
}
setNewCompetition({
...newCompetition,
awardTypes: [...newCompetition.awardTypes, newAwardType],
})
}}
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{newCompetition.awardTypes.length > 0 && (
<div className="space-y-3">
{newCompetition.awardTypes.map((awardType, index) => (
<div key={awardType.id} className="grid grid-cols-12 gap-3 items-end p-3 border rounded-lg">
<div className="col-span-1">
<Label className="text-xs"></Label>
<Select
value={awardType.icon || "🏆"}
onValueChange={(value) => {
const updatedAwardTypes = [...newCompetition.awardTypes]
updatedAwardTypes[index] = { ...awardType, icon: value }
setNewCompetition({ ...newCompetition, awardTypes: updatedAwardTypes })
}}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="🏆">🏆</SelectItem>
<SelectItem value="🥇">🥇</SelectItem>
<SelectItem value="🥈">🥈</SelectItem>
<SelectItem value="🥉">🥉</SelectItem>
<SelectItem value="⭐"></SelectItem>
<SelectItem value="💡">💡</SelectItem>
<SelectItem value="⚙️"></SelectItem>
<SelectItem value="🎯">🎯</SelectItem>
<SelectItem value="❤️"></SelectItem>
<SelectItem value="👥">👥</SelectItem>
<SelectItem value="🧠">🧠</SelectItem>
<SelectItem value="🚀">🚀</SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-3">
<Label className="text-xs"></Label>
<Input
value={awardType.name || ""}
onChange={(e) => {
const updatedAwardTypes = [...newCompetition.awardTypes]
updatedAwardTypes[index] = { ...awardType, name: e.target.value }
setNewCompetition({ ...newCompetition, awardTypes: updatedAwardTypes })
}}
placeholder="例如:最佳創新獎"
className="text-sm"
/>
</div>
<div className="col-span-5">
<Label className="text-xs"></Label>
<Input
value={awardType.description || ""}
onChange={(e) => {
const updatedAwardTypes = [...newCompetition.awardTypes]
updatedAwardTypes[index] = { ...awardType, description: e.target.value }
setNewCompetition({ ...newCompetition, awardTypes: updatedAwardTypes })
}}
placeholder="例如:最具創新性的應用"
className="text-sm"
/>
</div>
<div className="col-span-2">
<Label className="text-xs"></Label>
<Select
value={awardType.color || "text-yellow-600"}
onValueChange={(value) => {
const updatedAwardTypes = [...newCompetition.awardTypes]
updatedAwardTypes[index] = { ...awardType, color: value }
setNewCompetition({ ...newCompetition, awardTypes: updatedAwardTypes })
}}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text-yellow-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-yellow-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-blue-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-green-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-green-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-red-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-red-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-purple-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-purple-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
<SelectItem value="text-orange-600">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-orange-600 rounded-full"></div>
<span></span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-1">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const updatedAwardTypes = newCompetition.awardTypes.filter((_, i) => i !== index)
setNewCompetition({ ...newCompetition, awardTypes: updatedAwardTypes })
}}
className="text-red-600 border-red-300 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
))}
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<span className="text-sm text-green-700">
{newCompetition.awardTypes.length}
</span>
<div className="flex space-x-1">
{newCompetition.awardTypes.slice(0, 5).map((awardType, index) => (
<span key={index} className="text-lg">
{awardType.icon}
</span>
))}
{newCompetition.awardTypes.length > 5 && (
<span className="text-sm text-gray-500">+{newCompetition.awardTypes.length - 5}</span>
)}
</div>
</div>
</div>
)}
{newCompetition.awardTypes.length === 0 && (
<div className="text-center py-8 text-gray-500">
<Award className="w-12 h-12 mx-auto mb-2 text-gray-400" />
<p></p>
<p className="text-sm"></p>
</div>
)}
</div>
</div>
</>
)}
<Separator />
{/* Participant Selection */}
<div className="space-y-4">
{newCompetition.type === "mixed" ? (
// Mixed competition - separate sections
<>
<h3 className="text-lg font-semibold flex items-center">
<Trophy className="w-5 h-5 mr-2" />
<span> *</span>
</h3>
{/* Individual Apps Section */}
<div className="space-y-4 border rounded-lg p-4 bg-blue-50">
<h4 className="font-semibold flex items-center">
<User className="w-4 h-4 mr-2" />
</h4>
<div className="flex gap-4 mb-4">
<div className="flex-1">
<Input
placeholder="搜尋應用名稱或創作者..."
value={individualParticipantSearchTerm}
onChange={(e) => setIndividualParticipantSearchTerm(e.target.value)}
className="max-w-sm"
/>
</div>
<Select value={individualDepartmentFilter} onValueChange={setIndividualDepartmentFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="部門篩選" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<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="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-64 overflow-y-auto border rounded-lg p-4 bg-white">
{getFilteredParticipants("individual").map((participant) => {
const isSelected = newCompetition.participatingApps.includes(participant.id)
return (
<div
key={participant.id}
className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-gray-50 transition-colors"
>
<Checkbox
id={`individual-${participant.id}`}
checked={isSelected}
onCheckedChange={(checked) => {
if (checked) {
setNewCompetition({
...newCompetition,
participatingApps: [...newCompetition.participatingApps, participant.id],
})
} else {
setNewCompetition({
...newCompetition,
participatingApps: newCompetition.participatingApps.filter(
(id: string) => id !== participant.id,
),
})
}
}}
/>
<div className="flex-1 space-y-2">
<div className="flex items-center justify-between">
<p className="font-medium text-sm">{participant.name}</p>
<Badge variant="outline" className="text-xs">
{participant.department}
</Badge>
</div>
<p className="text-xs text-gray-600 mt-1">{participant.creator}</p>
<p className="text-xs text-gray-500">{participant.submissionDate}</p>
</div>
</div>
)
})}
</div>
<div className="flex items-center justify-between p-3 bg-blue-100 rounded-lg">
<span className="text-sm text-blue-700">
{newCompetition.participatingApps.length}
</span>
</div>
</div>
{/* Team Section */}
<div className="space-y-4 border rounded-lg p-4 bg-green-50">
<h4 className="font-semibold flex items-center">
<Users className="w-4 h-4 mr-2" />
</h4>
<div className="flex gap-4 mb-4">
<div className="flex-1">
<Input
placeholder="搜尋團隊名稱或隊長..."
value={teamParticipantSearchTerm}
onChange={(e) => setTeamParticipantSearchTerm(e.target.value)}
className="max-w-sm"
/>
</div>
<Select value={teamDepartmentFilter} onValueChange={setTeamDepartmentFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="部門篩選" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<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="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-64 overflow-y-auto border rounded-lg p-4 bg-white">
{getFilteredParticipants("team").map((participant) => {
const isSelected = newCompetition.participatingTeams.includes(participant.id)
return (
<div
key={participant.id}
className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-gray-50 transition-colors"
>
<Checkbox
id={`team-${participant.id}`}
checked={isSelected}
onCheckedChange={(checked) => {
if (checked) {
setNewCompetition({
...newCompetition,
participatingTeams: [...newCompetition.participatingTeams, participant.id],
})
} else {
setNewCompetition({
...newCompetition,
participatingTeams: newCompetition.participatingTeams.filter(
(id: string) => id !== participant.id,
),
})
}
}}
/>
<div className="flex-1 space-y-2">
<div className="flex items-center justify-between">
<p className="font-medium text-sm">{participant.name}</p>
<Badge variant="outline" className="text-xs">
{participant.department}
</Badge>
</div>
<div className="flex items-center space-x-4 text-xs text-gray-600">
<div className="flex items-center space-x-1">
<User className="w-3 h-3" />
<span>{participant.leader}</span>
</div>
<div className="flex items-center space-x-1">
<Users className="w-3 h-3" />
<span>{participant.memberCount}</span>
</div>
</div>
{participant.contactEmail && (
<div className="flex items-center space-x-1 text-xs text-gray-600">
<Mail className="w-3 h-3" />
<span>{participant.contactEmail}</span>
</div>
)}
{participant.description && (
<p className="text-xs text-gray-500 line-clamp-2">{participant.description}</p>
)}
<p className="text-xs text-gray-500">{participant.submissionDate}</p>
</div>
</div>
)
})}
</div>
<div className="flex items-center justify-between p-3 bg-green-100 rounded-lg">
<span className="text-sm text-green-700">
{newCompetition.participatingTeams.length}
</span>
</div>
</div>
{/* Mixed Competition Summary */}
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg border border-purple-200">
<span className="text-sm text-purple-700 font-medium">
{newCompetition.participatingApps.length} +{" "}
{newCompetition.participatingTeams.length}
</span>
</div>
</>
) : (
// Single competition type - existing logic
<>
<h3 className="text-lg font-semibold flex items-center">
{getCompetitionTypeIcon(newCompetition.type)}
<span className="ml-2">
{newCompetition.type === "individual" && "參賽應用選擇 *"}
{newCompetition.type === "team" && "參賽團隊選擇 *"}
</span>
</h3>
{/* Search and Filter */}
<div className="flex gap-4 mb-4">
<div className="flex-1">
<Input
placeholder={`搜尋${newCompetition.type === "team" ? "團隊名稱或隊長" : "應用名稱或創作者"}...`}
value={participantSearchTerm}
onChange={(e) => setParticipantSearchTerm(e.target.value)}
className="max-w-sm"
/>
</div>
<Select value={departmentFilter} onValueChange={setDepartmentFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="部門篩選" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="HQBU">HQBU</SelectItem>
<SelectItem value="ITBU">ITBU</SelectItem>
<SelectItem value="MBU1">MBU1</SelectItem>
<SelectItem value="SBU">SBU</SelectItem>
</SelectContent>
</Select>
</div>
{/* Participant Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-96 overflow-y-auto border rounded-lg p-4">
{getFilteredParticipants(newCompetition.type).map((participant) => {
const participantField =
newCompetition.type === "individual" ? "participatingApps" : "participatingTeams"
const isSelected = newCompetition[participantField].includes(participant.id)
return (
<div
key={participant.id}
className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-gray-50 transition-colors"
>
<Checkbox
id={`participant-${participant.id}`}
checked={isSelected}
onCheckedChange={(checked) => {
if (checked) {
setNewCompetition({
...newCompetition,
[participantField]: [...newCompetition[participantField], participant.id],
})
} else {
setNewCompetition({
...newCompetition,
[participantField]: newCompetition[participantField].filter(
(id: string) => id !== participant.id,
),
})
}
}}
/>
<div className="flex-1 space-y-2">
<div className="flex items-center justify-between">
<p className="font-medium text-sm">{participant.name}</p>
<Badge variant="outline" className="text-xs">
{participant.department}
</Badge>
</div>
{/* Team specific information */}
{newCompetition.type === "team" && (
<div className="space-y-2">
<div className="flex items-center space-x-4 text-xs text-gray-600">
<div className="flex items-center space-x-1">
<User className="w-3 h-3" />
<span>{participant.leader}</span>
</div>
<div className="flex items-center space-x-1">
<Users className="w-3 h-3" />
<span>{participant.memberCount}</span>
</div>
</div>
{participant.contactEmail && (
<div className="flex items-center space-x-1 text-xs text-gray-600">
<Mail className="w-3 h-3" />
<span>{participant.contactEmail}</span>
</div>
)}
{participant.description && (
<p className="text-xs text-gray-500 line-clamp-2">{participant.description}</p>
)}
{participant.apps && participant.apps.length > 0 && (
<div className="space-y-1">
<p className="text-xs font-medium text-gray-700"></p>
<div className="flex flex-wrap gap-1">
{participant.apps.slice(0, 2).map((app: string, index: number) => (
<Badge key={index} variant="secondary" className="text-xs">
{app}
</Badge>
))}
{participant.apps.length > 2 && (
<Badge variant="secondary" className="text-xs">
+{participant.apps.length - 2}
</Badge>
)}
</div>
</div>
)}
{participant.members && participant.members.length > 0 && (
<div className="space-y-1">
<p className="text-xs font-medium text-gray-700"></p>
<div className="space-y-1">
{participant.members.slice(0, 3).map((member: any) => (
<div key={member.id} className="flex items-center justify-between text-xs">
<span className="text-gray-600">{member.name}</span>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="text-xs px-1 py-0">
{member.role}
</Badge>
<span className="text-gray-500">{member.department}</span>
</div>
</div>
))}
{participant.members.length > 3 && (
<p className="text-xs text-gray-500">
{participant.members.length - 3} ...
</p>
)}
</div>
</div>
)}
</div>
)}
{/* Individual information */}
{newCompetition.type === "individual" && (
<p className="text-xs text-gray-600 mt-1">{participant.creator}</p>
)}
<p className="text-xs text-gray-500">{participant.submissionDate}</p>
</div>
</div>
)
})}
</div>
{/* Selection Summary */}
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<span className="text-sm text-blue-700">
{" "}
{newCompetition.type === "individual"
? newCompetition.participatingApps.length
: newCompetition.participatingTeams.length}{" "}
{newCompetition.type === "team" ? "團隊" : "應用"}
</span>
</div>
</>
)}
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowCreateCompetition(false)}>
</Button>
<Button onClick={handleCreateCompetition} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{selectedCompetitionForAction ? '更新中...' : '創建中...'}
</>
) : (
selectedCompetitionForAction ? "更新競賽" : "創建競賽"
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* 查看競賽詳情對話框 */}
<Dialog open={showCompetitionDetail} onOpenChange={setShowCompetitionDetail}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedCompetitionForAction && (
<div className="space-y-6">
{/* 競賽基本資訊卡片 */}
<div className="flex items-start space-x-4 p-4 bg-gradient-to-r from-purple-50 to-pink-50 rounded-lg border">
<div className="w-16 h-16 bg-gradient-to-r from-purple-600 to-pink-600 rounded-xl flex items-center justify-center flex-shrink-0">
<Trophy className="w-8 h-8 text-white" />
</div>
<div className="flex-1 space-y-3">
<div>
<h3 className="text-xl font-bold text-gray-900">{selectedCompetitionForAction.name}</h3>
<p className="text-gray-600 mt-1">{selectedCompetitionForAction.description || '展示最具創新性的 AI 應用,推動企業數位轉型'}</p>
</div>
<div className="flex items-center space-x-2">
<Badge variant={selectedCompetitionForAction.status === 'completed' ? 'default' : selectedCompetitionForAction.status === 'ongoing' ? 'secondary' : 'outline'}>
{getStatusText(selectedCompetitionForAction.status)}
</Badge>
<Badge variant="outline">
{getCompetitionTypeText(selectedCompetitionForAction.type)}
</Badge>
<Badge variant="outline">
{getParticipantCount(selectedCompetitionForAction)}
</Badge>
</div>
</div>
</div>
{/* 詳細資訊 */}
<div className="grid grid-cols-2 gap-6">
<div className="space-y-4">
<div>
<Label className="text-sm font-medium text-gray-500">ID</Label>
<p className="text-lg font-medium text-gray-900 mt-1 font-mono text-sm">{selectedCompetitionForAction.id}</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-500"></Label>
<p className="text-lg font-medium text-gray-900 mt-1">
{formatDateRange(
selectedCompetitionForAction.start_date || selectedCompetitionForAction.startDate,
selectedCompetitionForAction.end_date || selectedCompetitionForAction.endDate
).split(' ~ ')[0]}
</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-500"></Label>
<p className="text-lg font-medium text-gray-900 mt-1">
{selectedCompetitionForAction.evaluation_focus || '未設定'}
</p>
</div>
</div>
<div className="space-y-4">
<div>
<Label className="text-sm font-medium text-gray-500"></Label>
<p className="text-lg font-medium text-gray-900 mt-1">{selectedCompetitionForAction.year}{selectedCompetitionForAction.month}</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-500"></Label>
<p className="text-lg font-medium text-gray-900 mt-1">
{formatDateRange(
selectedCompetitionForAction.start_date || selectedCompetitionForAction.startDate,
selectedCompetitionForAction.end_date || selectedCompetitionForAction.endDate
).split(' ~ ')[1]}
</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-500"></Label>
<p className="text-lg font-medium text-gray-900 mt-1">
{selectedCompetitionForAction.max_team_size ? `${selectedCompetitionForAction.max_team_size}` : '無限制'}
</p>
</div>
</div>
</div>
{/* 評審團隊 */}
<div className="space-y-4">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{(selectedCompetitionForAction.judges && selectedCompetitionForAction.judges.length > 0) ?
selectedCompetitionForAction.judges.map((judge: any) => (
<div key={judge.id} className="flex items-center space-x-3 p-4 border rounded-lg bg-white">
<Avatar className="w-12 h-12">
<AvatarFallback className="bg-gradient-to-r from-blue-600 to-purple-600 text-white">
{judge.name.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium text-gray-900">{judge.name}</p>
<p className="text-sm text-gray-600">{judge.title || '評審'}</p>
<p className="text-xs text-gray-500">{judge.department || ''}</p>
{judge.expertise && Array.isArray(judge.expertise) && (
<p className="text-xs text-gray-500">{judge.expertise.join(', ')}</p>
)}
</div>
</div>
)) : (
<div className="col-span-full text-center py-8 text-gray-500">
<UserCheck className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p></p>
</div>
)
}
</div>
</div>
{/* 參賽團隊 */}
<div className="space-y-4">
<h4 className="text-lg font-semibold text-gray-900">
({getParticipantCount(selectedCompetitionForAction)})
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{(selectedCompetitionForAction.teams && selectedCompetitionForAction.teams.length > 0) ?
selectedCompetitionForAction.teams.map((team: any) => (
<div key={team.id} className="p-4 border rounded-lg bg-white space-y-2">
<h5 className="font-medium text-gray-900">{team.name}</h5>
<p className="text-sm text-gray-600">{team.leader_name || team.leader_id || '未知'}</p>
<p className="text-sm text-gray-600">{team.department || '未知'}</p>
<p className="text-sm text-gray-600">{team.contact_email || '未知'}</p>
{team.leader_phone && (
<p className="text-sm text-gray-600">{team.leader_phone}</p>
)}
{team.description && (
<p className="text-sm text-gray-500">{team.description}</p>
)}
{team.registered_at && (
<p className="text-xs text-gray-400">
{new Date(team.registered_at).toLocaleDateString('zh-TW')}
</p>
)}
</div>
)) : (
<div className="col-span-full text-center py-8 text-gray-500">
<Users className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p></p>
</div>
)
}
</div>
</div>
{/* 獎項類型 */}
<div className="space-y-4">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{(selectedCompetitionForAction.awardTypes && selectedCompetitionForAction.awardTypes.length > 0) ?
selectedCompetitionForAction.awardTypes.map((awardType: any) => (
<div key={awardType.id} className="p-4 border rounded-lg bg-white space-y-2">
<div className="flex items-center space-x-2">
<span className="text-2xl">{awardType.icon || '🏆'}</span>
<h5 className="font-medium text-gray-900">{awardType.name}</h5>
</div>
<p className="text-sm text-gray-600">{awardType.description || '無描述'}</p>
<div className="flex items-center space-x-2">
<Badge variant="outline" className={awardType.color || 'text-yellow-600'}>
{awardType.color || 'text-yellow-600'}
</Badge>
<span className="text-xs text-gray-500">{awardType.order_index || 0}</span>
</div>
</div>
)) : (
<div className="col-span-full text-center py-8 text-gray-500">
<Award className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p></p>
</div>
)
}
</div>
</div>
{/* 評分規則 */}
<div className="space-y-4">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<div className="space-y-3">
{(selectedCompetitionForAction.rules && selectedCompetitionForAction.rules.length > 0) ?
selectedCompetitionForAction.rules.map((rule: any) => (
<div key={rule.id} className="p-4 border rounded-lg bg-white">
<div className="flex items-center justify-between">
<div>
<h5 className="font-medium text-gray-900">{rule.name}</h5>
<p className="text-sm text-gray-600">{rule.description || '無描述'}</p>
</div>
<div className="text-right">
<Badge variant="outline" className="text-lg font-bold">
{Math.round(calculateWeightPercentage(rule.weight))}%
</Badge>
<p className="text-xs text-gray-500 mt-1"></p>
</div>
</div>
<div className="mt-2">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{
width: `${Math.min(calculateWeightPercentage(rule.weight), 100)}%`
}}
></div>
</div>
</div>
</div>
)) : (
<div className="text-center py-8 text-gray-500">
<ClipboardList className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p></p>
</div>
)
}
</div>
</div>
<div className="flex justify-end">
<Button onClick={() => setShowCompetitionDetail(false)}></Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
{/* 刪除競賽確認對話框 */}
<Dialog open={showDeleteCompetitionConfirm} onOpenChange={setShowDeleteCompetitionConfirm}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedCompetitionForAction && (
<div className="space-y-4">
<p className="text-sm text-gray-600">
{selectedCompetitionForAction.name}
</p>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowDeleteCompetitionConfirm(false)}>
</Button>
<Button
variant="destructive"
onClick={confirmDeleteCompetition}
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
"確認刪除"
)}
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
{/* 修改狀態對話框 */}
<Dialog open={showChangeStatusDialog} onOpenChange={setShowChangeStatusDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedCompetitionForAction && (
<div className="space-y-4">
<div className="space-y-2">
<Label></Label>
<p className="text-sm text-gray-600">
<Badge>{getStatusText(selectedCompetitionForAction.status)}</Badge>
</p>
</div>
<div className="space-y-2">
<Label htmlFor="status"></Label>
<Select value={newStatus} onValueChange={setNewStatus}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="upcoming"></SelectItem>
<SelectItem value="ongoing"></SelectItem>
<SelectItem value="completed"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowChangeStatusDialog(false)}>
</Button>
<Button onClick={handleUpdateStatus} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
"更新狀態"
)}
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
{/* 手動評分對話框 */}
<Dialog open={showManualScoring} onOpenChange={setShowManualScoring}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* 混合賽類型選擇 */}
{selectedCompetition?.type === 'mixed' && (
<div className="space-y-3">
<Label className="text-base font-medium"></Label>
<div className="flex space-x-2">
<Button
type="button"
variant={selectedParticipantType === 'individual' ? 'default' : 'outline'}
onClick={() => handleParticipantTypeChange('individual')}
className="flex-1"
>
<User className="w-4 h-4 mr-2" />
</Button>
<Button
type="button"
variant={selectedParticipantType === 'team' ? 'default' : 'outline'}
onClick={() => handleParticipantTypeChange('team')}
className="flex-1"
>
<Users className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
)}
{/* 選擇評審和參賽者 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Select
value={manualScoring.judgeId}
onValueChange={(value) => setManualScoring({...manualScoring, judgeId: value})}
>
<SelectTrigger>
<SelectValue placeholder="選擇評審" />
</SelectTrigger>
<SelectContent>
{/* 混合賽時根據參賽者類型過濾評審 */}
{selectedCompetition?.type === 'mixed' ? (
(dbJudges.length > 0 ? dbJudges : judges).filter(judge => {
const config = selectedParticipantType === 'individual'
? selectedCompetition.individualConfig
: selectedCompetition.teamConfig;
return config?.judges?.includes(judge.id) || false;
}).map((judge) => (
<SelectItem key={judge.id} value={judge.id}>
{judge.name} - {judge.expertise}
</SelectItem>
))
) : (
(dbJudges.length > 0 ? dbJudges : judges).filter(judge => selectedCompetition?.judges?.includes(judge.id) || false).map((judge) => (
<SelectItem key={judge.id} value={judge.id}>
{judge.name} - {judge.expertise}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>
{selectedCompetition?.type === 'mixed'
? (selectedParticipantType === 'individual' ? '選擇個人' : '選擇團隊')
: (selectedCompetition?.type === 'team' ? '選擇團隊' : '選擇個人')
}
</Label>
<Select
value={manualScoring.participantId}
onValueChange={(value) => setManualScoring({...manualScoring, participantId: value})}
>
<SelectTrigger>
<SelectValue placeholder={
selectedCompetition?.type === 'mixed'
? (selectedParticipantType === 'individual' ? '選擇個人' : '選擇團隊')
: (selectedCompetition?.type === 'team' ? '選擇團隊' : '選擇個人')
} />
</SelectTrigger>
<SelectContent>
{/* 根據競賽類型和選擇的參賽者類型顯示參賽者 */}
{(selectedCompetition?.type === 'individual' ||
(selectedCompetition?.type === 'mixed' && selectedParticipantType === 'individual')) &&
mockIndividualApps
.filter(app => selectedCompetition.participatingApps?.includes(app.id))
.map((app) => (
<SelectItem key={app.id} value={app.id}>
{app.name} - {app.creator}
</SelectItem>
))
}
{(selectedCompetition?.type === 'team' ||
(selectedCompetition?.type === 'mixed' && selectedParticipantType === 'team')) &&
teams
.filter(team => selectedCompetition.participatingTeams?.includes(team.id))
.map((team) => (
<SelectItem key={team.id} value={team.id}>
{team.name} - {team.leader}
</SelectItem>
))
}
</SelectContent>
</Select>
</div>
</div>
{/* 評分項目 */}
<div className="space-y-6">
<div className="flex items-center justify-between">
<Label className="text-lg font-medium"></Label>
{selectedCompetition?.type === 'mixed' && (
<Badge variant="outline" className="text-sm">
{selectedParticipantType === 'individual' ? '個人賽評分' : '團體賽評分'}
</Badge>
)}
</div>
{/* 動態顯示競賽的評分項目 */}
{(() => {
let currentRules: any[] = [];
if (selectedCompetition?.type === 'mixed') {
// 混合賽:根據當前選擇的參賽者類型獲取評分規則
const config = selectedParticipantType === 'individual'
? selectedCompetition.individualConfig
: selectedCompetition.teamConfig;
currentRules = config?.rules || [];
} else {
// 單一類型競賽
currentRules = selectedCompetition?.rules || [];
}
// 如果有自定義規則,使用自定義規則;否則使用預設規則
const scoringItems = currentRules.length > 0
? currentRules
: getDefaultScoringItems(
selectedCompetition?.type === 'mixed'
? selectedParticipantType
: selectedCompetition?.type || 'individual'
);
return scoringItems.map((item: any, index: number) => (
<div key={index} className="space-y-3">
<div className="flex justify-between items-center">
<div>
<Label className="text-base font-medium">{item.name}</Label>
<p className="text-sm text-gray-600 mt-1">{item.description}</p>
{item.weight && (
<p className="text-xs text-purple-600 mt-1">{item.weight}%</p>
)}
</div>
<div className="text-right">
<span className="text-lg font-bold">
{manualScoring.scores[item.name] || 0} / 10
</span>
</div>
</div>
{/* 評分按鈕 */}
<div className="flex flex-wrap gap-2">
{Array.from({ length: 10 }, (_, i) => i + 1).map((score) => (
<button
key={score}
type="button"
onClick={() => setManualScoring({
...manualScoring,
scores: { ...manualScoring.scores, [item.name]: score }
})}
className={`w-10 h-10 rounded-lg border-2 font-medium transition-all ${
(manualScoring.scores[item.name] || 0) === score
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:border-blue-400 hover:bg-blue-50'
}`}
>
{score}
</button>
))}
</div>
</div>
));
})()}
</div>
{/* 評審意見 */}
<div className="space-y-2">
<Label className="text-base font-medium"> *</Label>
<Textarea
value={manualScoring.comments}
onChange={(e) => setManualScoring({...manualScoring, comments: e.target.value})}
placeholder="請提供評審意見和建議..."
rows={4}
className="resize-none"
/>
</div>
{/* 按鈕區 */}
<div className="flex justify-end space-x-3 pt-4 border-t">
<Button variant="outline" onClick={() => setShowManualScoring(false)}>
</Button>
<Button
onClick={handleSubmitManualScore}
disabled={isLoading}
className="bg-gray-900 hover:bg-gray-800 text-white"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
"提交評分"
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* 創建團隊對話框 */}
<Dialog
open={showCreateTeam}
onOpenChange={(open) => {
setShowCreateTeam(open)
if (!open) {
setCreateError("")
setSelectedTeam(null) // 清除編輯狀態
resetTeamForm()
}
}}
>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{selectedTeam ? '編輯團隊' : '創建新團隊'}</DialogTitle>
<DialogDescription>
{selectedTeam ? '修改團隊的基本資訊、成員和應用' : '建立一個新的競賽團隊,包含完整的團隊資訊'}
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="basic"></TabsTrigger>
<TabsTrigger value="members"></TabsTrigger>
<TabsTrigger value="apps"></TabsTrigger>
</TabsList>
{/* 基本資訊標籤頁 */}
<TabsContent value="basic" className="space-y-6 mt-6">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="team-name"> *</Label>
<Input
id="team-name"
value={newTeam.name}
onChange={(e) => setNewTeam({ ...newTeam, name: e.target.value })}
placeholder="輸入團隊名稱"
/>
</div>
<div className="space-y-2">
<Label htmlFor="leader-name"> *</Label>
<Select
value={newTeam.leader_id}
onValueChange={(value) => {
const selectedUser = availableUsers.find(u => u.id === value);
setNewTeam({
...newTeam,
leader_id: value,
leader: selectedUser?.name || ''
});
}}
>
<SelectTrigger>
<SelectValue placeholder="選擇團隊隊長" />
</SelectTrigger>
<SelectContent>
{availableUsers.length > 0 ? (
availableUsers.map((user) => (
<SelectItem key={user.id} value={user.id}>
<div className="flex flex-col">
<span className="font-medium">{user.name}</span>
<span className="text-xs text-gray-500">{user.email} {user.department}</span>
</div>
</SelectItem>
))
) : (
<SelectItem value="loading" disabled>
<span className="text-gray-500">...</span>
</SelectItem>
)}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="department"></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 className="space-y-2">
<Label htmlFor="leader-phone"></Label>
<Input
id="leader-phone"
value={newTeam.leaderPhone}
onChange={(e) => setNewTeam({ ...newTeam, leaderPhone: e.target.value })}
placeholder="0912-345-678"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="contact-email"> *</Label>
<Input
id="contact-email"
type="email"
value={newTeam.contactEmail}
onChange={(e) => setNewTeam({ ...newTeam, contactEmail: e.target.value })}
placeholder="team@company.com"
/>
</div>
<div className="space-y-2">
<Label htmlFor="team-description"></Label>
<Textarea
id="team-description"
value={newTeam.description}
onChange={(e) => setNewTeam({ ...newTeam, description: e.target.value })}
placeholder="描述團隊的專長、目標或特色..."
rows={4}
className="resize-none"
/>
</div>
</TabsContent>
{/* 團隊成員標籤頁 */}
<TabsContent value="members" className="space-y-6 mt-6">
<div className="space-y-4">
<Label className="text-lg font-medium"></Label>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label></Label>
<Select
value={newMember.user_id}
onValueChange={(value) => {
const selectedUser = availableUsers.find(u => u.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}>
<div className="flex flex-col">
<span className="font-medium">{user.name}</span>
<span className="text-xs text-gray-500">{user.email} {user.department}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></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>/</Label>
<Select
value={newMember.role}
onValueChange={(value) => setNewMember({ ...newMember, role: value })}
>
<SelectTrigger>
<SelectValue placeholder="選擇角色" />
</SelectTrigger>
<SelectContent>
<SelectItem value="隊長"></SelectItem>
<SelectItem value="成員"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Button
type="button"
onClick={handleAddMember}
variant="outline"
className="w-full border-2 border-dashed border-gray-300 hover:border-blue-400 hover:bg-blue-50"
>
<UserPlus className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 成員列表 */}
{newTeam.members.length > 0 && (
<div className="space-y-4">
<Label className="text-lg font-medium"></Label>
<div className="space-y-3">
{newTeam.members.map((member) => (
<div key={member.id} className="flex items-center justify-between p-3 border rounded-lg bg-gray-50">
<div className="flex items-center space-x-4">
<Avatar className="w-10 h-10">
<AvatarFallback className="bg-gradient-to-r from-blue-600 to-purple-600 text-white">
{member.name.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{member.name}</p>
<div className="flex items-center space-x-2 text-sm text-gray-600">
<span>{member.department}</span>
<span></span>
<span>{member.role}</span>
</div>
</div>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleRemoveMember(member.id)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
))}
</div>
</div>
)}
</TabsContent>
{/* 提交應用標籤頁 */}
<TabsContent value="apps" className="space-y-6 mt-6">
<div className="space-y-4">
<Label className="text-lg font-medium"></Label>
<div className="space-y-2">
<Label></Label>
<Select onValueChange={handleAddApp}>
<SelectTrigger>
<SelectValue placeholder="選擇要加入團隊的應用" />
</SelectTrigger>
<SelectContent>
{availableApps.length > 0 ? (
availableApps.map((app) => (
<SelectItem key={app.id} value={app.id}>
<div className="flex flex-col">
<span className="font-medium">{app.name}</span>
<span className="text-xs text-gray-500">{app.category} {app.type}</span>
</div>
</SelectItem>
))
) : (
<SelectItem value="loading" disabled>
<span className="text-gray-500">...</span>
</SelectItem>
)}
</SelectContent>
</Select>
</div>
</div>
{/* 已加入的應用列表 */}
{newTeam.apps.length > 0 && (
<div className="space-y-4">
<Label className="text-lg font-medium"></Label>
<div className="space-y-3">
{newTeam.apps.map((appId) => {
const app = availableApps.find(a => a.id === appId);
if (!app) return null;
return (
<div key={appId} className="flex items-center justify-between p-3 border rounded-lg bg-gray-50">
<div className="flex-1">
<p className="font-medium">{app.name}</p>
<p className="text-sm text-gray-600">{app.description}</p>
<div className="flex items-center space-x-2 mt-1">
<Badge variant="outline" className="text-xs">{app.category}</Badge>
<Badge variant="outline" className="text-xs">{app.type}</Badge>
</div>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleRemoveApp(appId)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
);
})}
</div>
</div>
)}
</TabsContent>
</Tabs>
{/* 錯誤訊息 */}
{createError && (
<Alert variant="destructive" className="mt-4">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{createError}</AlertDescription>
</Alert>
)}
{/* 底部按鈕 */}
<div className="flex justify-end space-x-3 pt-4 border-t mt-6">
<Button variant="outline" onClick={() => setShowCreateTeam(false)}>
</Button>
<Button
onClick={handleCreateTeam}
disabled={isLoading}
className="bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{selectedTeam ? '更新中...' : '創建中...'}
</>
) : (
selectedTeam ? "更新團隊" : "創建團隊"
)}
</Button>
</div>
</DialogContent>
</Dialog>
{/* 查看團隊詳情對話框 */}
<Dialog open={showTeamDetail} onOpenChange={setShowTeamDetail}>
<DialogContent className="max-w-4xl 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 p-4 bg-gradient-to-r from-green-50 to-blue-50 rounded-lg border">
<div className="w-16 h-16 bg-gradient-to-r from-green-600 to-blue-600 rounded-xl flex items-center justify-center flex-shrink-0">
<Users className="w-8 h-8 text-white" />
</div>
<div className="flex-1 space-y-3">
<div>
<h3 className="text-xl font-bold text-gray-900">{selectedTeam.name}</h3>
<p className="text-gray-600 mt-1">{selectedTeam.description || '專注於AI技術創新與應用的團隊'}</p>
</div>
<div className="flex items-center space-x-2">
<Badge variant="outline">
{selectedTeam.department}
</Badge>
<Badge variant="outline">
{selectedTeam.members?.length || 0}
</Badge>
<Badge variant="outline">
{selectedTeam.apps?.length || 0}
</Badge>
</div>
</div>
</div>
{/* 詳細資訊 */}
<div className="grid grid-cols-2 gap-6">
<div className="space-y-4">
<div>
<Label className="text-sm font-medium text-gray-500">ID</Label>
<p className="text-lg font-medium text-gray-900 mt-1">{selectedTeam.id}</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-500"></Label>
<p className="text-lg font-medium text-gray-900 mt-1">{selectedTeam.contact_email || '未提供'}</p>
</div>
</div>
<div className="space-y-4">
<div>
<Label className="text-sm font-medium text-gray-500"></Label>
<p className="text-lg font-medium text-gray-900 mt-1">{selectedTeam.leader_name || '未指定'}</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-500"></Label>
<p className="text-lg font-medium text-gray-900 mt-1">{selectedTeam.leader_phone || '未提供'}</p>
</div>
</div>
</div>
{/* 團隊成員 */}
<div className="space-y-4">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{selectedTeam.members?.map((member: any) => (
<div key={member.id} className="flex items-center space-x-3 p-3 border rounded-lg bg-white">
<Avatar className="w-12 h-12">
<AvatarFallback className="bg-gradient-to-r from-green-600 to-blue-600 text-white">
{member.name.charAt(0)}
</AvatarFallback>
</Avatar>
<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 && (
<Badge variant="default" className="text-xs bg-orange-100 text-orange-800">
</Badge>
)}
</div>
<p className="text-sm text-gray-600">{member.department} {member.role}</p>
</div>
</div>
))}
</div>
</div>
{/* 團隊應用 */}
{selectedTeam.apps && selectedTeam.apps.length > 0 && (
<div className="space-y-4">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<div className="space-y-3">
{selectedTeam.apps.map((app: any, index: number) => (
<div key={app.id || index} className="flex items-center justify-between p-4 border rounded-lg bg-white">
<div className="flex-1">
<h5 className="font-medium text-gray-900">{app.name || app.title}</h5>
<p className="text-sm text-gray-600 mt-1">{app.description || '無描述'}</p>
{app.url && (
<div className="flex items-center space-x-1 mt-2">
<Link className="w-3 h-3 text-blue-600" />
<a
href={app.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-800"
>
{app.url}
</a>
</div>
)}
</div>
</div>
))}
</div>
</div>
)}
<div className="flex justify-end">
<Button onClick={() => setShowTeamDetail(false)}></Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
{/* 刪除團隊確認對話框 */}
<Dialog open={showDeleteTeamConfirm} onOpenChange={setShowDeleteTeamConfirm}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2 text-red-600">
<AlertTriangle className="w-5 h-5" />
<span></span>
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{teamToDelete && (
<div className="space-y-4">
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start space-x-3">
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center flex-shrink-0">
<Users className="w-5 h-5 text-red-600" />
</div>
<div className="flex-1">
<h4 className="font-medium text-red-900">{teamToDelete.name}</h4>
<p className="text-sm text-red-700 mt-1">
{teamToDelete.leader} {teamToDelete.memberCount}
</p>
<p className="text-sm text-red-700">
{teamToDelete.submittedAppCount}
</p>
</div>
</div>
</div>
<div className="text-sm text-gray-600 space-y-1">
<p> </p>
<ul className="list-disc list-inside space-y-1 ml-4">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
)}
<div className="flex justify-end space-x-3 pt-4">
<Button
variant="outline"
onClick={() => {
setShowDeleteTeamConfirm(false)
setTeamToDelete(null)
}}
disabled={isLoading}
>
</Button>
<Button
variant="destructive"
onClick={handleConfirmDeleteTeam}
disabled={isLoading}
className="bg-red-600 hover:bg-red-700"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Trash2 className="w-4 h-4 mr-2" />
</>
)}
</Button>
</div>
</DialogContent>
</Dialog>
{/* 新增評審對話框 */}
<Dialog open={showAddJudge} onOpenChange={setShowAddJudge}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{selectedJudge ? '編輯評審' : '新增評審'}</DialogTitle>
<DialogDescription>
{selectedJudge ? '修改評審的基本資訊和專業領域' : '新增專業評審到評審團'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 姓名 */}
<div className="space-y-2">
<Label htmlFor="judge-name">
<span className="text-red-500">*</span>
</Label>
<Input
id="judge-name"
value={newJudge.name}
onChange={(e) => setNewJudge({ ...newJudge, name: e.target.value })}
placeholder="輸入評審姓名"
/>
</div>
{/* 職稱 */}
<div className="space-y-2">
<Label htmlFor="judge-title">
<span className="text-red-500">*</span>
</Label>
<Input
id="judge-title"
value={newJudge.title}
onChange={(e) => setNewJudge({ ...newJudge, title: e.target.value })}
placeholder="輸入職稱"
/>
</div>
{/* 部門 */}
<div className="space-y-2">
<Label htmlFor="judge-department">
<span className="text-red-500">*</span>
</Label>
<Select
value={newJudge.department === "" || !["HQBU", "ITBU", "MBU1", "MBU2", "SBU", "研發部", "產品部", "技術部"].includes(newJudge.department) ? "custom" : newJudge.department}
onValueChange={(value) => {
if (value === "custom") {
setNewJudge({ ...newJudge, department: "" })
} else {
setNewJudge({ ...newJudge, department: value })
}
}}
>
<SelectTrigger>
<SelectValue placeholder="選擇部門" />
</SelectTrigger>
<SelectContent>
<SelectItem value="HQBU">HQBU</SelectItem>
<SelectItem value="ITBU">ITBU</SelectItem>
<SelectItem value="MBU1">MBU1</SelectItem>
<SelectItem value="MBU2">MBU2</SelectItem>
<SelectItem value="SBU">SBU</SelectItem>
<SelectItem value="研發部"></SelectItem>
<SelectItem value="產品部"></SelectItem>
<SelectItem value="技術部"></SelectItem>
<SelectItem value="custom">/...</SelectItem>
</SelectContent>
</Select>
{/* 自定義部門輸入框 */}
{(newJudge.department === "" || !["HQBU", "ITBU", "MBU1", "MBU2", "SBU", "研發部", "產品部", "技術部"].includes(newJudge.department)) && (
<Input
value={newJudge.department}
onChange={(e) => setNewJudge({ ...newJudge, department: e.target.value })}
placeholder="請輸入部門/機構名稱例如外部顧問、XX顧問公司"
className="mt-2"
/>
)}
</div>
{/* 專業領域 */}
<div className="space-y-3">
<Label htmlFor="judge-expertise"></Label>
<Input
id="judge-expertise"
value={newJudge.expertise}
onChange={(e) => setNewJudge({ ...newJudge, expertise: e.target.value })}
placeholder="用逗號分隔,例如:機器學習, 深度學習"
/>
{/* 快速選擇標籤 */}
<div className="space-y-2">
<p className="text-sm text-gray-600"></p>
<div className="flex flex-wrap gap-2">
{[
"機器學習",
"深度學習",
"自然語言處理",
"計算機視覺",
"數據科學",
"人工智能",
"雲端計算",
"網路安全",
"軟體工程",
"用戶體驗"
].map((tag) => (
<Button
key={tag}
type="button"
variant="outline"
size="sm"
className="h-7 px-3 text-xs border-gray-300 hover:border-blue-400 hover:bg-blue-50"
onClick={() => {
const currentExpertise = newJudge.expertise.trim()
if (currentExpertise === "") {
setNewJudge({ ...newJudge, expertise: tag })
} else {
// 檢查是否已經包含這個標籤
const expertiseList = currentExpertise.split(',').map(item => item.trim())
if (!expertiseList.includes(tag)) {
setNewJudge({ ...newJudge, expertise: currentExpertise + ", " + tag })
}
}
}}
>
+ {tag}
</Button>
))}
</div>
</div>
</div>
{/* 錯誤訊息 */}
{error && (
<Alert className="border-red-200 bg-red-50">
<AlertTriangle className="h-4 w-4 text-red-600" />
<AlertDescription className="text-red-600">{error}</AlertDescription>
</Alert>
)}
</div>
<div className="flex justify-end space-x-3 pt-4">
<Button
variant="outline"
onClick={() => {
setShowAddJudge(false)
setSelectedJudge(null)
setNewJudge({
name: "",
title: "",
department: "",
expertise: "",
})
setError("")
}}
disabled={isLoading}
>
</Button>
<Button
onClick={handleAddJudge}
disabled={isLoading}
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{selectedJudge ? '更新中...' : '新增中...'}
</>
) : (
<>
<UserPlus className="w-4 h-4 mr-2" />
{selectedJudge ? '更新評審' : '新增評審'}
</>
)}
</Button>
</div>
</DialogContent>
</Dialog>
{/* 查看評審詳情對話框 */}
<Dialog open={showJudgeDetail} onOpenChange={setShowJudgeDetail}>
<DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedJudge && (
<div className="space-y-6">
{/* 評審基本資訊卡片 */}
<div className="flex items-start space-x-4 p-4 bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg border">
<Avatar className="w-16 h-16">
<AvatarFallback className="bg-gradient-to-r from-purple-600 to-blue-600 text-white text-lg font-bold">
{selectedJudge.name.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex-1 space-y-2">
<div>
<h3 className="text-xl font-bold text-gray-900">{selectedJudge.name}</h3>
<p className="text-gray-600 font-medium">{selectedJudge.title}</p>
</div>
<div className="flex items-center space-x-4 text-sm text-gray-600">
<span>{selectedJudge.department}</span>
<Badge variant="outline" className="text-xs">
ID: {selectedJudge.id}
</Badge>
</div>
</div>
</div>
{/* 專業領域 */}
<div className="space-y-3">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<div className="flex flex-wrap gap-2">
{selectedJudge.expertise && selectedJudge.expertise.length > 0 ? (
selectedJudge.expertise.map((field, index) => (
<Badge
key={index}
variant="secondary"
className="bg-purple-100 text-purple-800 hover:bg-purple-200"
>
{field}
</Badge>
))
) : (
<p className="text-gray-500 text-sm"></p>
)}
</div>
</div>
{/* 統計資訊 */}
<div className="grid grid-cols-2 gap-4 pt-4 border-t">
<div className="text-center">
<div className="text-sm text-gray-500">ID</div>
<div className="text-lg font-bold text-gray-900">{selectedJudge.id}</div>
</div>
<div className="text-center">
<div className="text-sm text-gray-500"></div>
<div className="text-lg font-bold text-gray-900">
{selectedJudge.expertise ? selectedJudge.expertise.length : 0}
</div>
</div>
</div>
{/* 底部按鈕 */}
<div className="flex justify-end space-x-3 pt-4">
<Button
variant="outline"
onClick={() => setShowJudgeDetail(false)}
>
</Button>
<Button
onClick={() => {
setShowJudgeDetail(false)
handleEditJudge(selectedJudge)
}}
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700"
>
<Edit className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
{/* 停用評審確認對話框 */}
<Dialog open={showDisableJudgeConfirm} onOpenChange={setShowDisableJudgeConfirm}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2 text-orange-600">
<UserX className="w-5 h-5" />
<span></span>
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{selectedJudge && (
<div className="space-y-4">
<div className="p-4 bg-orange-50 border border-orange-200 rounded-lg">
<div className="flex items-start space-x-3">
<Avatar className="w-12 h-12">
<AvatarFallback className="bg-orange-100 text-orange-600">
{selectedJudge.name.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<h4 className="font-medium text-orange-900">{selectedJudge.name}</h4>
<p className="text-sm text-orange-700 mt-1">
{selectedJudge.title}
</p>
<p className="text-sm text-orange-700">
{selectedJudge.department}
</p>
</div>
</div>
</div>
<div className="text-sm text-gray-600 space-y-1">
<p> </p>
<ul className="list-disc list-inside space-y-1 ml-4">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
)}
<div className="flex justify-end space-x-3 pt-4">
<Button
variant="outline"
onClick={() => {
setShowDisableJudgeConfirm(false)
setSelectedJudge(null)
}}
disabled={isLoading}
>
</Button>
<Button
variant="destructive"
onClick={confirmDisableJudge}
disabled={isLoading}
className="bg-orange-600 hover:bg-orange-700"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<UserX className="w-4 h-4 mr-2" />
</>
)}
</Button>
</div>
</DialogContent>
</Dialog>
{/* 刪除評審確認對話框 */}
<Dialog open={showDeleteJudgeConfirm} onOpenChange={setShowDeleteJudgeConfirm}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2 text-red-600">
<AlertTriangle className="w-5 h-5" />
<span></span>
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{selectedJudge && (
<div className="space-y-4">
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start space-x-3">
<Avatar className="w-12 h-12">
<AvatarFallback className="bg-red-100 text-red-600">
{selectedJudge.name.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<h4 className="font-medium text-red-900">{selectedJudge.name}</h4>
<p className="text-sm text-red-700 mt-1">
{selectedJudge.title}
</p>
<p className="text-sm text-red-700">
{selectedJudge.department}
</p>
<p className="text-sm text-red-700">
{selectedJudge.expertise?.length || 0}
</p>
</div>
</div>
</div>
<div className="text-sm text-gray-600 space-y-1">
<p> </p>
<ul className="list-disc list-inside space-y-1 ml-4">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
)}
<div className="flex justify-end space-x-3 pt-4">
<Button
variant="outline"
onClick={() => {
setShowDeleteJudgeConfirm(false)
setSelectedJudge(null)
}}
disabled={isLoading}
>
</Button>
<Button
variant="destructive"
onClick={confirmDeleteJudge}
disabled={isLoading}
className="bg-red-600 hover:bg-red-700"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Trash2 className="w-4 h-4 mr-2" />
</>
)}
</Button>
</div>
</DialogContent>
</Dialog>
{/* 創建獎項對話框 */}
<Dialog open={showCreateAward} onOpenChange={setShowCreateAward}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<Award className="w-6 h-6 text-orange-600" />
<span>{selectedAward ? "編輯獎項" : "創建獎項"}</span>
</DialogTitle>
<DialogDescription>
{selectedAward ? "修改獎項資訊,更新相關競賽、團隊和評審資訊" : "為競賽參賽者創建獎項,系統將自動關聯相關競賽、團隊和評審資訊"}
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="basic"></TabsTrigger>
<TabsTrigger value="participants"></TabsTrigger>
<TabsTrigger value="links"></TabsTrigger>
<TabsTrigger value="documents"></TabsTrigger>
<TabsTrigger value="photos"></TabsTrigger>
</TabsList>
{/* 基本資訊 */}
<TabsContent value="basic" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 競賽選擇 */}
<div className="space-y-2">
<Label htmlFor="award-competition">
<span className="text-red-500">*</span>
</Label>
<Select
value={newAward.competitionId}
onValueChange={(value) => {
setNewAward({ ...newAward, competitionId: value, participantId: "" })
}}
>
<SelectTrigger>
<SelectValue placeholder="選擇競賽" />
</SelectTrigger>
<SelectContent>
{competitions.map((competition) => (
<SelectItem key={competition.id} value={competition.id}>
<div className="flex flex-col">
<span className="font-medium">{competition.name}</span>
<span className="text-xs text-gray-500">
{competition.year}{competition.month} {competition.type === "individual" ? "個人賽" : competition.type === "team" ? "團體賽" : "混合賽"}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 獎項名稱 */}
<div className="space-y-2">
<Label htmlFor="award-name">
<span className="text-red-500">*</span>
</Label>
<Input
id="award-name"
value={newAward.awardName}
onChange={(e) => setNewAward({ ...newAward, awardName: e.target.value })}
placeholder="例如:最佳創新獎、金獎、銀獎"
/>
</div>
{/* 獎項類型 */}
<div className="space-y-2">
<Label></Label>
<Select
value={newAward.awardType}
onValueChange={(value: any) => setNewAward({ ...newAward, awardType: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="gold">🥇 </SelectItem>
<SelectItem value="silver">🥈 </SelectItem>
<SelectItem value="bronze">🥉 </SelectItem>
<SelectItem value="popular">👥 </SelectItem>
<SelectItem value="innovation">💡 </SelectItem>
<SelectItem value="technical"> </SelectItem>
<SelectItem value="custom">🏆 </SelectItem>
</SelectContent>
</Select>
</div>
{/* 獎項類別 */}
<div className="space-y-2">
<Label></Label>
<Select
value={newAward.category}
onValueChange={(value: any) => setNewAward({ ...newAward, category: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="innovation"></SelectItem>
<SelectItem value="technical"></SelectItem>
<SelectItem value="practical"></SelectItem>
<SelectItem value="popular"></SelectItem>
<SelectItem value="teamwork"></SelectItem>
<SelectItem value="solution"></SelectItem>
<SelectItem value="creativity"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 獎項排名 */}
<div className="space-y-2">
<Label></Label>
<Select
value={newAward.rank.toString()}
onValueChange={(value) => setNewAward({ ...newAward, rank: parseInt(value) })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0"></SelectItem>
<SelectItem value="1"></SelectItem>
<SelectItem value="2"></SelectItem>
<SelectItem value="3"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 評分 */}
<div className="space-y-2">
<Label></Label>
<Input
type="number"
min="0"
max="5"
step="0.1"
value={newAward.score}
onChange={(e) => setNewAward({ ...newAward, score: parseFloat(e.target.value) || 0 })}
placeholder="0.0 - 5.0"
/>
<p className="text-xs text-gray-500">滿5.0</p>
</div>
</div>
{/* 獎項描述 */}
<div className="space-y-2">
<Label></Label>
<Textarea
value={newAward.description}
onChange={(e) => setNewAward({ ...newAward, description: e.target.value })}
placeholder="描述此獎項的意義、評選標準或特殊說明..."
rows={3}
/>
</div>
{/* 評審評語 */}
<div className="space-y-2">
<Label></Label>
<Textarea
value={newAward.judgeComments}
onChange={(e) => setNewAward({ ...newAward, judgeComments: e.target.value })}
placeholder="評審團對此作品的整體評語和建議..."
rows={3}
/>
</div>
{/* 競賽資訊預覽 */}
{newAward.competitionId && (() => {
const selectedCompetition = competitions.find(c => c.id === newAward.competitionId)
if (!selectedCompetition) return null
return (
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="font-medium text-blue-900 mb-2"></h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
<div><span className="font-medium"></span>{selectedCompetition.name}</div>
<div><span className="font-medium"></span>{selectedCompetition.type === "individual" ? "個人賽" : selectedCompetition.type === "team" ? "團體賽" : "混合賽"}</div>
<div><span className="font-medium"></span>{selectedCompetition.startDate} ~ {selectedCompetition.endDate}</div>
<div><span className="font-medium"></span>{judges.filter(j => selectedCompetition.judges?.includes(j.id)).length} </div>
</div>
</div>
)
})()}
</TabsContent>
{/* 參賽者選擇 */}
<TabsContent value="participants" className="space-y-4">
{newAward.competitionId ? (() => {
const selectedCompetition = competitions.find(c => c.id === newAward.competitionId)
if (!selectedCompetition) return <p className="text-gray-500"></p>
return (
<div className="space-y-4">
{/* 參賽者類型選擇 */}
{selectedCompetition.type === "mixed" && (
<div className="space-y-2">
<Label></Label>
<div className="flex space-x-2">
<Button
type="button"
variant={newAward.participantType === "individual" ? "default" : "outline"}
onClick={() => setNewAward({ ...newAward, participantType: "individual", participantId: "" })}
>
</Button>
<Button
type="button"
variant={newAward.participantType === "team" ? "default" : "outline"}
onClick={() => setNewAward({ ...newAward, participantType: "team", participantId: "" })}
>
</Button>
</div>
</div>
)}
{/* 參賽者選擇 */}
<div className="space-y-2">
<Label>
{newAward.participantType === "individual" ? "個人" : "團隊"} <span className="text-red-500">*</span>
</Label>
<Select
value={newAward.participantId}
onValueChange={(value) => setNewAward({ ...newAward, participantId: value })}
>
<SelectTrigger>
<SelectValue placeholder={`選擇${newAward.participantType === "individual" ? "個人" : "團隊"}參賽者`} />
</SelectTrigger>
<SelectContent>
{newAward.participantType === "individual" ? (
// 這裡應該從 mockIndividualApps 獲取,暫時用示例數據
[
{ id: "app1", name: "智能客服系統", creator: "張小明", department: "ITBU" },
{ id: "app2", name: "數據分析平台", creator: "李美華", department: "研發部" },
].map((app) => (
<SelectItem key={app.id} value={app.id}>
<div className="flex flex-col">
<span className="font-medium">{app.name}</span>
<span className="text-xs text-gray-500">by {app.creator} {app.department}</span>
</div>
</SelectItem>
))
) : (
(dbTeams.length > 0 ? dbTeams : teams).map((team) => (
<SelectItem key={team.id} value={team.id}>
<div className="flex flex-col">
<span className="font-medium">{team.name}</span>
<span className="text-xs text-gray-500">{team.leader} {team.department} {team.memberCount}</span>
</div>
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
{/* 參賽者資訊預覽 */}
{newAward.participantId && (() => {
if (newAward.participantType === "individual") {
const app = [
{ id: "app1", name: "智能客服系統", creator: "張小明", department: "ITBU", description: "基於AI的智能客服解決方案" },
{ id: "app2", name: "數據分析平台", creator: "李美華", department: "研發部", description: "企業級數據分析和視覺化平台" },
].find(a => a.id === newAward.participantId)
if (!app) return null
return (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 className="font-medium text-green-900 mb-2"></h4>
<div className="space-y-2 text-sm">
<div><span className="font-medium"></span>{app.name}</div>
<div><span className="font-medium"></span>{app.creator}</div>
<div><span className="font-medium"></span>{app.department}</div>
<div><span className="font-medium"></span>{app.description}</div>
</div>
</div>
)
} else {
const team = (dbTeams.length > 0 ? dbTeams : teams).find(t => t.id === newAward.participantId)
if (!team) return null
return (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 className="font-medium text-green-900 mb-2"></h4>
<div className="space-y-2 text-sm">
<div><span className="font-medium"></span>{team.name}</div>
<div><span className="font-medium"></span>{team.leader}</div>
<div><span className="font-medium"></span>{team.department}</div>
<div><span className="font-medium"></span>{team.memberCount}</div>
<div><span className="font-medium"></span>{team.submittedAppCount}</div>
<div><span className="font-medium"></span>{team.contactEmail}</div>
</div>
</div>
)
}
})()}
{/* 評審團資訊 */}
<div className="p-4 bg-purple-50 border border-purple-200 rounded-lg">
<h4 className="font-medium text-purple-900 mb-2"></h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{(dbJudges.length > 0 ? dbJudges : judges).filter(judge => selectedCompetition.judges?.includes(judge.id)).map((judge) => (
<div key={judge.id} className="flex items-center space-x-2 text-sm">
<Avatar className="w-6 h-6">
<AvatarFallback className="text-xs bg-purple-100 text-purple-600">
{judge.name.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<span className="font-medium">{judge.name}</span>
<span className="text-gray-500 ml-1"> {judge.department}</span>
</div>
</div>
))}
</div>
</div>
</div>
)
})() : (
<div className="text-center py-8 text-gray-500">
<Trophy className="w-12 h-12 mx-auto mb-4 text-gray-300" />
<p></p>
</div>
)}
</TabsContent>
{/* 應用連結 */}
<TabsContent value="links" className="space-y-4">
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* 正式應用 */}
<div className="space-y-2">
<Label className="flex items-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<span></span>
</Label>
<Input
value={newAward.applicationLinks.production}
onChange={(e) => setNewAward({
...newAward,
applicationLinks: { ...newAward.applicationLinks, production: e.target.value }
})}
placeholder="https://app.example.com"
/>
<p className="text-xs text-gray-500"></p>
</div>
{/* 演示版本 */}
<div className="space-y-2">
<Label className="flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<span></span>
</Label>
<Input
value={newAward.applicationLinks.demo}
onChange={(e) => setNewAward({
...newAward,
applicationLinks: { ...newAward.applicationLinks, demo: e.target.value }
})}
placeholder="https://demo.example.com"
/>
<p className="text-xs text-gray-500"></p>
</div>
{/* GitHub */}
<div className="space-y-2">
<Label className="flex items-center space-x-2">
<div className="w-3 h-3 bg-gray-800 rounded-full"></div>
<span></span>
</Label>
<Input
value={newAward.applicationLinks.github}
onChange={(e) => setNewAward({
...newAward,
applicationLinks: { ...newAward.applicationLinks, github: e.target.value }
})}
placeholder="https://github.com/..."
/>
<p className="text-xs text-gray-500">GitHub </p>
</div>
</div>
{/* 連結預覽 */}
<div className="p-4 bg-gray-50 border border-gray-200 rounded-lg">
<h4 className="font-medium text-gray-900 mb-3"></h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{newAward.applicationLinks.production && (
<div className="p-3 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center space-x-2 mb-2">
<div className="w-4 h-4 bg-green-500 rounded-full flex items-center justify-center">
<span className="text-white text-xs"></span>
</div>
<span className="text-sm font-medium"></span>
</div>
<p className="text-xs text-gray-600 break-all">{newAward.applicationLinks.production}</p>
</div>
)}
{newAward.applicationLinks.demo && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center space-x-2 mb-2">
<div className="w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-white text-xs"></span>
</div>
<span className="text-sm font-medium"></span>
</div>
<p className="text-xs text-gray-600 break-all">{newAward.applicationLinks.demo}</p>
</div>
)}
{newAward.applicationLinks.github && (
<div className="p-3 bg-gray-50 border border-gray-300 rounded-lg">
<div className="flex items-center space-x-2 mb-2">
<div className="w-4 h-4 bg-gray-800 rounded-full flex items-center justify-center">
<span className="text-white text-xs"></span>
</div>
<span className="text-sm font-medium"></span>
</div>
<p className="text-xs text-gray-600 break-all">{newAward.applicationLinks.github}</p>
</div>
)}
</div>
</div>
</div>
</TabsContent>
{/* 相關文檔 */}
<TabsContent value="documents" className="space-y-4">
<div className="space-y-4">
{/* 文檔上傳區域 */}
<div className="p-6 border-2 border-dashed border-gray-300 rounded-lg text-center">
<Upload className="w-12 h-12 mx-auto mb-4 text-gray-400" />
<h4 className="text-lg font-medium text-gray-900 mb-2"></h4>
<p className="text-gray-600 mb-4"> PDFDOCDOCXPPTX 10MB</p>
<Button
type="button"
variant="outline"
onClick={() => {
// 模擬文檔上傳
const newDoc = {
id: `doc_${Date.now()}`,
name: `示例文檔_${newAward.documents.length + 1}.pdf`,
type: "PDF",
size: "2.5 MB",
uploadDate: new Date().toISOString().split("T")[0],
url: "#"
}
setNewAward({
...newAward,
documents: [...newAward.documents, newDoc]
})
}}
>
<Upload className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 已上傳文檔列表 */}
{newAward.documents.length > 0 && (
<div className="space-y-3">
<h4 className="font-medium text-gray-900"></h4>
{newAward.documents.map((doc) => (
<div key={doc.id} className="flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center">
<span className="text-xs font-medium text-red-600">{doc.type}</span>
</div>
<div>
<p className="font-medium text-gray-900">{doc.name}</p>
<p className="text-xs text-gray-500">
{doc.size} {doc.uploadDate}
</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Button variant="outline" size="sm">
<Eye className="w-4 h-4 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setNewAward({
...newAward,
documents: newAward.documents.filter(d => d.id !== doc.id)
})
}}
className="text-red-600 border-red-300 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
))}
</div>
)}
{/* 常見文檔類型模板 */}
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="font-medium text-blue-900 mb-3"></h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
<span> ()</span>
</div>
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
<span> ()</span>
</div>
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
<span> (使)</span>
</div>
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
<span> ()</span>
</div>
</div>
</div>
</div>
</TabsContent>
{/* 得獎照片 */}
<TabsContent value="photos" className="space-y-4">
<div className="space-y-4">
{/* 照片上傳區域 */}
<div className="p-6 border-2 border-dashed border-gray-300 rounded-lg text-center">
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-lg flex items-center justify-center">
📸
</div>
<h4 className="text-lg font-medium text-gray-900 mb-2"></h4>
<p className="text-gray-600 mb-4"> JPGPNGGIF 1920x1080 5MB</p>
<Button
type="button"
variant="outline"
onClick={() => {
// 模擬照片上傳
const newPhoto = {
id: `photo_${Date.now()}`,
name: `得獎照片_${newAward.photos.length + 1}.jpg`,
url: "/placeholder.jpg", // 實際應用中會是真實的圖片URL
caption: "",
uploadDate: new Date().toISOString().split("T")[0],
size: "2.5 MB"
}
setNewAward({
...newAward,
photos: [...newAward.photos, newPhoto]
})
}}
>
<Upload className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 已上傳照片預覽 */}
{newAward.photos.length > 0 && (
<div className="space-y-4">
<h4 className="font-medium text-gray-900"> ({newAward.photos.length})</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{newAward.photos.map((photo) => (
<div key={photo.id} className="relative group border border-gray-200 rounded-lg overflow-hidden">
{/* 照片預覽 */}
<div className="aspect-video bg-gray-100 flex items-center justify-center">
<div className="text-4xl">🖼</div>
</div>
{/* 照片資訊 */}
<div className="p-3 space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-900 truncate">{photo.name}</span>
<Button
variant="ghost"
size="sm"
onClick={() => {
setNewAward({
...newAward,
photos: newAward.photos.filter(p => p.id !== photo.id)
})
}}
className="text-red-600 hover:bg-red-50 h-6 w-6 p-0"
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
<div className="text-xs text-gray-500">
{photo.size} {photo.uploadDate}
</div>
{/* 照片說明 */}
<Input
placeholder="輸入照片說明..."
value={photo.caption}
onChange={(e) => {
const updatedPhotos = newAward.photos.map(p =>
p.id === photo.id ? { ...p, caption: e.target.value } : p
)
setNewAward({ ...newAward, photos: updatedPhotos })
}}
className="text-xs"
/>
</div>
</div>
))}
</div>
</div>
)}
{/* 照片說明指南 */}
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg">
<h4 className="font-medium text-amber-900 mb-3"></h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm text-amber-800">
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-amber-500 rounded-full"></span>
<span></span>
</div>
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-amber-500 rounded-full"></span>
<span></span>
</div>
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-amber-500 rounded-full"></span>
<span></span>
</div>
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-amber-500 rounded-full"></span>
<span></span>
</div>
</div>
</div>
{/* 照片展示效果預覽 */}
{newAward.photos.length > 0 && (
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="font-medium text-blue-900 mb-3"></h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{newAward.photos.slice(0, 4).map((photo, index) => (
<div key={photo.id} className="aspect-video bg-blue-100 rounded border flex items-center justify-center text-xs text-blue-600">
{index + 1}
</div>
))}
{newAward.photos.length > 4 && (
<div className="aspect-video bg-blue-100 rounded border flex items-center justify-center text-xs text-blue-600">
+{newAward.photos.length - 4}
</div>
)}
</div>
<p className="text-xs text-blue-700 mt-2">
{newAward.photos.length > 4
? "前台將顯示前4張照片用戶可點擊「查看所有照片」查看完整相簿"
: "前台將顯示所有上傳的照片"}
</p>
</div>
)}
</div>
</TabsContent>
</Tabs>
{error && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="flex justify-end space-x-3 pt-4 border-t">
<Button variant="outline" onClick={() => {
setShowCreateAward(false)
setSelectedAward(null)
setNewAward({
competitionId: "",
participantId: "",
participantType: "individual",
awardType: "custom",
awardName: "",
customAwardTypeId: "",
description: "",
score: 0,
category: "innovation",
rank: 0,
applicationLinks: {
production: "",
demo: "",
github: "",
},
documents: [],
judgeComments: "",
photos: [],
})
}} disabled={isLoading}>
</Button>
<Button onClick={handleCreateAward} disabled={isLoading} className="bg-gradient-to-r from-orange-600 to-red-600 hover:from-orange-700 hover:to-red-700">
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{selectedAward ? "更新中..." : "創建中..."}
</>
) : (
<>
<Award className="w-4 h-4 mr-2" />
{selectedAward ? "更新獎項" : "創建獎項"}
</>
)}
</Button>
</div>
</DialogContent>
</Dialog>
{/* 查看獎項詳情對話框 */}
<Dialog open={showAwardDetail} onOpenChange={setShowAwardDetail}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<Award className="w-6 h-6 text-orange-600" />
<span></span>
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{selectedAward && (
<div className="space-y-6">
{/* 獎項基本資訊卡片 */}
<div className="flex items-start space-x-4 p-4 bg-gradient-to-r from-orange-50 to-amber-50 rounded-lg border">
<div className="w-16 h-16 bg-gradient-to-r from-orange-600 to-amber-600 rounded-xl flex items-center justify-center flex-shrink-0 text-2xl">
{selectedAward.icon}
</div>
<div className="flex-1 space-y-3">
<div>
<h3 className="text-xl font-bold text-gray-900">{selectedAward.awardName}</h3>
<p className="text-gray-600 mt-1">
{selectedAward.appName || "團隊作品"} by {selectedAward.creator}
</p>
</div>
<div className="flex items-center space-x-3">
<Badge
variant="secondary"
className={`${
selectedAward.awardType === "gold"
? "bg-yellow-100 text-yellow-800"
: selectedAward.awardType === "silver"
? "bg-gray-100 text-gray-800"
: selectedAward.awardType === "bronze"
? "bg-orange-100 text-orange-800"
: selectedAward.awardType === "popular"
? "bg-purple-100 text-purple-800"
: selectedAward.awardType === "innovation"
? "bg-green-100 text-green-800"
: selectedAward.awardType === "technical"
? "bg-indigo-100 text-indigo-800"
: "bg-blue-100 text-blue-800"
}`}
>
{selectedAward.awardType}
</Badge>
<Badge variant="outline">
{(selectedAward as any).category || "innovation"}
</Badge>
{selectedAward.rank > 0 && (
<Badge variant="outline" className="bg-amber-50 text-amber-700">
{selectedAward.rank}
</Badge>
)}
{selectedAward.score > 0 && (
<div className="flex items-center space-x-1">
<Star className="w-4 h-4 text-yellow-500 fill-current" />
<span className="font-semibold text-orange-600">{selectedAward.score.toFixed(1)}</span>
</div>
)}
</div>
</div>
</div>
{/* 基本資訊 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<div className="space-y-3">
<div>
<Label className="text-sm font-medium text-gray-700">ID</Label>
<p className="text-sm text-gray-900">{selectedAward.id}</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-700"></Label>
<p className="text-sm text-gray-900">{selectedAward.year}{selectedAward.month}</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-700"></Label>
<p className="text-sm text-gray-900">
{selectedAward.competitionType === "individual" ? "個人賽" : "團體賽"}
</p>
</div>
</div>
</div>
<div className="space-y-4">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<div className="space-y-3">
<div>
<Label className="text-sm font-medium text-gray-700"></Label>
<p className="text-sm text-gray-900">
{competitions.find(c => c.id === selectedAward.competitionId)?.name || "未知競賽"}
</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-700"></Label>
<div className="flex items-center space-x-2 mt-1">
{(() => {
const competition = competitions.find(c => c.id === selectedAward.competitionId)
const competitionJudges = judges.filter(j => competition?.judges?.includes(j.id))
return competitionJudges.slice(0, 3).map((judge) => (
<Avatar key={judge.id} className="w-6 h-6">
<AvatarFallback className="text-xs bg-orange-100 text-orange-600">
{judge.name.charAt(0)}
</AvatarFallback>
</Avatar>
))
})()}
{(() => {
const competition = competitions.find(c => c.id === selectedAward.competitionId)
const competitionJudges = judges.filter(j => competition?.judges?.includes(j.id))
if (competitionJudges.length > 3) {
return <span className="text-xs text-gray-500">+{competitionJudges.length - 3}</span>
}
return null
})()}
</div>
</div>
</div>
</div>
</div>
{/* 獎項描述 */}
{(selectedAward as any).description && (
<div className="space-y-3">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<p className="text-gray-700 leading-relaxed">{(selectedAward as any).description}</p>
</div>
)}
{/* 評審評語 */}
{(selectedAward as any).judgeComments && (
<div className="space-y-3">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-blue-900 leading-relaxed">{(selectedAward as any).judgeComments}</p>
</div>
</div>
)}
{/* 應用連結 */}
{(selectedAward as any).applicationLinks && (
<div className="space-y-3">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{(selectedAward as any).applicationLinks.production && (
<div className="p-3 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center space-x-2 mb-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<span className="text-sm font-medium text-green-900"></span>
</div>
<p className="text-xs text-green-700 break-all">
{(selectedAward as any).applicationLinks.production}
</p>
</div>
)}
{(selectedAward as any).applicationLinks.demo && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center space-x-2 mb-2">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<span className="text-sm font-medium text-blue-900"></span>
</div>
<p className="text-xs text-blue-700 break-all">
{(selectedAward as any).applicationLinks.demo}
</p>
</div>
)}
{(selectedAward as any).applicationLinks.github && (
<div className="p-3 bg-gray-50 border border-gray-300 rounded-lg">
<div className="flex items-center space-x-2 mb-2">
<div className="w-3 h-3 bg-gray-800 rounded-full"></div>
<span className="text-sm font-medium text-gray-900"></span>
</div>
<p className="text-xs text-gray-700 break-all">
{(selectedAward as any).applicationLinks.github}
</p>
</div>
)}
</div>
</div>
)}
{/* 相關文檔 */}
{(selectedAward as any).documents && (selectedAward as any).documents.length > 0 && (
<div className="space-y-3">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<div className="space-y-2">
{(selectedAward as any).documents.map((doc: any) => (
<div key={doc.id} className="flex items-center space-x-3 p-3 bg-gray-50 border border-gray-200 rounded-lg">
<div className="w-8 h-8 bg-red-100 rounded flex items-center justify-center">
<span className="text-xs font-medium text-red-600">{doc.type}</span>
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">{doc.name}</p>
<p className="text-xs text-gray-500">{doc.size} {doc.uploadDate}</p>
</div>
<Button variant="outline" size="sm">
<Eye className="w-3 h-3 mr-1" />
</Button>
</div>
))}
</div>
</div>
)}
{/* 得獎照片 */}
{(selectedAward as any).photos && (selectedAward as any).photos.length > 0 && (
<div className="space-y-3">
<h4 className="text-lg font-semibold text-gray-900"></h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{(selectedAward as any).photos.map((photo: any) => (
<div key={photo.id} className="space-y-2">
<div className="aspect-video bg-gray-100 rounded-lg border flex items-center justify-center">
<div className="text-2xl">🖼</div>
</div>
{photo.caption && (
<p className="text-xs text-gray-600 text-center">{photo.caption}</p>
)}
</div>
))}
</div>
</div>
)}
<div className="flex justify-end space-x-3 pt-4 border-t">
<Button variant="outline" onClick={() => setShowAwardDetail(false)}>
</Button>
<Button onClick={() => {
setShowAwardDetail(false)
handleEditAward(selectedAward)
}} className="bg-gradient-to-r from-orange-600 to-amber-600 hover:from-orange-700 hover:to-amber-700">
<Edit className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
{/* 刪除獎項確認對話框 */}
<Dialog open={showDeleteAwardConfirm} onOpenChange={setShowDeleteAwardConfirm}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2 text-red-600">
<AlertTriangle className="w-5 h-5" />
<span></span>
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{awardToDelete && (
<div className="space-y-4">
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start space-x-3">
<div className="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center flex-shrink-0 text-lg">
{awardToDelete.icon}
</div>
<div className="flex-1">
<h4 className="font-medium text-red-900">{awardToDelete.awardName}</h4>
<p className="text-sm text-red-700 mt-1">
{awardToDelete.appName || "團隊作品"}
</p>
<p className="text-sm text-red-700">
{awardToDelete.creator}
</p>
<p className="text-sm text-red-700">
{awardToDelete.year}{awardToDelete.month}
</p>
</div>
</div>
</div>
<div className="text-sm text-gray-600 space-y-1">
<p> </p>
<ul className="list-disc list-inside space-y-1 ml-4">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
)}
<div className="flex justify-end space-x-3 pt-4">
<Button
variant="outline"
onClick={() => {
setShowDeleteAwardConfirm(false)
setAwardToDelete(null)
}}
disabled={isLoading}
>
</Button>
<Button
variant="destructive"
onClick={confirmDeleteAward}
disabled={isLoading}
className="bg-red-600 hover:bg-red-700"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Trash2 className="w-4 h-4 mr-2" />
</>
)}
</Button>
</div>
</DialogContent>
</Dialog>
</div>
)
}