實現完整的後台得獎資訊
This commit is contained in:
@@ -134,7 +134,7 @@ export async function GET(request: NextRequest) {
|
|||||||
awards = awards.filter(award => award.month === parseInt(month));
|
awards = awards.filter(award => award.month === parseInt(month));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 JSON 欄位
|
// 解析 JSON 欄位並統一欄位命名
|
||||||
const processedAwards = awards.map(award => {
|
const processedAwards = awards.map(award => {
|
||||||
console.log('🔍 處理獎項:', {
|
console.log('🔍 處理獎項:', {
|
||||||
id: award.id,
|
id: award.id,
|
||||||
@@ -145,7 +145,15 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...award,
|
...award,
|
||||||
application_links: award.application_links ? JSON.parse(award.application_links) : null,
|
// 統一欄位命名:將下劃線命名轉換為駝峰命名
|
||||||
|
competitionId: award.competition_id,
|
||||||
|
competitionName: (award as any).competition_name,
|
||||||
|
competitionType: (award as any).competition_type,
|
||||||
|
awardName: award.award_name,
|
||||||
|
awardType: award.award_type,
|
||||||
|
teamName: award.team_name,
|
||||||
|
appName: award.app_name,
|
||||||
|
applicationLinks: award.application_links ? JSON.parse(award.application_links) : null,
|
||||||
documents: award.documents ? JSON.parse(award.documents) : [],
|
documents: award.documents ? JSON.parse(award.documents) : [],
|
||||||
photos: award.photos ? JSON.parse(award.photos) : [],
|
photos: award.photos ? JSON.parse(award.photos) : [],
|
||||||
};
|
};
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { AwardService } from '@/lib/services/database-service';
|
import { CompetitionService } from '@/lib/services/database-service';
|
||||||
|
|
||||||
// 獲取競賽評審列表
|
// 獲取競賽評審列表
|
||||||
export async function GET(
|
export async function GET(
|
||||||
@@ -22,7 +22,7 @@ export async function GET(
|
|||||||
|
|
||||||
console.log('🔍 獲取競賽評審:', competitionId);
|
console.log('🔍 獲取競賽評審:', competitionId);
|
||||||
|
|
||||||
const judges = await AwardService.getCompetitionJudges(competitionId);
|
const judges = await CompetitionService.getCompetitionJudges(competitionId);
|
||||||
|
|
||||||
console.log('✅ 獲取到評審數量:', judges?.length || 0);
|
console.log('✅ 獲取到評審數量:', judges?.length || 0);
|
||||||
|
|
||||||
|
@@ -9,10 +9,12 @@ import { CompetitionService } from '@/lib/services/database-service';
|
|||||||
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
|
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
|
||||||
try {
|
try {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
console.log('🔍 獲取競賽評審團 API 被調用,競賽ID:', id);
|
||||||
|
|
||||||
// 獲取競賽信息
|
// 獲取競賽信息
|
||||||
const competition = await CompetitionService.getCompetitionWithDetails(id);
|
const competition = await CompetitionService.getCompetitionWithDetails(id);
|
||||||
if (!competition) {
|
if (!competition) {
|
||||||
|
console.log('❌ 競賽不存在:', id);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '競賽不存在',
|
message: '競賽不存在',
|
||||||
@@ -20,14 +22,22 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri
|
|||||||
}, { status: 404 });
|
}, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('✅ 找到競賽:', competition.name);
|
||||||
|
|
||||||
// 獲取競賽的評審團
|
// 獲取競賽的評審團
|
||||||
const judges = await CompetitionService.getCompetitionJudges(id);
|
const judges = await CompetitionService.getCompetitionJudges(id);
|
||||||
|
console.log('📊 查詢到評審數量:', judges.length);
|
||||||
|
console.log('👥 評審詳細資料:', judges);
|
||||||
|
|
||||||
return NextResponse.json({
|
const response = NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '競賽評審團獲取成功',
|
message: '競賽評審團獲取成功',
|
||||||
data: {
|
data: {
|
||||||
competition,
|
competition: {
|
||||||
|
id: competition.id,
|
||||||
|
name: competition.name,
|
||||||
|
type: competition.type
|
||||||
|
},
|
||||||
judges: judges.map(judge => ({
|
judges: judges.map(judge => ({
|
||||||
id: judge.id,
|
id: judge.id,
|
||||||
name: judge.name,
|
name: judge.name,
|
||||||
@@ -43,8 +53,15 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 設置快取控制標頭,防止快取
|
||||||
|
response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
response.headers.set('Pragma', 'no-cache');
|
||||||
|
response.headers.set('Expires', '0');
|
||||||
|
|
||||||
|
return response;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('獲取競賽評審團失敗:', error);
|
console.error('❌ 獲取競賽評審團失敗:', error);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '獲取競賽評審團失敗',
|
message: '獲取競賽評審團失敗',
|
||||||
|
25
app/page.tsx
25
app/page.tsx
@@ -186,6 +186,13 @@ export default function AIShowcasePlatform() {
|
|||||||
const [showAwardDetail, setShowAwardDetail] = useState(false)
|
const [showAwardDetail, setShowAwardDetail] = useState(false)
|
||||||
const [selectedAward, setSelectedAward] = useState<any>(null)
|
const [selectedAward, setSelectedAward] = useState<any>(null)
|
||||||
|
|
||||||
|
// 添加頁面調試資訊
|
||||||
|
console.log('🏠 主頁面渲染:', {
|
||||||
|
awardsCount: awards.length,
|
||||||
|
selectedAward: selectedAward ? selectedAward.id : null,
|
||||||
|
showAwardDetail
|
||||||
|
});
|
||||||
|
|
||||||
// 載入應用數據
|
// 載入應用數據
|
||||||
const loadApps = async () => {
|
const loadApps = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -418,6 +425,14 @@ export default function AIShowcasePlatform() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleShowAwardDetail = (award: any) => {
|
const handleShowAwardDetail = (award: any) => {
|
||||||
|
console.log('🎯 handleShowAwardDetail 被調用:', {
|
||||||
|
award: award ? {
|
||||||
|
id: award.id,
|
||||||
|
competitionId: award.competitionId,
|
||||||
|
awardName: award.awardName,
|
||||||
|
hasCompetitionId: !!award.competitionId
|
||||||
|
} : null
|
||||||
|
});
|
||||||
setSelectedAward(award)
|
setSelectedAward(award)
|
||||||
setShowAwardDetail(true)
|
setShowAwardDetail(true)
|
||||||
}
|
}
|
||||||
@@ -931,7 +946,17 @@ export default function AIShowcasePlatform() {
|
|||||||
|
|
||||||
{/* Award Detail Dialog */}
|
{/* Award Detail Dialog */}
|
||||||
{selectedAward && (
|
{selectedAward && (
|
||||||
|
<>
|
||||||
|
{console.log('🎯 渲染 AwardDetailDialog:', {
|
||||||
|
selectedAward: selectedAward ? {
|
||||||
|
id: selectedAward.id,
|
||||||
|
competitionId: selectedAward.competitionId,
|
||||||
|
awardName: selectedAward.awardName
|
||||||
|
} : null,
|
||||||
|
showAwardDetail
|
||||||
|
})}
|
||||||
<AwardDetailDialog open={showAwardDetail} onOpenChange={setShowAwardDetail} award={selectedAward} />
|
<AwardDetailDialog open={showAwardDetail} onOpenChange={setShowAwardDetail} award={selectedAward} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@@ -103,6 +103,11 @@ export function CompetitionManagement() {
|
|||||||
const [availableUsers, setAvailableUsers] = useState<any[]>([])
|
const [availableUsers, setAvailableUsers] = useState<any[]>([])
|
||||||
const [isLoadingUsers, setIsLoadingUsers] = useState(false)
|
const [isLoadingUsers, setIsLoadingUsers] = useState(false)
|
||||||
|
|
||||||
|
// 獎項資料庫整合狀態
|
||||||
|
const [dbAwards, setDbAwards] = useState<any[]>([])
|
||||||
|
const [isLoadingAwards, setIsLoadingAwards] = useState(false)
|
||||||
|
const [awardStats, setAwardStats] = useState<any>(null)
|
||||||
|
|
||||||
const [showCreateCompetition, setShowCreateCompetition] = useState(false)
|
const [showCreateCompetition, setShowCreateCompetition] = useState(false)
|
||||||
const [showAddJudge, setShowAddJudge] = useState(false)
|
const [showAddJudge, setShowAddJudge] = useState(false)
|
||||||
const [showCreateAward, setShowCreateAward] = useState(false)
|
const [showCreateAward, setShowCreateAward] = useState(false)
|
||||||
@@ -704,6 +709,7 @@ export function CompetitionManagement() {
|
|||||||
fetchTeams()
|
fetchTeams()
|
||||||
fetchTeamStats()
|
fetchTeamStats()
|
||||||
fetchAvailableApps()
|
fetchAvailableApps()
|
||||||
|
loadAwardsData()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 當競賽列表載入完成後,載入每個競賽的評分進度
|
// 當競賽列表載入完成後,載入每個競賽的評分進度
|
||||||
@@ -1980,6 +1986,15 @@ export function CompetitionManagement() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleViewAward = async (award: any) => {
|
const handleViewAward = async (award: any) => {
|
||||||
|
console.log('🎯 handleViewAward 被調用:', {
|
||||||
|
award: award ? {
|
||||||
|
id: award.id,
|
||||||
|
competitionId: award.competitionId,
|
||||||
|
awardName: award.award_name,
|
||||||
|
hasCompetitionId: !!award.competitionId
|
||||||
|
} : null
|
||||||
|
});
|
||||||
|
|
||||||
setSelectedAward(award)
|
setSelectedAward(award)
|
||||||
setShowAwardDetail(true)
|
setShowAwardDetail(true)
|
||||||
|
|
||||||
@@ -1992,6 +2007,7 @@ export function CompetitionManagement() {
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
console.log('✅ 獲取到評審團:', data.data.length, '位');
|
console.log('✅ 獲取到評審團:', data.data.length, '位');
|
||||||
|
console.log('👥 評審團詳細資料:', data.data);
|
||||||
setCompetitionJudges(data.data);
|
setCompetitionJudges(data.data);
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ 獲取評審團失敗:', data.message);
|
console.error('❌ 獲取評審團失敗:', data.message);
|
||||||
@@ -2001,6 +2017,8 @@ export function CompetitionManagement() {
|
|||||||
console.error('❌ 載入評審團失敗:', error);
|
console.error('❌ 載入評審團失敗:', error);
|
||||||
setCompetitionJudges([]);
|
setCompetitionJudges([]);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log('❌ 獎項沒有 competitionId,無法載入評審團');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2080,6 +2098,41 @@ export function CompetitionManagement() {
|
|||||||
setShowManualScoring(true)
|
setShowManualScoring(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 載入獎項資料
|
||||||
|
const loadAwardsData = async () => {
|
||||||
|
setIsLoadingAwards(true)
|
||||||
|
try {
|
||||||
|
console.log('🔍 開始載入獎項資料...')
|
||||||
|
const response = await fetch('/api/admin/awards')
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
console.log('✅ 獎項資料載入成功:', data.data.length, '個獎項')
|
||||||
|
setDbAwards(data.data)
|
||||||
|
|
||||||
|
// 計算獎項統計
|
||||||
|
const stats = {
|
||||||
|
total: data.data.length,
|
||||||
|
top3: data.data.filter((award: any) => award.rank && award.rank <= 3).length,
|
||||||
|
popular: data.data.filter((award: any) => award.award_type === 'popular').length,
|
||||||
|
competitions: [...new Set(data.data.map((award: any) => award.competition_id))].length
|
||||||
|
}
|
||||||
|
setAwardStats(stats)
|
||||||
|
console.log('📊 獎項統計:', stats)
|
||||||
|
} else {
|
||||||
|
console.error('❌ 獎項資料載入失敗:', data.message)
|
||||||
|
setDbAwards([])
|
||||||
|
setAwardStats({ total: 0, top3: 0, popular: 0, competitions: 0 })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 載入獎項資料失敗:', error)
|
||||||
|
setDbAwards([])
|
||||||
|
setAwardStats({ total: 0, top3: 0, popular: 0, competitions: 0 })
|
||||||
|
} finally {
|
||||||
|
setIsLoadingAwards(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 載入競賽相關數據用於評分
|
// 載入競賽相關數據用於評分
|
||||||
const loadCompetitionDataForScoring = async (competition: any) => {
|
const loadCompetitionDataForScoring = async (competition: any) => {
|
||||||
try {
|
try {
|
||||||
@@ -2735,7 +2788,8 @@ export function CompetitionManagement() {
|
|||||||
|
|
||||||
// 获取筛选后的奖项
|
// 获取筛选后的奖项
|
||||||
const getFilteredAwards = () => {
|
const getFilteredAwards = () => {
|
||||||
let filteredAwards = [...awards]
|
// 使用資料庫中的獎項資料,如果沒有則使用 context 中的資料作為備用
|
||||||
|
let filteredAwards = dbAwards.length > 0 ? [...dbAwards] : [...awards]
|
||||||
|
|
||||||
// 搜索功能 - 按应用名称、创作者或奖项名称搜索
|
// 搜索功能 - 按应用名称、创作者或奖项名称搜索
|
||||||
if (awardSearchQuery.trim()) {
|
if (awardSearchQuery.trim()) {
|
||||||
@@ -3888,24 +3942,26 @@ export function CompetitionManagement() {
|
|||||||
{/* 统计信息 */}
|
{/* 统计信息 */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
<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-center p-3 bg-blue-50 rounded-lg">
|
||||||
<div className="text-lg font-bold text-blue-600">{getFilteredAwards().length}</div>
|
<div className="text-lg font-bold text-blue-600">
|
||||||
|
{isLoadingAwards ? '...' : getFilteredAwards().length}
|
||||||
|
</div>
|
||||||
<div className="text-xs text-blue-600">篩選結果</div>
|
<div className="text-xs text-blue-600">篩選結果</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center p-3 bg-yellow-50 rounded-lg">
|
<div className="text-center p-3 bg-yellow-50 rounded-lg">
|
||||||
<div className="text-lg font-bold text-yellow-600">
|
<div className="text-lg font-bold text-yellow-600">
|
||||||
{getFilteredAwards().filter((a) => parseInt(a.rank) > 0 && parseInt(a.rank) <= 3).length}
|
{isLoadingAwards ? '...' : (awardStats?.top3 ?? getFilteredAwards().filter((a) => parseInt(a.rank) > 0 && parseInt(a.rank) <= 3).length)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-yellow-600">前三名獎項</div>
|
<div className="text-xs text-yellow-600">前三名獎項</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center p-3 bg-red-50 rounded-lg">
|
<div className="text-center p-3 bg-red-50 rounded-lg">
|
||||||
<div className="text-lg font-bold text-red-600">
|
<div className="text-lg font-bold text-red-600">
|
||||||
{getFilteredAwards().filter((a) => a.awardType === "popular").length}
|
{isLoadingAwards ? '...' : (awardStats?.popular ?? getFilteredAwards().filter((a) => a.award_type === "popular").length)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-red-600">人氣獎項</div>
|
<div className="text-xs text-red-600">人氣獎項</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center p-3 bg-green-50 rounded-lg">
|
<div className="text-center p-3 bg-green-50 rounded-lg">
|
||||||
<div className="text-lg font-bold text-green-600">
|
<div className="text-lg font-bold text-green-600">
|
||||||
{new Set(getFilteredAwards().map((a) => `${a.year}-${a.month}`)).size}
|
{isLoadingAwards ? '...' : (awardStats?.competitions ?? new Set(getFilteredAwards().map((a) => `${a.year}-${a.month}`)).size)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-green-600">競賽場次</div>
|
<div className="text-xs text-green-600">競賽場次</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -7815,52 +7871,68 @@ export function CompetitionManagement() {
|
|||||||
<Label htmlFor="award-name">
|
<Label htmlFor="award-name">
|
||||||
獎項名稱 <span className="text-red-500">*</span>
|
獎項名稱 <span className="text-red-500">*</span>
|
||||||
</Label>
|
</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
|
<Select
|
||||||
value={newAward.customAwardTypeId ?? newAward.awardType}
|
value={newAward.customAwardTypeId || "custom-input"}
|
||||||
onValueChange={(value: any) => {
|
onValueChange={(value: any) => {
|
||||||
|
if (value === "custom-input") {
|
||||||
|
setNewAward({ ...newAward, customAwardTypeId: "", awardName: "" })
|
||||||
|
} else {
|
||||||
// 檢查是否為自定義獎項類型
|
// 檢查是否為自定義獎項類型
|
||||||
const customAwardType = competitionAwardTypes.find(type => type.id === value)
|
const customAwardType = competitionAwardTypes.find(type => type.id === value)
|
||||||
if (customAwardType) {
|
if (customAwardType) {
|
||||||
setNewAward({
|
setNewAward({
|
||||||
...newAward,
|
...newAward,
|
||||||
awardType: 'custom',
|
|
||||||
customAwardTypeId: value,
|
customAwardTypeId: value,
|
||||||
awardName: customAwardType.name
|
awardName: customAwardType.name
|
||||||
})
|
})
|
||||||
} else {
|
}
|
||||||
setNewAward({ ...newAward, awardType: value, customAwardTypeId: "" })
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={loadingAwardTypes}
|
disabled={loadingAwardTypes}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={loadingAwardTypes ? "載入中..." : "選擇獎項類型"} />
|
<SelectValue placeholder={loadingAwardTypes ? "載入中..." : "選擇獎項名稱或自定義輸入"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{competitionAwardTypes.length > 0 ? (
|
<SelectItem value="custom-input">自定義輸入</SelectItem>
|
||||||
// 顯示競賽自定義的獎項類型
|
{competitionAwardTypes.map((awardType) => (
|
||||||
competitionAwardTypes.map((awardType) => (
|
|
||||||
<SelectItem key={awardType.id} value={awardType.id}>
|
<SelectItem key={awardType.id} value={awardType.id}>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span>{awardType.icon}</span>
|
<span>{awardType.icon}</span>
|
||||||
<span>{awardType.name}</span>
|
<span>{awardType.name}</span>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))
|
))}
|
||||||
) : (
|
</SelectContent>
|
||||||
// 如果沒有自定義獎項類型,顯示預設選項
|
</Select>
|
||||||
<>
|
{!newAward.customAwardTypeId && (
|
||||||
|
<Input
|
||||||
|
id="award-name"
|
||||||
|
value={newAward.awardName}
|
||||||
|
onChange={(e) => setNewAward({ ...newAward, awardName: e.target.value })}
|
||||||
|
placeholder="例如:最佳創新獎、金獎、銀獎、人氣獎"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{competitionAwardTypes.length > 0 && (
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
此競賽有 {competitionAwardTypes.length} 個自定義獎項名稱可選擇
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 獎項類型 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>獎項類型</Label>
|
||||||
|
<Select
|
||||||
|
value={newAward.awardType}
|
||||||
|
onValueChange={(value: any) => {
|
||||||
|
setNewAward({ ...newAward, awardType: value })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="選擇獎項類型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
<SelectItem value="gold">🥇 金獎</SelectItem>
|
<SelectItem value="gold">🥇 金獎</SelectItem>
|
||||||
<SelectItem value="silver">🥈 銀獎</SelectItem>
|
<SelectItem value="silver">🥈 銀獎</SelectItem>
|
||||||
<SelectItem value="bronze">🥉 銅獎</SelectItem>
|
<SelectItem value="bronze">🥉 銅獎</SelectItem>
|
||||||
@@ -7868,15 +7940,8 @@ export function CompetitionManagement() {
|
|||||||
<SelectItem value="innovation">💡 創新獎</SelectItem>
|
<SelectItem value="innovation">💡 創新獎</SelectItem>
|
||||||
<SelectItem value="technical">⚙️ 技術獎</SelectItem>
|
<SelectItem value="technical">⚙️ 技術獎</SelectItem>
|
||||||
<SelectItem value="custom">🏆 自定義獎項</SelectItem>
|
<SelectItem value="custom">🏆 自定義獎項</SelectItem>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{competitionAwardTypes.length > 0 && (
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
此競賽有 {competitionAwardTypes.length} 個自定義獎項類型
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 獎項類別 */}
|
{/* 獎項類別 */}
|
||||||
|
@@ -60,33 +60,95 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
|
|||||||
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0)
|
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0)
|
||||||
const [competitionJudges, setCompetitionJudges] = useState<any[]>([])
|
const [competitionJudges, setCompetitionJudges] = useState<any[]>([])
|
||||||
|
|
||||||
|
// 添加調試資訊
|
||||||
|
console.log('🏆 AwardDetailDialog 渲染:', {
|
||||||
|
open,
|
||||||
|
award: award ? {
|
||||||
|
id: award.id,
|
||||||
|
competitionId: award.competitionId,
|
||||||
|
awardName: award.awardName,
|
||||||
|
hasCompetitionId: !!award.competitionId
|
||||||
|
} : null
|
||||||
|
});
|
||||||
|
|
||||||
const competition = competitions.find((c) => c.id === award.competitionId)
|
const competition = competitions.find((c) => c.id === award.competitionId)
|
||||||
const judgeScores = getJudgeScores(award.id)
|
const judgeScores = getJudgeScores(award.id)
|
||||||
const appData = getAppData(award.id)
|
const appData = getAppData(award.id)
|
||||||
|
|
||||||
// 載入競賽評審團資訊
|
// 載入競賽評審團資訊
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('🔍 useEffect 觸發:', { open, competitionId: award.competitionId, awardId: award.id });
|
||||||
|
|
||||||
if (open && award.competitionId) {
|
if (open && award.competitionId) {
|
||||||
const loadCompetitionJudges = async () => {
|
const loadCompetitionJudges = async (retryCount = 0) => {
|
||||||
try {
|
try {
|
||||||
console.log('🔍 載入競賽評審團:', award.competitionId);
|
console.log('🔍 載入競賽評審團:', award.competitionId, '重試次數:', retryCount);
|
||||||
const response = await fetch(`/api/competitions/${award.competitionId}/judges`);
|
console.log('🏆 獎項資料:', award);
|
||||||
|
|
||||||
|
// 添加時間戳防止快取,並設置快取控制標頭
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const apiUrl = `/api/competitions/${award.competitionId}/judges?t=${timestamp}`;
|
||||||
|
console.log('🌐 API URL:', apiUrl);
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
'Expires': '0'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📡 API 回應狀態:', response.status);
|
||||||
|
console.log('📡 API 回應標頭:', Object.fromEntries(response.headers.entries()));
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('❌ API 錯誤回應:', errorText);
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log('📄 API 回應資料:', JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
if (data.success && data.data && data.data.judges) {
|
if (data.success && data.data && data.data.judges) {
|
||||||
console.log('✅ 獲取到評審團:', data.data.judges.length, '位');
|
console.log('✅ 獲取到評審團:', data.data.judges.length, '位');
|
||||||
|
console.log('👥 評審團詳細資料:', data.data.judges);
|
||||||
setCompetitionJudges(data.data.judges);
|
setCompetitionJudges(data.data.judges);
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ 獲取評審團失敗:', data.message);
|
console.error('❌ 獲取評審團失敗:', data.message || '未知錯誤');
|
||||||
|
console.error('❌ 完整錯誤資料:', data);
|
||||||
|
|
||||||
|
// 如果沒有評審資料且是第一次嘗試,重試一次
|
||||||
|
if (retryCount === 0) {
|
||||||
|
console.log('🔄 重試載入評審團...');
|
||||||
|
setTimeout(() => loadCompetitionJudges(1), 1000);
|
||||||
|
} else {
|
||||||
setCompetitionJudges([]);
|
setCompetitionJudges([]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 載入評審團失敗:', error);
|
console.error('❌ 載入評審團失敗:', error);
|
||||||
|
|
||||||
|
// 如果是網路錯誤且是第一次嘗試,重試一次
|
||||||
|
if (retryCount === 0) {
|
||||||
|
console.log('🔄 網路錯誤,重試載入評審團...');
|
||||||
|
setTimeout(() => loadCompetitionJudges(1), 2000);
|
||||||
|
} else {
|
||||||
setCompetitionJudges([]);
|
setCompetitionJudges([]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 清空之前的評審資料,確保重新載入
|
||||||
|
setCompetitionJudges([]);
|
||||||
loadCompetitionJudges();
|
loadCompetitionJudges();
|
||||||
|
} else {
|
||||||
|
console.log('❌ useEffect 條件不滿足:', {
|
||||||
|
open,
|
||||||
|
competitionId: award.competitionId,
|
||||||
|
hasCompetitionId: !!award.competitionId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [open, award.competitionId]);
|
}, [open, award.competitionId]);
|
||||||
|
|
||||||
|
@@ -287,7 +287,7 @@ export function PopularityRankings() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={app.id}
|
key={`app-${app.id}-${index}`}
|
||||||
className="hover:shadow-lg transition-all duration-300 bg-gradient-to-br from-yellow-50 to-orange-50 border border-yellow-200 flex flex-col"
|
className="hover:shadow-lg transition-all duration-300 bg-gradient-to-br from-yellow-50 to-orange-50 border border-yellow-200 flex flex-col"
|
||||||
>
|
>
|
||||||
<CardContent className="p-4 flex flex-col flex-1">
|
<CardContent className="p-4 flex flex-col flex-1">
|
||||||
@@ -454,7 +454,7 @@ export function PopularityRankings() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={team.id}
|
key={`team-${team.id}-${index}`}
|
||||||
className="hover:shadow-lg transition-all duration-300 bg-gradient-to-br from-green-50 to-blue-50 border border-green-200 flex flex-col"
|
className="hover:shadow-lg transition-all duration-300 bg-gradient-to-br from-green-50 to-blue-50 border border-green-200 flex flex-col"
|
||||||
>
|
>
|
||||||
<CardContent className="p-4 flex flex-col flex-1">
|
<CardContent className="p-4 flex flex-col flex-1">
|
||||||
@@ -487,8 +487,8 @@ export function PopularityRankings() {
|
|||||||
<div className="mb-4 flex-1">
|
<div className="mb-4 flex-1">
|
||||||
<h5 className="text-sm font-medium text-gray-700 mb-2">團隊成員 ({team.members.length}人)</h5>
|
<h5 className="text-sm font-medium text-gray-700 mb-2">團隊成員 ({team.members.length}人)</h5>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{team.members.slice(0, 3).map((member: any) => (
|
{team.members.slice(0, 3).map((member: any, memberIndex: number) => (
|
||||||
<div key={member.id} className="flex items-center space-x-2 text-xs">
|
<div key={`member-${member.id}-${memberIndex}`} className="flex items-center space-x-2 text-xs">
|
||||||
<div className="w-6 h-6 bg-green-100 rounded-full flex items-center justify-center text-green-700 font-medium">
|
<div className="w-6 h-6 bg-green-100 rounded-full flex items-center justify-center text-green-700 font-medium">
|
||||||
{member.name[0]}
|
{member.name[0]}
|
||||||
</div>
|
</div>
|
||||||
@@ -687,8 +687,8 @@ export function PopularityRankings() {
|
|||||||
</div>
|
</div>
|
||||||
) : competitionJudges.length > 0 ? (
|
) : competitionJudges.length > 0 ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{competitionJudges.map((judge) => (
|
{competitionJudges.map((judge, judgeIndex) => (
|
||||||
<div key={judge.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
<div key={`judge-${judge.id}-${judgeIndex}`} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={judge.avatar} />
|
<AvatarImage src={judge.avatar} />
|
||||||
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
|
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
|
||||||
@@ -697,8 +697,8 @@ export function PopularityRankings() {
|
|||||||
<h4 className="font-medium">{judge.name}</h4>
|
<h4 className="font-medium">{judge.name}</h4>
|
||||||
<p className="text-sm text-gray-600">{judge.title}</p>
|
<p className="text-sm text-gray-600">{judge.title}</p>
|
||||||
<div className="flex flex-wrap gap-1 mt-1">
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
{judge.expertise.slice(0, 2).map((skill) => (
|
{judge.expertise.slice(0, 2).map((skill, skillIndex) => (
|
||||||
<Badge key={skill} variant="secondary" className="text-xs">
|
<Badge key={`skill-${skill}-${skillIndex}`} variant="secondary" className="text-xs">
|
||||||
{skill}
|
{skill}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
|
@@ -1670,17 +1670,6 @@ export class CompetitionService extends DatabaseServiceBase {
|
|||||||
// 競賽關聯數據管理方法
|
// 競賽關聯數據管理方法
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
// 獲取競賽的評審列表
|
|
||||||
static async getCompetitionJudges(competitionId: string): Promise<any[]> {
|
|
||||||
const sql = `
|
|
||||||
SELECT j.*, cj.assigned_at
|
|
||||||
FROM competition_judges cj
|
|
||||||
JOIN judges j ON cj.judge_id = j.id
|
|
||||||
WHERE cj.competition_id = ? AND j.is_active = 1
|
|
||||||
ORDER BY cj.assigned_at ASC
|
|
||||||
`;
|
|
||||||
return await DatabaseServiceBase.safeQuery(sql, [competitionId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 為競賽添加評審
|
// 為競賽添加評審
|
||||||
static async addCompetitionJudges(competitionId: string, judgeIds: string[]): Promise<boolean> {
|
static async addCompetitionJudges(competitionId: string, judgeIds: string[]): Promise<boolean> {
|
||||||
@@ -2080,6 +2069,63 @@ export class CompetitionService extends DatabaseServiceBase {
|
|||||||
rules
|
rules
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根據競賽ID獲取評審信息
|
||||||
|
static async getCompetitionJudges(competitionId: string): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
console.log('🔍 查詢競賽評審:', competitionId);
|
||||||
|
|
||||||
|
// 先檢查 competition_judges 表是否有資料
|
||||||
|
const checkSql = `SELECT COUNT(*) as count FROM competition_judges WHERE competition_id = ?`;
|
||||||
|
const checkResult = await db.query<any>(checkSql, [competitionId]);
|
||||||
|
console.log('📊 competition_judges 表中該競賽的記錄數:', checkResult[0]?.count || 0);
|
||||||
|
|
||||||
|
// 檢查 judges 表是否有資料
|
||||||
|
const judgesCountSql = `SELECT COUNT(*) as count FROM judges WHERE is_active = TRUE`;
|
||||||
|
const judgesCountResult = await db.query<any>(judgesCountSql, []);
|
||||||
|
console.log('📊 judges 表中活躍評審數:', judgesCountResult[0]?.count || 0);
|
||||||
|
|
||||||
|
// 檢查關聯查詢
|
||||||
|
const joinCheckSql = `
|
||||||
|
SELECT
|
||||||
|
cj.competition_id,
|
||||||
|
cj.judge_id,
|
||||||
|
j.id as judge_table_id,
|
||||||
|
j.name,
|
||||||
|
j.is_active
|
||||||
|
FROM competition_judges cj
|
||||||
|
LEFT JOIN judges j ON cj.judge_id = j.id
|
||||||
|
WHERE cj.competition_id = ?
|
||||||
|
`;
|
||||||
|
const joinResult = await db.query<any>(joinCheckSql, [competitionId]);
|
||||||
|
console.log('🔗 關聯查詢結果:', joinResult);
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
j.id,
|
||||||
|
j.name,
|
||||||
|
j.title,
|
||||||
|
j.department,
|
||||||
|
j.expertise,
|
||||||
|
j.avatar,
|
||||||
|
cj.assigned_at
|
||||||
|
FROM competition_judges cj
|
||||||
|
LEFT JOIN judges j ON cj.judge_id = j.id
|
||||||
|
WHERE cj.competition_id = ? AND j.is_active = TRUE
|
||||||
|
ORDER BY j.name
|
||||||
|
`;
|
||||||
|
console.log('📝 執行評審查詢SQL:', sql);
|
||||||
|
console.log('📝 查詢參數:', [competitionId]);
|
||||||
|
|
||||||
|
const result = await db.query<any>(sql, [competitionId]);
|
||||||
|
console.log('✅ 查詢評審結果:', result?.length || 0, '位評審');
|
||||||
|
console.log('👥 評審詳細結果:', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 查詢評審失敗:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
@@ -4569,33 +4615,6 @@ export class AwardService extends DatabaseServiceBase {
|
|||||||
return await db.queryOne<Award>(sql, [id]);
|
return await db.queryOne<Award>(sql, [id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根據競賽ID獲取評審信息
|
|
||||||
static async getCompetitionJudges(competitionId: string): Promise<any[]> {
|
|
||||||
try {
|
|
||||||
console.log('🔍 查詢競賽評審:', competitionId);
|
|
||||||
const sql = `
|
|
||||||
SELECT
|
|
||||||
j.id,
|
|
||||||
j.name,
|
|
||||||
j.title,
|
|
||||||
j.department,
|
|
||||||
j.expertise,
|
|
||||||
j.avatar
|
|
||||||
FROM competition_judges cj
|
|
||||||
LEFT JOIN judges j ON cj.judge_id = j.id
|
|
||||||
WHERE cj.competition_id = ? AND j.is_active = TRUE
|
|
||||||
ORDER BY j.name
|
|
||||||
`;
|
|
||||||
console.log('📝 執行評審查詢SQL:', sql);
|
|
||||||
const result = await db.query<any>(sql, [competitionId]);
|
|
||||||
console.log('✅ 查詢評審結果:', result?.length || 0, '位評審');
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 查詢評審失敗:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新獎項
|
// 更新獎項
|
||||||
static async updateAward(id: string, updates: Partial<Award>): Promise<boolean> {
|
static async updateAward(id: string, updates: Partial<Award>): Promise<boolean> {
|
||||||
const fields = Object.keys(updates).filter(key =>
|
const fields = Object.keys(updates).filter(key =>
|
||||||
@@ -4664,11 +4683,6 @@ export class AwardService extends DatabaseServiceBase {
|
|||||||
return await db.query<Award>(sql, [year]);
|
return await db.query<Award>(sql, [year]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根據競賽獲取獎項
|
|
||||||
static async getAwardsByCompetition(competitionId: string): Promise<Award[]> {
|
|
||||||
const sql = 'SELECT * FROM awards WHERE competition_id = ? ORDER BY rank ASC';
|
|
||||||
return await db.query<Award>(sql, [competitionId]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
Reference in New Issue
Block a user