修正獎勵專區評審團資料異常 bug

This commit is contained in:
2025-09-27 01:06:26 +08:00
parent 1aa8a06da1
commit 2597a07514
3 changed files with 160 additions and 65 deletions

View File

@@ -2143,11 +2143,17 @@ export function CompetitionManagement() {
if (teamsData.success && teamsData.data && teamsData.data.teams) {
console.log('✅ 競賽團隊載入成功:', teamsData.data.teams.length, '個團隊')
updatedCompetition.teams = teamsData.data.teams
// 同時更新dbTeams
// 確保團隊數據的唯一性避免重複的ID
const uniqueTeams = teamsData.data.teams.filter((team: any, index: number, self: any[]) =>
self.findIndex(t => t.id === team.id) === index
)
updatedCompetition.teams = uniqueTeams
console.log('🔍 去重後的團隊數量:', uniqueTeams.length)
// 同時更新dbTeams確保不重複添加
setDbTeams(prev => {
const existingIds = prev.map(t => t.id)
const newTeams = teamsData.data.teams.filter((t: any) => !existingIds.includes(t.id))
const newTeams = uniqueTeams.filter((t: any) => !existingIds.includes(t.id))
return [...prev, ...newTeams]
})
} else {
@@ -3195,8 +3201,8 @@ export function CompetitionManagement() {
)
}
return paginatedTeams.map((team) => (
<Card key={team.id} className="relative h-fit">
return paginatedTeams.map((team, index) => (
<Card key={`${team.id}-${index}`} className="relative h-fit">
<CardContent className="p-4">
<div className="space-y-3">
<div className="flex items-start justify-between">
@@ -4067,7 +4073,7 @@ export function CompetitionManagement() {
if (links && links.production) {
linkItems.push(
<div key="production" className="flex items-center space-x-1">
<div className="w-2 h-2 bg-green-500 rounded-full" title="生產環境"></div>
<div className="w-2 h-2 bg-green-500 rounded-full" title="APP 連結"></div>
<a
href={links.production}
target="_blank"
@@ -4075,7 +4081,7 @@ export function CompetitionManagement() {
className="text-xs text-blue-600 hover:text-blue-800 underline truncate max-w-24"
title={links.production}
>
APP
</a>
</div>
);
@@ -4538,7 +4544,7 @@ export function CompetitionManagement() {
<div className="col-span-4">
<Label className="text-xs"></Label>
<Input
value={rule.name || ""}
value={rule.name ?? ""}
onChange={(e) => {
const updatedRules = [...newCompetition.individualConfig.rules]
updatedRules[index] = { ...rule, name: e.target.value }
@@ -4557,7 +4563,7 @@ export function CompetitionManagement() {
<div className="col-span-5">
<Label className="text-xs"></Label>
<Input
value={rule.description || ""}
value={rule.description ?? ""}
onChange={(e) => {
const updatedRules = [...newCompetition.individualConfig.rules]
updatedRules[index] = { ...rule, description: e.target.value }
@@ -4579,7 +4585,7 @@ export function CompetitionManagement() {
type="number"
min="0"
max="100"
value={rule.weight || 0}
value={rule.weight ?? 0}
onChange={(e) => {
const updatedRules = [...newCompetition.individualConfig.rules]
updatedRules[index] = { ...rule, weight: Number.parseInt(e.target.value) || 0 }
@@ -4672,7 +4678,7 @@ export function CompetitionManagement() {
<div className="col-span-1">
<Label className="text-xs"></Label>
<Select
value={awardType.icon || "🏆"}
value={awardType.icon ?? "🏆"}
onValueChange={(value) => {
const updatedAwardTypes = [...newCompetition.individualConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, icon: value }
@@ -4706,7 +4712,7 @@ export function CompetitionManagement() {
<div className="col-span-3">
<Label className="text-xs"></Label>
<Input
value={awardType.name || ""}
value={awardType.name ?? ""}
onChange={(e) => {
const updatedAwardTypes = [...newCompetition.individualConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, name: e.target.value }
@@ -4725,7 +4731,7 @@ export function CompetitionManagement() {
<div className="col-span-5">
<Label className="text-xs"></Label>
<Input
value={awardType.description || ""}
value={awardType.description ?? ""}
onChange={(e) => {
const updatedAwardTypes = [...newCompetition.individualConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, description: e.target.value }
@@ -4744,7 +4750,7 @@ export function CompetitionManagement() {
<div className="col-span-2">
<Label className="text-xs"></Label>
<Select
value={awardType.color || "text-yellow-600"}
value={awardType.color ?? "text-yellow-600"}
onValueChange={(value) => {
const updatedAwardTypes = [...newCompetition.individualConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, color: value }
@@ -4950,7 +4956,7 @@ export function CompetitionManagement() {
<div className="col-span-4">
<Label className="text-xs"></Label>
<Input
value={rule.name || ""}
value={rule.name ?? ""}
onChange={(e) => {
const updatedRules = [...newCompetition.teamConfig.rules]
updatedRules[index] = { ...rule, name: e.target.value }
@@ -4969,7 +4975,7 @@ export function CompetitionManagement() {
<div className="col-span-5">
<Label className="text-xs"></Label>
<Input
value={rule.description || ""}
value={rule.description ?? ""}
onChange={(e) => {
const updatedRules = [...newCompetition.teamConfig.rules]
updatedRules[index] = { ...rule, description: e.target.value }
@@ -4991,7 +4997,7 @@ export function CompetitionManagement() {
type="number"
min="0"
max="100"
value={rule.weight || 0}
value={rule.weight ?? 0}
onChange={(e) => {
const updatedRules = [...newCompetition.teamConfig.rules]
updatedRules[index] = { ...rule, weight: Number.parseInt(e.target.value) || 0 }
@@ -5084,7 +5090,7 @@ export function CompetitionManagement() {
<div className="col-span-1">
<Label className="text-xs"></Label>
<Select
value={awardType.icon || "🏆"}
value={awardType.icon ?? "🏆"}
onValueChange={(value) => {
const updatedAwardTypes = [...newCompetition.teamConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, icon: value }
@@ -5119,7 +5125,7 @@ export function CompetitionManagement() {
<div className="col-span-3">
<Label className="text-xs"></Label>
<Input
value={awardType.name || ""}
value={awardType.name ?? ""}
onChange={(e) => {
const updatedAwardTypes = [...newCompetition.teamConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, name: e.target.value }
@@ -5138,7 +5144,7 @@ export function CompetitionManagement() {
<div className="col-span-5">
<Label className="text-xs"></Label>
<Input
value={awardType.description || ""}
value={awardType.description ?? ""}
onChange={(e) => {
const updatedAwardTypes = [...newCompetition.teamConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, description: e.target.value }
@@ -5157,7 +5163,7 @@ export function CompetitionManagement() {
<div className="col-span-2">
<Label className="text-xs"></Label>
<Select
value={awardType.color || "text-yellow-600"}
value={awardType.color ?? "text-yellow-600"}
onValueChange={(value) => {
const updatedAwardTypes = [...newCompetition.teamConfig.awardTypes]
updatedAwardTypes[index] = { ...awardType, color: value }
@@ -5350,7 +5356,7 @@ export function CompetitionManagement() {
<div className="col-span-4">
<Label className="text-xs"></Label>
<Input
value={rule.name || ""}
value={rule.name ?? ""}
onChange={(e) => {
const updatedRules = [...newCompetition.rules]
updatedRules[index] = { ...rule, name: e.target.value }
@@ -5363,7 +5369,7 @@ export function CompetitionManagement() {
<div className="col-span-5">
<Label className="text-xs"></Label>
<Input
value={rule.description || ""}
value={rule.description ?? ""}
onChange={(e) => {
const updatedRules = [...newCompetition.rules]
updatedRules[index] = { ...rule, description: e.target.value }
@@ -5379,7 +5385,7 @@ export function CompetitionManagement() {
type="number"
min="0"
max="100"
value={rule.weight || 0}
value={rule.weight ?? 0}
onChange={(e) => {
const updatedRules = [...newCompetition.rules]
updatedRules[index] = { ...rule, weight: Number.parseInt(e.target.value) || 0 }
@@ -5468,7 +5474,7 @@ export function CompetitionManagement() {
<div className="col-span-1">
<Label className="text-xs"></Label>
<Select
value={awardType.icon || "🏆"}
value={awardType.icon ?? "🏆"}
onValueChange={(value) => {
const updatedAwardTypes = [...newCompetition.awardTypes]
updatedAwardTypes[index] = { ...awardType, icon: value }
@@ -5497,7 +5503,7 @@ export function CompetitionManagement() {
<div className="col-span-3">
<Label className="text-xs"></Label>
<Input
value={awardType.name || ""}
value={awardType.name ?? ""}
onChange={(e) => {
const updatedAwardTypes = [...newCompetition.awardTypes]
updatedAwardTypes[index] = { ...awardType, name: e.target.value }
@@ -5510,7 +5516,7 @@ export function CompetitionManagement() {
<div className="col-span-5">
<Label className="text-xs"></Label>
<Input
value={awardType.description || ""}
value={awardType.description ?? ""}
onChange={(e) => {
const updatedAwardTypes = [...newCompetition.awardTypes]
updatedAwardTypes[index] = { ...awardType, description: e.target.value }
@@ -5523,7 +5529,7 @@ export function CompetitionManagement() {
<div className="col-span-2">
<Label className="text-xs"></Label>
<Select
value={awardType.color || "text-yellow-600"}
value={awardType.color ?? "text-yellow-600"}
onValueChange={(value) => {
const updatedAwardTypes = [...newCompetition.awardTypes]
updatedAwardTypes[index] = { ...awardType, color: value }
@@ -6175,8 +6181,12 @@ export function CompetitionManagement() {
</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">
selectedCompetitionForAction.teams
.filter((team: any, index: number, self: any[]) =>
self.findIndex(t => t.id === team.id) === index
)
.map((team: any, index: number) => (
<div key={`${team.id}-${index}`} 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>
@@ -6485,12 +6495,16 @@ export function CompetitionManagement() {
(() => {
// 使用selectedCompetition.teams數據顯示為「團隊名 - APP名」格式
const teamsData = selectedCompetition?.teams || []
return teamsData.length > 0 ? teamsData.map((team: any) => {
return teamsData.length > 0 ? teamsData
.filter((team: any, index: number, self: any[]) =>
self.findIndex(t => t.id === team.id) === index
)
.map((team: any, index: number) => {
// 檢查團隊是否有APP
const teamApps = team.apps || []
if (teamApps.length === 0) {
return (
<SelectItem key={team.id} value={team.id} disabled>
<SelectItem key={`${team.id}-${index}`} value={team.id} disabled>
<div className="flex items-center space-x-2">
<Users className="w-4 h-4 text-gray-400" />
<span className="text-gray-400">{team.name}</span>
@@ -6503,7 +6517,7 @@ export function CompetitionManagement() {
// 顯示團隊和其第一個APP
const firstApp = teamApps[0]
return (
<SelectItem key={team.id} value={team.id}>
<SelectItem key={`${team.id}-${index}`} value={team.id}>
<div className="flex items-center space-x-2">
<Users className="w-4 h-4 text-green-600" />
<span>{team.name}</span>
@@ -7813,7 +7827,7 @@ export function CompetitionManagement() {
<div className="space-y-2">
<Label></Label>
<Select
value={newAward.customAwardTypeId || newAward.awardType}
value={newAward.customAwardTypeId ?? newAward.awardType}
onValueChange={(value: any) => {
// 檢查是否為自定義獎項類型
const customAwardType = competitionAwardTypes.find(type => type.id === value)
@@ -8095,8 +8109,12 @@ export function CompetitionManagement() {
} else if (shouldShowTeam) {
// 團體賽:只顯示團隊,不顯示 app
return competitionTeams.length > 0 ? (
competitionTeams.map((team) => (
<SelectItem key={team.id} value={team.id}>
competitionTeams
.filter((team: any, index: number, self: any[]) =>
self.findIndex(t => t.id === team.id) === index
)
.map((team: any, index: number) => (
<SelectItem key={`${team.id}-${index}`} value={team.id}>
<div className="flex flex-col">
<span className="font-medium">{team.name}</span>
<span className="text-xs text-gray-500">
@@ -8226,7 +8244,7 @@ export function CompetitionManagement() {
})}
placeholder="https://app.example.com"
/>
<p className="text-xs text-gray-500"></p>
<p className="text-xs text-gray-500">APP </p>
</div>
{/* 網站預覽內容 */}
@@ -8574,7 +8592,7 @@ export function CompetitionManagement() {
{/* 照片說明 */}
<Input
placeholder="輸入照片說明..."
value={photo.caption || ''}
value={photo.caption ?? ''}
onChange={(e) => {
const updatedPhotos = newAward.photos.map(p =>
p.id === photo.id ? { ...p, caption: e.target.value } : p
@@ -8925,7 +8943,7 @@ export function CompetitionManagement() {
<div key="production" 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>
<span className="text-sm font-medium text-green-900">APP </span>
</div>
<a
href={links.production}

View File

@@ -1,6 +1,6 @@
"use client"
import { useState } from "react"
import { useState, useEffect } from "react"
import { useCompetition } from "@/contexts/competition-context"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
@@ -58,11 +58,38 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
const [activeTab, setActiveTab] = useState("overview")
const [showPhotoGallery, setShowPhotoGallery] = useState(false)
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0)
const [competitionJudges, setCompetitionJudges] = useState<any[]>([])
const competition = competitions.find((c) => c.id === award.competitionId)
const judgeScores = getJudgeScores(award.id)
const appData = getAppData(award.id)
// 載入競賽評審團資訊
useEffect(() => {
if (open && award.competitionId) {
const loadCompetitionJudges = async () => {
try {
console.log('🔍 載入競賽評審團:', award.competitionId);
const response = await fetch(`/api/competitions/${award.competitionId}/judges`);
const data = await response.json();
if (data.success && data.data && data.data.judges) {
console.log('✅ 獲取到評審團:', data.data.judges.length, '位');
setCompetitionJudges(data.data.judges);
} else {
console.error('❌ 獲取評審團失敗:', data.message);
setCompetitionJudges([]);
}
} catch (error) {
console.error('❌ 載入評審團失敗:', error);
setCompetitionJudges([]);
}
};
loadCompetitionJudges();
}
}, [open, award.competitionId]);
// Competition photos - empty for production
const getCompetitionPhotos = () => {
return []
@@ -224,10 +251,20 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
<strong></strong>
{competition.description}
</p>
<p>
<p className="mb-2">
<strong></strong>
{competition.startDate} ~ {competition.endDate}
</p>
<p>
<strong></strong>
{competitionJudges && competitionJudges.length > 0 ? (
<span className="text-green-700">
{competitionJudges.length}
</span>
) : (
<span className="text-gray-500"></span>
)}
</p>
</div>
</div>
)}
@@ -251,7 +288,7 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
<ExternalLink className="w-5 h-5 text-green-600" />
<div>
<p className="font-medium text-green-800"></p>
<p className="text-xs text-green-600"></p>
<p className="text-xs text-green-600">APP </p>
</div>
</div>
<Button
@@ -407,7 +444,25 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
)
const renderJudgePanel = () => {
if (!competition) return null
if (!competitionJudges || competitionJudges.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Crown className="w-5 h-5 text-purple-500" />
<span></span>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="text-center py-8 text-gray-500">
<Crown className="w-12 h-12 mx-auto mb-4 text-gray-300" />
<p></p>
</div>
</CardContent>
</Card>
)
}
return (
<Card>
@@ -420,30 +475,25 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{competition.judges.map((judgeId) => {
const judge = judges.find((j) => j.id === judgeId)
if (!judge) return null
return (
<div key={judge.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<Avatar>
<AvatarImage src={judge.avatar} />
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
</Avatar>
<div className="flex-1">
<h4 className="font-medium">{judge.name}</h4>
<p className="text-sm text-gray-600">{judge.title}</p>
<div className="flex flex-wrap gap-1 mt-1">
{judge.expertise.slice(0, 2).map((skill) => (
<Badge key={skill} variant="secondary" className="text-xs">
{skill}
</Badge>
))}
</div>
{competitionJudges.map((judge) => (
<div key={judge.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<Avatar>
<AvatarImage src={judge.avatar} />
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
</Avatar>
<div className="flex-1">
<h4 className="font-medium">{judge.name}</h4>
<p className="text-sm text-gray-600">{judge.title}</p>
<div className="flex flex-wrap gap-1 mt-1">
{judge.expertise && judge.expertise.slice(0, 2).map((skill) => (
<Badge key={skill} variant="secondary" className="text-xs">
{skill}
</Badge>
))}
</div>
</div>
)
})}
</div>
))}
</div>
</CardContent>
</Card>

View File

@@ -0,0 +1,27 @@
-- =====================================================
-- 擴展 awards 表結構
-- 添加缺少的欄位以支持完整的獎項功能
-- =====================================================
-- 添加獎項描述欄位
ALTER TABLE `awards`
ADD COLUMN `description` TEXT NULL COMMENT '獎項描述' AFTER `category`;
-- 添加評審評語欄位
ALTER TABLE `awards`
ADD COLUMN `judge_comments` TEXT NULL COMMENT '評審評語' AFTER `description`;
-- 添加應用連結欄位JSON 格式)
ALTER TABLE `awards`
ADD COLUMN `application_links` JSON NULL COMMENT '應用連結' AFTER `judge_comments`;
-- 添加相關文檔欄位JSON 格式)
ALTER TABLE `awards`
ADD COLUMN `documents` JSON NULL COMMENT '相關文檔' AFTER `application_links`;
-- 添加得獎照片欄位JSON 格式)
ALTER TABLE `awards`
ADD COLUMN `photos` JSON NULL COMMENT '得獎照片' AFTER `documents`;
-- 驗證欄位是否添加成功
DESCRIBE `awards`;