新增得獎更新、刪除的功能
This commit is contained in:
145
app/api/admin/awards/[id]/route.ts
Normal file
145
app/api/admin/awards/[id]/route.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 獎項編輯和刪除 API
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { AwardService } from '@/lib/services/database-service';
|
||||||
|
|
||||||
|
// 編輯獎項
|
||||||
|
export async function PUT(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { id } = await params;
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
// 驗證必填欄位
|
||||||
|
if (!body.award_name || !body.creator) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: '缺少必填欄位',
|
||||||
|
error: 'award_name, creator 為必填欄位'
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 驗證獎項類型
|
||||||
|
const validAwardTypes = ['gold', 'silver', 'bronze', 'popular', 'innovation', 'technical', 'custom'];
|
||||||
|
if (body.award_type && !validAwardTypes.includes(body.award_type)) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: '無效的獎項類型',
|
||||||
|
error: `award_type 必須是以下之一: ${validAwardTypes.join(', ')}`
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 驗證競賽類型
|
||||||
|
const validCompetitionTypes = ['individual', 'team', 'proposal'];
|
||||||
|
if (body.competition_type && !validCompetitionTypes.includes(body.competition_type)) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: '無效的競賽類型',
|
||||||
|
error: `competition_type 必須是以下之一: ${validCompetitionTypes.join(', ')}`
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 驗證獎項類別
|
||||||
|
const validCategories = ['innovation', 'technical', 'practical', 'popular', 'teamwork', 'solution', 'creativity'];
|
||||||
|
if (body.category && !validCategories.includes(body.category)) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: '無效的獎項類別',
|
||||||
|
error: `category 必須是以下之一: ${validCategories.join(', ')}`
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 準備更新資料
|
||||||
|
const updateData = {
|
||||||
|
competition_id: body.competition_id,
|
||||||
|
app_id: body.app_id || null,
|
||||||
|
team_id: body.team_id || null,
|
||||||
|
proposal_id: body.proposal_id || null,
|
||||||
|
app_name: body.app_name || null,
|
||||||
|
team_name: body.team_name || null,
|
||||||
|
proposal_title: body.proposal_title || null,
|
||||||
|
creator: body.creator,
|
||||||
|
award_type: body.award_type,
|
||||||
|
award_name: body.award_name,
|
||||||
|
score: parseFloat(body.score) || 0,
|
||||||
|
year: parseInt(body.year) || new Date().getFullYear(),
|
||||||
|
month: parseInt(body.month) || new Date().getMonth() + 1,
|
||||||
|
icon: body.icon || '🏆',
|
||||||
|
custom_award_type_id: body.custom_award_type_id || null,
|
||||||
|
competition_type: body.competition_type,
|
||||||
|
rank: parseInt(body.rank) || 0,
|
||||||
|
category: body.category,
|
||||||
|
description: body.description || null,
|
||||||
|
judge_comments: body.judge_comments || null,
|
||||||
|
application_links: body.application_links ? JSON.stringify(body.application_links) : null,
|
||||||
|
documents: body.documents ? JSON.stringify(body.documents) : null,
|
||||||
|
photos: body.photos ? JSON.stringify(body.photos) : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新獎項
|
||||||
|
const success = await AwardService.updateAward(id, updateData);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// 獲取更新後的獎項資料
|
||||||
|
const updatedAward = await AwardService.getAwardById(id);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '獎項更新成功',
|
||||||
|
data: updatedAward
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: '獎項更新失敗',
|
||||||
|
error: '找不到指定的獎項或更新失敗'
|
||||||
|
}, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新獎項失敗:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: '更新獎項失敗',
|
||||||
|
error: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刪除獎項
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
// 刪除獎項
|
||||||
|
const success = await AwardService.deleteAward(id);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '獎項刪除成功'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: '獎項刪除失敗',
|
||||||
|
error: '找不到指定的獎項或刪除失敗'
|
||||||
|
}, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刪除獎項失敗:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: '刪除獎項失敗',
|
||||||
|
error: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
@@ -160,9 +160,45 @@ export async function GET(request: NextRequest) {
|
|||||||
awardType: award.award_type,
|
awardType: award.award_type,
|
||||||
teamName: (award as any).team_name_from_teams || award.team_name,
|
teamName: (award as any).team_name_from_teams || award.team_name,
|
||||||
appName: award.app_name,
|
appName: award.app_name,
|
||||||
applicationLinks: (award as any).application_links ? JSON.parse((award as any).application_links) : null,
|
applicationLinks: (() => {
|
||||||
documents: (award as any).documents ? JSON.parse((award as any).documents) : [],
|
const links = (award as any).application_links;
|
||||||
photos: (award as any).photos ? JSON.parse((award as any).photos) : [],
|
if (!links) return null;
|
||||||
|
if (typeof links === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(links);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('解析 application_links JSON 失敗:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return links; // 已經是對象
|
||||||
|
})(),
|
||||||
|
documents: (() => {
|
||||||
|
const docs = (award as any).documents;
|
||||||
|
if (!docs) return [];
|
||||||
|
if (typeof docs === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(docs);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('解析 documents JSON 失敗:', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return docs; // 已經是數組
|
||||||
|
})(),
|
||||||
|
photos: (() => {
|
||||||
|
const pics = (award as any).photos;
|
||||||
|
if (!pics) return [];
|
||||||
|
if (typeof pics === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(pics);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('解析 photos JSON 失敗:', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pics; // 已經是數組
|
||||||
|
})(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -71,6 +71,8 @@ export function CompetitionManagement() {
|
|||||||
deleteJudge,
|
deleteJudge,
|
||||||
awards,
|
awards,
|
||||||
addAward,
|
addAward,
|
||||||
|
updateAward,
|
||||||
|
deleteAward,
|
||||||
getAppDetailedScores,
|
getAppDetailedScores,
|
||||||
judgeScores,
|
judgeScores,
|
||||||
getAppJudgeScores,
|
getAppJudgeScores,
|
||||||
@@ -1081,9 +1083,11 @@ export function CompetitionManagement() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (judgesData.success) {
|
if (judgesData.success) {
|
||||||
|
console.log('✅ 載入評審成功:', judgesData.data?.length || 0, '位評審')
|
||||||
|
console.log('👥 評審詳細資料:', judgesData.data)
|
||||||
setCompetitionJudges(judgesData.data)
|
setCompetitionJudges(judgesData.data)
|
||||||
} else {
|
} else {
|
||||||
console.error('載入評審失敗:', judgesData.message)
|
console.error('❌ 載入評審失敗:', judgesData.message)
|
||||||
setCompetitionJudges([])
|
setCompetitionJudges([])
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1870,7 +1874,7 @@ export function CompetitionManagement() {
|
|||||||
|
|
||||||
// 根據獎項類型設定圖標
|
// 根據獎項類型設定圖標
|
||||||
let icon = "🏆"
|
let icon = "🏆"
|
||||||
switch (newAward.awardType) {
|
switch (newAward.awardType as any) {
|
||||||
case "gold": icon = "🥇"; break;
|
case "gold": icon = "🥇"; break;
|
||||||
case "silver": icon = "🥈"; break;
|
case "silver": icon = "🥈"; break;
|
||||||
case "bronze": icon = "🥉"; break;
|
case "bronze": icon = "🥉"; break;
|
||||||
@@ -1908,9 +1912,14 @@ export function CompetitionManagement() {
|
|||||||
|
|
||||||
console.log('📝 準備獎項數據:', awardData)
|
console.log('📝 準備獎項數據:', awardData)
|
||||||
|
|
||||||
// 調用 API 創建獎項
|
// 判斷是創建還是編輯模式
|
||||||
const response = await fetch('/api/admin/awards', {
|
const isEditMode = selectedAward && selectedAward.id
|
||||||
method: 'POST',
|
const apiUrl = isEditMode ? `/api/admin/awards/${selectedAward.id}` : '/api/admin/awards'
|
||||||
|
const method = isEditMode ? 'PUT' : 'POST'
|
||||||
|
|
||||||
|
// 調用 API 創建或更新獎項
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
@@ -1920,36 +1929,38 @@ export function CompetitionManagement() {
|
|||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(result.message || '創建獎項失敗')
|
throw new Error(result.message || (isEditMode ? '更新獎項失敗' : '創建獎項失敗'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 創建成功,添加到本地列表
|
// 創建或更新成功,處理本地列表
|
||||||
const newAwardItem = {
|
const awardItem = {
|
||||||
id: result.data.id,
|
id: result.data.id || selectedAward.id,
|
||||||
competitionId: newAward.competitionId,
|
competitionId: newAward.competitionId,
|
||||||
competitionName: competition.name,
|
appId: actualParticipantType === "individual" ? newAward.participantId : (participant?.app_id || null),
|
||||||
participantId: newAward.participantId,
|
teamId: actualParticipantType === "team" ? newAward.participantId : null,
|
||||||
participantType: newAward.participantType,
|
appName: actualParticipantType === "individual" ? participantName : (participant?.app_name || null),
|
||||||
participantName: participantName,
|
teamName: actualParticipantType === "team" ? participantName : null,
|
||||||
creatorName: creatorName,
|
creator: creatorName,
|
||||||
awardType: newAward.awardType,
|
awardType: newAward.awardType as any,
|
||||||
awardName: newAward.awardName,
|
awardName: newAward.awardName,
|
||||||
customAwardTypeId: newAward.customAwardTypeId,
|
|
||||||
score: newAward.score,
|
score: newAward.score,
|
||||||
rank: newAward.rank,
|
|
||||||
category: newAward.category,
|
|
||||||
description: newAward.description,
|
|
||||||
judgeComments: newAward.judgeComments,
|
|
||||||
applicationLinks: newAward.applicationLinks,
|
|
||||||
documents: newAward.documents,
|
|
||||||
photos: newAward.photos,
|
|
||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
month: new Date().getMonth() + 1,
|
month: new Date().getMonth() + 1,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
createdAt: new Date().toISOString(),
|
customAwardTypeId: newAward.customAwardTypeId,
|
||||||
|
competitionType: competition.type as any,
|
||||||
|
rank: newAward.rank,
|
||||||
|
category: newAward.category as any,
|
||||||
}
|
}
|
||||||
|
|
||||||
addAward(newAwardItem)
|
if (isEditMode) {
|
||||||
|
// 編輯模式:更新現有獎項
|
||||||
|
updateAward(awardItem)
|
||||||
|
} else {
|
||||||
|
// 創建模式:添加新獎項
|
||||||
|
const { id, ...awardWithoutId } = awardItem
|
||||||
|
addAward(awardWithoutId)
|
||||||
|
}
|
||||||
|
|
||||||
// 重置表單
|
// 重置表單
|
||||||
setNewAward({
|
setNewAward({
|
||||||
@@ -1974,7 +1985,8 @@ export function CompetitionManagement() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
setShowCreateAward(false)
|
setShowCreateAward(false)
|
||||||
setSuccess("獎項創建成功!")
|
setSelectedAward(null)
|
||||||
|
setSuccess(isEditMode ? "獎項更新成功!" : "獎項創建成功!")
|
||||||
setTimeout(() => setSuccess(""), 3000)
|
setTimeout(() => setSuccess(""), 3000)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1991,7 +2003,10 @@ export function CompetitionManagement() {
|
|||||||
id: award.id,
|
id: award.id,
|
||||||
competitionId: award.competitionId,
|
competitionId: award.competitionId,
|
||||||
awardName: award.award_name,
|
awardName: award.award_name,
|
||||||
hasCompetitionId: !!award.competitionId
|
hasCompetitionId: !!award.competitionId,
|
||||||
|
photos: award.photos,
|
||||||
|
photosType: typeof award.photos,
|
||||||
|
photosLength: award.photos?.length
|
||||||
} : null
|
} : null
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2022,7 +2037,8 @@ export function CompetitionManagement() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditAward = (award: any) => {
|
const handleEditAward = async (award: any) => {
|
||||||
|
console.log('🎯 開始編輯獎項:', award)
|
||||||
setSelectedAward(award)
|
setSelectedAward(award)
|
||||||
setNewAward({
|
setNewAward({
|
||||||
competitionId: award.competitionId,
|
competitionId: award.competitionId,
|
||||||
@@ -2044,6 +2060,16 @@ export function CompetitionManagement() {
|
|||||||
judgeComments: (award as any).judgeComments || "",
|
judgeComments: (award as any).judgeComments || "",
|
||||||
photos: (award as any).photos || [],
|
photos: (award as any).photos || [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 載入對應競賽的評審和參賽者資料
|
||||||
|
if (award.competitionId) {
|
||||||
|
console.log('🔄 載入競賽資料,競賽ID:', award.competitionId)
|
||||||
|
await loadCompetitionParticipants(award.competitionId)
|
||||||
|
console.log('✅ 載入完成,評審數量:', competitionJudges.length)
|
||||||
|
} else {
|
||||||
|
console.log('❌ 獎項沒有 competitionId')
|
||||||
|
}
|
||||||
|
|
||||||
setShowCreateAward(true)
|
setShowCreateAward(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2056,16 +2082,36 @@ export function CompetitionManagement() {
|
|||||||
if (!awardToDelete) return
|
if (!awardToDelete) return
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
||||||
|
|
||||||
// 這裡應該調用 context 中的刪除函數
|
try {
|
||||||
// deleteAward(awardToDelete.id)
|
// 調用 API 刪除獎項
|
||||||
|
const response = await fetch(`/api/admin/awards/${awardToDelete.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.message || '刪除獎項失敗')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刪除成功,從本地列表移除
|
||||||
|
deleteAward(awardToDelete.id)
|
||||||
|
|
||||||
setShowDeleteAwardConfirm(false)
|
setShowDeleteAwardConfirm(false)
|
||||||
setAwardToDelete(null)
|
setAwardToDelete(null)
|
||||||
setSuccess("獎項刪除成功!")
|
setSuccess("獎項刪除成功!")
|
||||||
setIsLoading(false)
|
|
||||||
setTimeout(() => setSuccess(""), 3000)
|
setTimeout(() => setSuccess(""), 3000)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刪除獎項失敗:', error)
|
||||||
|
setError(error instanceof Error ? error.message : '刪除獎項失敗')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleManualScoring = async (competition: any) => {
|
const handleManualScoring = async (competition: any) => {
|
||||||
@@ -4048,7 +4094,7 @@ export function CompetitionManagement() {
|
|||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 min-h-[400px]">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 min-h-[400px]">
|
||||||
{paginatedAwards.map((award: any) => (
|
{paginatedAwards.map((award: any) => (
|
||||||
<Card key={award.id} className="relative overflow-hidden hover:shadow-lg transition-shadow h-[280px] flex flex-col">
|
<Card key={award.id} className="relative overflow-hidden hover:shadow-lg transition-shadow flex flex-col">
|
||||||
<div className="absolute top-4 right-4 text-2xl">{award.icon}</div>
|
<div className="absolute top-4 right-4 text-2xl">{award.icon}</div>
|
||||||
<CardContent className="p-4 flex-grow flex flex-col justify-between">
|
<CardContent className="p-4 flex-grow flex flex-col justify-between">
|
||||||
<div className="space-y-3 flex-grow">
|
<div className="space-y-3 flex-grow">
|
||||||
@@ -9127,36 +9173,63 @@ export function CompetitionManagement() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 得獎照片 */}
|
{/* 得獎照片 */}
|
||||||
{(selectedAward as any).photos && (selectedAward as any).photos.length > 0 && (
|
{(() => {
|
||||||
|
const photos = (selectedAward as any)?.photos;
|
||||||
|
console.log('🖼️ 檢查照片資料:', {
|
||||||
|
hasPhotos: !!photos,
|
||||||
|
photosType: typeof photos,
|
||||||
|
photosLength: photos?.length,
|
||||||
|
photosData: photos
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!photos || !Array.isArray(photos) || photos.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-lg font-semibold text-gray-900">得獎照片</h4>
|
<h4 className="text-lg font-semibold text-gray-900">得獎照片</h4>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{(selectedAward as any).photos.map((photo: any) => (
|
{photos.map((photo: any, index: number) => {
|
||||||
<div key={photo.id} className="space-y-2">
|
console.log('📸 處理照片:', { index, photo });
|
||||||
|
return (
|
||||||
|
<div key={photo.id || photo.url || index} className="space-y-2">
|
||||||
<div className="aspect-video bg-gray-100 rounded-lg border overflow-hidden">
|
<div className="aspect-video bg-gray-100 rounded-lg border overflow-hidden">
|
||||||
{photo.url ? (
|
{photo.url ? (
|
||||||
<img
|
<img
|
||||||
src={photo.url}
|
src={photo.url}
|
||||||
alt={photo.caption || "得獎照片"}
|
alt={photo.caption || photo.name || "得獎照片"}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
|
console.log('❌ 圖片載入失敗:', photo.url);
|
||||||
e.currentTarget.style.display = 'none';
|
e.currentTarget.style.display = 'none';
|
||||||
e.currentTarget.nextElementSibling?.classList.remove('hidden');
|
e.currentTarget.nextElementSibling?.classList.remove('hidden');
|
||||||
}}
|
}}
|
||||||
|
onLoad={() => {
|
||||||
|
console.log('✅ 圖片載入成功:', photo.url);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : (
|
||||||
|
<div className="w-full h-full bg-gray-200 flex items-center justify-center text-2xl">
|
||||||
|
🖼️
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="w-full h-full bg-gray-200 flex items-center justify-center text-2xl hidden">
|
<div className="w-full h-full bg-gray-200 flex items-center justify-center text-2xl hidden">
|
||||||
🖼️
|
🖼️
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{photo.caption && (
|
{(photo.caption || photo.name) && (
|
||||||
<p className="text-xs text-gray-600 text-center">{photo.caption}</p>
|
<p className="text-xs text-gray-600 text-center">
|
||||||
|
{photo.caption || photo.name}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3 pt-4 border-t">
|
<div className="flex justify-end space-x-3 pt-4 border-t">
|
||||||
<Button variant="outline" onClick={() => setShowAwardDetail(false)}>
|
<Button variant="outline" onClick={() => setShowAwardDetail(false)}>
|
||||||
|
@@ -82,20 +82,14 @@ export function ScoringManagement() {
|
|||||||
const [isLoadingRules, setIsLoadingRules] = useState(false)
|
const [isLoadingRules, setIsLoadingRules] = useState(false)
|
||||||
|
|
||||||
|
|
||||||
// 調試:檢查競賽數據
|
|
||||||
console.log('📋 競賽數據:', competitions)
|
|
||||||
console.log('👨⚖️ 評審數據:', judges)
|
|
||||||
console.log('📊 競賽數量:', competitions?.length || 0)
|
|
||||||
|
|
||||||
// 檢查初始載入狀態
|
// 檢查初始載入狀態
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (competitions && competitions.length > 0) {
|
if (competitions && competitions.length > 0) {
|
||||||
console.log('✅ 競賽數據已載入,關閉初始載入狀態')
|
|
||||||
setIsInitialLoading(false)
|
setIsInitialLoading(false)
|
||||||
|
|
||||||
// 自動選擇第一個競賽(如果沒有選中的話)
|
// 自動選擇第一個競賽(如果沒有選中的話)
|
||||||
if (!selectedCompetition) {
|
if (!selectedCompetition) {
|
||||||
console.log('🎯 自動選擇第一個競賽:', competitions[0].name)
|
|
||||||
setSelectedCompetition(competitions[0])
|
setSelectedCompetition(competitions[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,7 +170,6 @@ export function ScoringManagement() {
|
|||||||
setError('載入評分數據失敗')
|
setError('載入評分數據失敗')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('載入評分數據失敗:', err)
|
|
||||||
setError('載入評分數據失敗')
|
setError('載入評分數據失敗')
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@@ -376,7 +369,6 @@ export function ScoringManagement() {
|
|||||||
|
|
||||||
// 根據參賽者類型確定participantType
|
// 根據參賽者類型確定participantType
|
||||||
const selectedParticipant = competitionParticipants.find(p => p.id === manualScoring.participantId)
|
const selectedParticipant = competitionParticipants.find(p => p.id === manualScoring.participantId)
|
||||||
console.log('🔍 選中的參賽者:', selectedParticipant);
|
|
||||||
|
|
||||||
// 由於所有參賽者都是團隊的 app,所以 participantType 應該是 'app'
|
// 由於所有參賽者都是團隊的 app,所以 participantType 應該是 'app'
|
||||||
const participantType = 'app'
|
const participantType = 'app'
|
||||||
@@ -392,7 +384,6 @@ export function ScoringManagement() {
|
|||||||
recordId: selectedRecord?.id // 編輯時的記錄ID
|
recordId: selectedRecord?.id // 編輯時的記錄ID
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔍 提交評分請求數據:', requestData);
|
|
||||||
|
|
||||||
const response = await fetch('/api/admin/scoring', {
|
const response = await fetch('/api/admin/scoring', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -403,7 +394,6 @@ export function ScoringManagement() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
console.log('🔍 API 回應:', data);
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setSuccess(showEditScoring ? "評分更新成功!" : "評分提交成功!")
|
setSuccess(showEditScoring ? "評分更新成功!" : "評分提交成功!")
|
||||||
@@ -444,7 +434,6 @@ export function ScoringManagement() {
|
|||||||
setError(data.message || "評分提交失敗")
|
setError(data.message || "評分提交失敗")
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('評分提交失敗:', err)
|
|
||||||
setError("評分提交失敗,請重試")
|
setError("評分提交失敗,請重試")
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@@ -479,7 +468,6 @@ export function ScoringManagement() {
|
|||||||
setScoringStats(data.data)
|
setScoringStats(data.data)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('載入評分統計失敗:', err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,33 +484,24 @@ export function ScoringManagement() {
|
|||||||
const loadCompetitionData = async () => {
|
const loadCompetitionData = async () => {
|
||||||
if (!selectedCompetition) return
|
if (!selectedCompetition) return
|
||||||
|
|
||||||
console.log('🔍 開始載入競賽數據,競賽ID:', selectedCompetition.id)
|
|
||||||
setIsLoadingData(true)
|
setIsLoadingData(true)
|
||||||
setError("")
|
setError("")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 載入競賽評審
|
// 載入競賽評審
|
||||||
console.log('📋 載入競賽評審...')
|
|
||||||
const judgesResponse = await fetch(`/api/competitions/${selectedCompetition.id}/judges`)
|
const judgesResponse = await fetch(`/api/competitions/${selectedCompetition.id}/judges`)
|
||||||
const judgesData = await judgesResponse.json()
|
const judgesData = await judgesResponse.json()
|
||||||
|
|
||||||
console.log('評審API回應:', judgesData)
|
|
||||||
|
|
||||||
if (judgesData.success && judgesData.data && judgesData.data.judges) {
|
if (judgesData.success && judgesData.data && judgesData.data.judges) {
|
||||||
setCompetitionJudges(judgesData.data.judges)
|
setCompetitionJudges(judgesData.data.judges)
|
||||||
console.log('✅ 評審數據載入成功:', judgesData.data.judges.length, '個評審')
|
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ 評審數據載入失敗:', judgesData.message || 'API回應格式錯誤')
|
|
||||||
setCompetitionJudges([])
|
setCompetitionJudges([])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用統一的評分進度API來獲取準確的參賽者數量
|
// 使用統一的評分進度API來獲取準確的參賽者數量
|
||||||
console.log('📱 載入競賽參賽者(使用統一計算邏輯)...')
|
|
||||||
const scoringProgressResponse = await fetch(`/api/competitions/scoring-progress?competitionId=${selectedCompetition.id}`)
|
const scoringProgressResponse = await fetch(`/api/competitions/scoring-progress?competitionId=${selectedCompetition.id}`)
|
||||||
const scoringProgressData = await scoringProgressResponse.json()
|
const scoringProgressData = await scoringProgressResponse.json()
|
||||||
|
|
||||||
console.log('評分進度API回應:', scoringProgressData)
|
|
||||||
|
|
||||||
let participants = []
|
let participants = []
|
||||||
|
|
||||||
if (scoringProgressData.success && scoringProgressData.data) {
|
if (scoringProgressData.success && scoringProgressData.data) {
|
||||||
@@ -533,7 +512,6 @@ export function ScoringManagement() {
|
|||||||
const appsResponse = await fetch(`/api/competitions/${selectedCompetition.id}/apps`)
|
const appsResponse = await fetch(`/api/competitions/${selectedCompetition.id}/apps`)
|
||||||
const appsData = await appsResponse.json()
|
const appsData = await appsResponse.json()
|
||||||
|
|
||||||
console.log('應用API回應:', appsData)
|
|
||||||
|
|
||||||
if (appsData.success && appsData.data && appsData.data.apps) {
|
if (appsData.success && appsData.data && appsData.data.apps) {
|
||||||
// 直接使用API返回的APP數據,確保數量與評分進度一致
|
// 直接使用API返回的APP數據,確保數量與評分進度一致
|
||||||
@@ -546,38 +524,26 @@ export function ScoringManagement() {
|
|||||||
creator: app.creator || '未知作者',
|
creator: app.creator || '未知作者',
|
||||||
teamId: app.teamId || null
|
teamId: app.teamId || null
|
||||||
}))
|
}))
|
||||||
console.log(`✅ ${selectedCompetition.type === 'team' ? '團隊' : '個人'}競賽APP數據載入成功:`, participants.length, '個APP')
|
|
||||||
} else {
|
|
||||||
console.error('❌ 應用數據載入失敗:', appsData.message || 'API回應格式錯誤')
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.error('❌ 評分進度數據載入失敗:', scoringProgressData.message || 'API回應格式錯誤')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCompetitionParticipants(participants)
|
setCompetitionParticipants(participants)
|
||||||
console.log('✅ 參賽者數據載入完成:', participants.length, '個參賽者')
|
|
||||||
console.log('🔍 參賽者詳細數據:', participants)
|
|
||||||
|
|
||||||
// 載入競賽規則
|
// 載入競賽規則
|
||||||
console.log('📋 載入競賽規則...')
|
|
||||||
const rulesResponse = await fetch(`/api/competitions/${selectedCompetition.id}/rules`)
|
const rulesResponse = await fetch(`/api/competitions/${selectedCompetition.id}/rules`)
|
||||||
const rulesData = await rulesResponse.json()
|
const rulesData = await rulesResponse.json()
|
||||||
|
|
||||||
if (rulesData.success && rulesData.data) {
|
if (rulesData.success && rulesData.data) {
|
||||||
setCompetitionRules(rulesData.data)
|
setCompetitionRules(rulesData.data)
|
||||||
console.log('✅ 競賽規則載入成功:', rulesData.data.length, '個規則')
|
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ 競賽規則載入失敗:', rulesData.message || 'API回應格式錯誤')
|
|
||||||
setCompetitionRules([])
|
setCompetitionRules([])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果沒有載入到任何數據,顯示警告
|
// 如果沒有載入到任何數據,顯示警告
|
||||||
if (participants.length === 0) {
|
if (participants.length === 0) {
|
||||||
console.warn('⚠️ 沒有載入到任何參賽者數據')
|
|
||||||
setError('該競賽暫無參賽者數據,請檢查競賽設置')
|
setError('該競賽暫無參賽者數據,請檢查競賽設置')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ 載入競賽數據失敗:', err)
|
|
||||||
setError('載入競賽數據失敗: ' + (err instanceof Error ? err.message : '未知錯誤'))
|
setError('載入競賽數據失敗: ' + (err instanceof Error ? err.message : '未知錯誤'))
|
||||||
|
|
||||||
// 設置空數組以避免undefined錯誤
|
// 設置空數組以避免undefined錯誤
|
||||||
@@ -599,13 +565,10 @@ export function ScoringManagement() {
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setScoringSummary(data.data)
|
setScoringSummary(data.data)
|
||||||
console.log('✅ 評分完成度匯總載入成功:', data.data)
|
|
||||||
} else {
|
} else {
|
||||||
console.log('❌ 評分完成度匯總載入失敗:', data)
|
|
||||||
setScoringSummary(null)
|
setScoringSummary(null)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('載入評分完成度匯總失敗:', error)
|
|
||||||
setScoringSummary(null)
|
setScoringSummary(null)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingSummary(false)
|
setIsLoadingSummary(false)
|
||||||
@@ -634,11 +597,9 @@ export function ScoringManagement() {
|
|||||||
scoredJudges: data.data?.length || 0
|
scoredJudges: data.data?.length || 0
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
console.error('載入APP評分詳情失敗:', data.message)
|
|
||||||
setError(data.message || '載入APP評分詳情失敗')
|
setError(data.message || '載入APP評分詳情失敗')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('載入APP評分詳情失敗:', error)
|
|
||||||
setError('載入APP評分詳情失敗')
|
setError('載入APP評分詳情失敗')
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingAppDetails(false)
|
setIsLoadingAppDetails(false)
|
||||||
@@ -713,9 +674,7 @@ export function ScoringManagement() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Select value={selectedCompetition?.id || ""} onValueChange={(value) => {
|
<Select value={selectedCompetition?.id || ""} onValueChange={(value) => {
|
||||||
console.log('🎯 選擇競賽:', value)
|
|
||||||
const competition = competitions.find(c => c.id === value)
|
const competition = competitions.find(c => c.id === value)
|
||||||
console.log('🏆 找到競賽:', competition)
|
|
||||||
setSelectedCompetition(competition)
|
setSelectedCompetition(competition)
|
||||||
}}>
|
}}>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
|
@@ -65,6 +65,8 @@ interface CompetitionContextType {
|
|||||||
loadingAwards: boolean
|
loadingAwards: boolean
|
||||||
loadAwards: () => Promise<void>
|
loadAwards: () => Promise<void>
|
||||||
addAward: (award: Omit<Award, "id">) => void
|
addAward: (award: Omit<Award, "id">) => void
|
||||||
|
updateAward: (award: Award) => void
|
||||||
|
deleteAward: (awardId: string) => void
|
||||||
getAwardsByYear: (year: number) => Award[]
|
getAwardsByYear: (year: number) => Award[]
|
||||||
getAwardsByMonth: (year: number, month: number) => Award[]
|
getAwardsByMonth: (year: number, month: number) => Award[]
|
||||||
getAvailableYears: () => number[]
|
getAvailableYears: () => number[]
|
||||||
@@ -571,6 +573,34 @@ export function CompetitionProvider({ children }: { children: ReactNode }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateAward = (award: Award) => {
|
||||||
|
setAwards((prev) => {
|
||||||
|
const index = prev.findIndex((existingAward) => existingAward.id === award.id)
|
||||||
|
if (index === -1) {
|
||||||
|
console.log('⚠️ 找不到要更新的獎項:', award.id)
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ 更新獎項:', award)
|
||||||
|
const updated = [...prev]
|
||||||
|
updated[index] = award
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAward = (awardId: string) => {
|
||||||
|
setAwards((prev) => {
|
||||||
|
const filtered = prev.filter((award) => award.id !== awardId)
|
||||||
|
if (filtered.length === prev.length) {
|
||||||
|
console.log('⚠️ 找不到要刪除的獎項:', awardId)
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ 刪除獎項:', awardId)
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const getAwardsByYear = (year: number): Award[] => {
|
const getAwardsByYear = (year: number): Award[] => {
|
||||||
return awards.filter((award) => award.year === year)
|
return awards.filter((award) => award.year === year)
|
||||||
}
|
}
|
||||||
@@ -843,6 +873,8 @@ export function CompetitionProvider({ children }: { children: ReactNode }) {
|
|||||||
loadingAwards,
|
loadingAwards,
|
||||||
loadAwards,
|
loadAwards,
|
||||||
addAward,
|
addAward,
|
||||||
|
updateAward,
|
||||||
|
deleteAward,
|
||||||
getAwardsByYear,
|
getAwardsByYear,
|
||||||
getAwardsByMonth,
|
getAwardsByMonth,
|
||||||
getAvailableYears,
|
getAvailableYears,
|
||||||
|
@@ -4676,7 +4676,17 @@ export class AwardService extends DatabaseServiceBase {
|
|||||||
|
|
||||||
if (fields.length === 0) return true;
|
if (fields.length === 0) return true;
|
||||||
|
|
||||||
const setClause = fields.map(field => `${field} = ?`).join(', ');
|
// 處理 MySQL 保留字,需要用反引號包圍
|
||||||
|
const setClause = fields.map(field => {
|
||||||
|
// 將駝峰命名轉換為下劃線命名,並處理保留字
|
||||||
|
const dbField = field.replace(/([A-Z])/g, '_$1').toLowerCase();
|
||||||
|
const reservedWords = ['rank', 'order', 'group', 'select', 'from', 'where', 'table'];
|
||||||
|
if (reservedWords.includes(dbField)) {
|
||||||
|
return `\`${dbField}\` = ?`;
|
||||||
|
}
|
||||||
|
return `${dbField} = ?`;
|
||||||
|
}).join(', ');
|
||||||
|
|
||||||
const values = fields.map(field => (updates as any)[field]);
|
const values = fields.map(field => (updates as any)[field]);
|
||||||
|
|
||||||
const sql = `UPDATE awards SET ${setClause} WHERE id = ?`;
|
const sql = `UPDATE awards SET ${setClause} WHERE id = ?`;
|
||||||
|
Reference in New Issue
Block a user