完成評審評分機制
This commit is contained in:
166
app/api/admin/scoring/[id]/route.ts
Normal file
166
app/api/admin/scoring/[id]/route.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
// =====================================================
|
||||
// 評分記錄管理 API
|
||||
// =====================================================
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { ScoringService } 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();
|
||||
const {
|
||||
participantType,
|
||||
scores,
|
||||
comments
|
||||
} = body;
|
||||
|
||||
// 驗證必填欄位
|
||||
if (!participantType || !scores) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '缺少必填欄位',
|
||||
error: 'participantType, scores 為必填欄位'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 驗證評分類型
|
||||
if (!['app', 'proposal'].includes(participantType)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '無效的參賽者類型',
|
||||
error: 'participantType 必須是 "app" 或 "proposal"'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
if (participantType === 'app') {
|
||||
// 驗證應用評分格式
|
||||
const requiredScores = ['innovation_score', 'technical_score', 'usability_score', 'presentation_score', 'impact_score'];
|
||||
const missingScores = requiredScores.filter(score => !(score in scores) || scores[score] < 1 || scores[score] > 10);
|
||||
|
||||
if (missingScores.length > 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '評分格式無效',
|
||||
error: `缺少或無效的評分項目: ${missingScores.join(', ')}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 計算總分
|
||||
const totalScore = (
|
||||
scores.innovation_score +
|
||||
scores.technical_score +
|
||||
scores.usability_score +
|
||||
scores.presentation_score +
|
||||
scores.impact_score
|
||||
) / 5;
|
||||
|
||||
result = await ScoringService.updateAppScore(id, {
|
||||
innovation_score: scores.innovation_score,
|
||||
technical_score: scores.technical_score,
|
||||
usability_score: scores.usability_score,
|
||||
presentation_score: scores.presentation_score,
|
||||
impact_score: scores.impact_score,
|
||||
total_score: totalScore,
|
||||
comments: comments || null
|
||||
});
|
||||
} else {
|
||||
// 驗證提案評分格式
|
||||
const requiredScores = ['problem_identification_score', 'solution_feasibility_score', 'innovation_score', 'impact_score', 'presentation_score'];
|
||||
const missingScores = requiredScores.filter(score => !(score in scores) || scores[score] < 1 || scores[score] > 10);
|
||||
|
||||
if (missingScores.length > 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '評分格式無效',
|
||||
error: `缺少或無效的評分項目: ${missingScores.join(', ')}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 計算總分
|
||||
const totalScore = (
|
||||
scores.problem_identification_score +
|
||||
scores.solution_feasibility_score +
|
||||
scores.innovation_score +
|
||||
scores.impact_score +
|
||||
scores.presentation_score
|
||||
) / 5;
|
||||
|
||||
result = await ScoringService.updateProposalScore(id, {
|
||||
problem_identification_score: scores.problem_identification_score,
|
||||
solution_feasibility_score: scores.solution_feasibility_score,
|
||||
innovation_score: scores.innovation_score,
|
||||
impact_score: scores.impact_score,
|
||||
presentation_score: scores.presentation_score,
|
||||
total_score: totalScore,
|
||||
comments: comments || null
|
||||
});
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '評分更新失敗',
|
||||
error: '找不到指定的評分記錄或更新失敗'
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分更新成功',
|
||||
data: { updated: true }
|
||||
});
|
||||
|
||||
} 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 { searchParams } = new URL(request.url);
|
||||
const participantType = searchParams.get('type');
|
||||
|
||||
if (!participantType || !['app', 'proposal'].includes(participantType)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '缺少或無效的參賽者類型',
|
||||
error: 'type 參數必須是 "app" 或 "proposal"'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await ScoringService.deleteScore(id, participantType as 'app' | 'proposal');
|
||||
|
||||
if (!result) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '評分刪除失敗',
|
||||
error: '找不到指定的評分記錄或刪除失敗'
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分刪除成功',
|
||||
data: { deleted: true }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('刪除評分失敗:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '刪除評分失敗',
|
||||
error: error instanceof Error ? error.message : '未知錯誤'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
399
app/api/admin/scoring/route.ts
Normal file
399
app/api/admin/scoring/route.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
// =====================================================
|
||||
// 評分管理 API
|
||||
// =====================================================
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { ScoringService } from '@/lib/services/database-service';
|
||||
|
||||
// 獲取競賽評分記錄
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const competitionId = searchParams.get('competitionId');
|
||||
const judgeId = searchParams.get('judgeId');
|
||||
const status = searchParams.get('status');
|
||||
const search = searchParams.get('search');
|
||||
|
||||
if (!competitionId) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '缺少競賽ID',
|
||||
error: 'competitionId 參數是必需的'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
let scores;
|
||||
|
||||
if (judgeId) {
|
||||
// 獲取特定評審的評分記錄
|
||||
scores = await ScoringService.getJudgeScores(judgeId, competitionId);
|
||||
} else {
|
||||
// 獲取競賽的所有評分記錄
|
||||
scores = await ScoringService.getCompetitionScores(competitionId);
|
||||
}
|
||||
|
||||
// 狀態篩選
|
||||
if (status && status !== 'all') {
|
||||
if (status === 'completed') {
|
||||
scores = scores.filter(score => score.total_score > 0);
|
||||
} else if (status === 'pending') {
|
||||
scores = scores.filter(score => !score.total_score || score.total_score === 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 搜尋篩選
|
||||
if (search) {
|
||||
const searchLower = search.toLowerCase();
|
||||
scores = scores.filter(score =>
|
||||
score.judge_name?.toLowerCase().includes(searchLower) ||
|
||||
score.app_name?.toLowerCase().includes(searchLower) ||
|
||||
score.creator_name?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
// 獲取評分統計
|
||||
const stats = await ScoringService.getCompetitionScoreStats(competitionId);
|
||||
|
||||
// 處理評分數據,將 score_details 轉換為前端期望的格式
|
||||
const processedScores = scores.map((score: any) => {
|
||||
// 解析 score_details 字符串
|
||||
let scoreDetails: Record<string, number> = {};
|
||||
if (score.score_details) {
|
||||
const details = score.score_details.split(',');
|
||||
details.forEach((detail: string) => {
|
||||
const [ruleName, scoreValue] = detail.split(':');
|
||||
if (ruleName && scoreValue) {
|
||||
scoreDetails[ruleName] = parseInt(scoreValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 映射到前端期望的字段
|
||||
return {
|
||||
...score,
|
||||
innovation_score: scoreDetails['創新性'] || scoreDetails['innovation'] || 0,
|
||||
technical_score: scoreDetails['技術性'] || scoreDetails['technical'] || 0,
|
||||
usability_score: scoreDetails['實用性'] || scoreDetails['usability'] || 0,
|
||||
presentation_score: scoreDetails['展示效果'] || scoreDetails['presentation'] || 0,
|
||||
impact_score: scoreDetails['影響力'] || scoreDetails['impact'] || 0,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分記錄獲取成功',
|
||||
data: {
|
||||
scores: processedScores,
|
||||
stats,
|
||||
total: processedScores.length
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('獲取評分記錄失敗:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '獲取評分記錄失敗',
|
||||
error: error instanceof Error ? error.message : '未知錯誤'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 處理評分的輔助函數
|
||||
async function processScoringWithCompetitionId(participantId: string, judgeId: string, scores: any, comments: string, competitionId: string, isEdit: boolean = false, recordId: string | null = null) {
|
||||
const rules = await ScoringService.getCompetitionRules(competitionId);
|
||||
if (!rules || rules.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '競賽評分規則未設置',
|
||||
error: '請先設置競賽評分規則'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 驗證評分格式(基於實際的競賽規則)
|
||||
const providedScores = Object.keys(scores).filter(key => scores[key] > 0);
|
||||
const invalidScores = providedScores.filter(score => scores[score] < 1 || scores[score] > 10);
|
||||
|
||||
if (invalidScores.length > 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '評分格式無效',
|
||||
error: `無效的評分項目: ${invalidScores.join(', ')}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
if (providedScores.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '評分格式無效',
|
||||
error: '至少需要提供一個評分項目'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 計算總分(基於權重,轉換為100分制)
|
||||
let totalScore = 0;
|
||||
let totalWeight = 0;
|
||||
|
||||
rules.forEach((rule: any) => {
|
||||
const score = scores[rule.name];
|
||||
if (score && score > 0) {
|
||||
totalScore += score * (rule.weight / 100);
|
||||
totalWeight += rule.weight;
|
||||
}
|
||||
});
|
||||
|
||||
// 如果總權重為0,使用平均分
|
||||
if (totalWeight === 0) {
|
||||
const validScores = Object.values(scores).filter(score => score > 0);
|
||||
totalScore = validScores.length > 0 ? validScores.reduce((sum, score) => sum + score, 0) / validScores.length : 0;
|
||||
}
|
||||
|
||||
// 轉換為100分制(10分制 * 10 = 100分制)
|
||||
totalScore = totalScore * 10;
|
||||
|
||||
// 將自定義評分映射到標準字段
|
||||
const validScoreData: any = {
|
||||
judge_id: judgeId,
|
||||
app_id: participantId,
|
||||
competition_id: competitionId,
|
||||
scores: scores, // 傳遞原始評分數據
|
||||
total_score: totalScore,
|
||||
comments: comments || null,
|
||||
isEdit: isEdit || false,
|
||||
recordId: recordId || null
|
||||
};
|
||||
|
||||
// 按順序將自定義評分映射到標準字段
|
||||
const standardFields = ['innovation_score', 'technical_score', 'usability_score', 'presentation_score', 'impact_score'];
|
||||
const customScores = Object.entries(scores).filter(([key, value]) => value > 0);
|
||||
|
||||
customScores.forEach(([customKey, score], index) => {
|
||||
if (index < standardFields.length) {
|
||||
validScoreData[standardFields[index]] = score;
|
||||
}
|
||||
});
|
||||
|
||||
const result = await ScoringService.submitAppScore(validScoreData);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分提交成功',
|
||||
data: result
|
||||
});
|
||||
}
|
||||
|
||||
// 提交評分
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
console.log('🔍 API 接收到的請求數據:', JSON.stringify(body, null, 2));
|
||||
|
||||
const {
|
||||
judgeId,
|
||||
participantId,
|
||||
participantType,
|
||||
scores,
|
||||
comments,
|
||||
competitionId,
|
||||
isEdit,
|
||||
recordId
|
||||
} = body;
|
||||
|
||||
console.log('🔍 解析後的參數:');
|
||||
console.log('judgeId:', judgeId, typeof judgeId);
|
||||
console.log('participantId:', participantId, typeof participantId);
|
||||
console.log('participantType:', participantType, typeof participantType);
|
||||
console.log('scores:', scores, typeof scores);
|
||||
console.log('competitionId:', competitionId, typeof competitionId);
|
||||
console.log('isEdit:', isEdit, typeof isEdit);
|
||||
console.log('recordId:', recordId, typeof recordId);
|
||||
|
||||
// 驗證必填欄位
|
||||
if (!judgeId || !participantId || !participantType || !scores) {
|
||||
console.log('❌ 缺少必填欄位驗證失敗');
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '缺少必填欄位',
|
||||
error: 'judgeId, participantId, participantType, scores 為必填欄位'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 驗證評分類型
|
||||
if (!['app', 'proposal', 'team'].includes(participantType)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '無效的參賽者類型',
|
||||
error: 'participantType 必須是 "app"、"proposal" 或 "team"'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
if (participantType === 'app') {
|
||||
// 獲取競賽規則來驗證評分格式
|
||||
let finalCompetitionId = await ScoringService.getCompetitionIdByAppId(participantId);
|
||||
|
||||
if (!finalCompetitionId) {
|
||||
// 如果找不到競賽關聯,嘗試通過其他方式獲取競賽ID
|
||||
console.log('⚠️ 找不到APP的競賽關聯,嘗試其他方式...');
|
||||
|
||||
// 檢查是否有其他方式獲取競賽ID(例如通過請求參數)
|
||||
if (competitionId) {
|
||||
console.log('✅ 使用參數中的競賽ID:', competitionId);
|
||||
finalCompetitionId = competitionId;
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '找不到對應的競賽',
|
||||
error: 'APP未註冊到任何競賽中,請先在競賽管理中將APP添加到競賽'
|
||||
}, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const rules = await ScoringService.getCompetitionRules(finalCompetitionId);
|
||||
if (!rules || rules.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '競賽評分規則未設置',
|
||||
error: '請先設置競賽評分規則'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 驗證評分格式(基於實際的競賽規則)
|
||||
const providedScores = Object.keys(scores).filter(key => scores[key] > 0);
|
||||
const invalidScores = providedScores.filter(score => scores[score] < 1 || scores[score] > 10);
|
||||
|
||||
if (invalidScores.length > 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '評分格式無效',
|
||||
error: `無效的評分項目: ${invalidScores.join(', ')}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
if (providedScores.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '評分格式無效',
|
||||
error: '至少需要提供一個評分項目'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 計算總分(基於權重,轉換為100分制)
|
||||
let totalScore = 0;
|
||||
let totalWeight = 0;
|
||||
|
||||
rules.forEach((rule: any) => {
|
||||
const score = scores[rule.name];
|
||||
if (score && score > 0) {
|
||||
totalScore += score * (rule.weight / 100);
|
||||
totalWeight += rule.weight;
|
||||
}
|
||||
});
|
||||
|
||||
// 如果總權重為0,使用平均分
|
||||
if (totalWeight === 0) {
|
||||
const validScores = Object.values(scores).filter(score => score > 0);
|
||||
totalScore = validScores.length > 0 ? validScores.reduce((sum, score) => sum + score, 0) / validScores.length : 0;
|
||||
}
|
||||
|
||||
// 轉換為100分制(10分制 * 10 = 100分制)
|
||||
totalScore = totalScore * 10;
|
||||
|
||||
// 使用新的基於競賽規則的評分系統
|
||||
const validScoreData = {
|
||||
judge_id: judgeId,
|
||||
app_id: participantId,
|
||||
competition_id: finalCompetitionId,
|
||||
scores: scores, // 直接使用原始評分數據
|
||||
total_score: totalScore,
|
||||
comments: comments || null,
|
||||
isEdit: isEdit || false,
|
||||
recordId: recordId || null
|
||||
};
|
||||
|
||||
result = await ScoringService.submitAppScore(validScoreData);
|
||||
} else if (participantType === 'proposal') {
|
||||
// 驗證提案評分格式
|
||||
const requiredScores = ['problem_identification_score', 'solution_feasibility_score', 'innovation_score', 'impact_score', 'presentation_score'];
|
||||
const missingScores = requiredScores.filter(score => !(score in scores) || scores[score] < 1 || scores[score] > 10);
|
||||
|
||||
if (missingScores.length > 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '評分格式無效',
|
||||
error: `缺少或無效的評分項目: ${missingScores.join(', ')}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 計算總分
|
||||
const totalScore = (
|
||||
scores.problem_identification_score +
|
||||
scores.solution_feasibility_score +
|
||||
scores.innovation_score +
|
||||
scores.impact_score +
|
||||
scores.presentation_score
|
||||
) / 5;
|
||||
|
||||
result = await ScoringService.submitProposalScore({
|
||||
judge_id: judgeId,
|
||||
proposal_id: participantId,
|
||||
problem_identification_score: scores.problem_identification_score,
|
||||
solution_feasibility_score: scores.solution_feasibility_score,
|
||||
innovation_score: scores.innovation_score,
|
||||
impact_score: scores.impact_score,
|
||||
presentation_score: scores.presentation_score,
|
||||
total_score: totalScore,
|
||||
comments: comments || null
|
||||
});
|
||||
} else if (participantType === 'team') {
|
||||
// 驗證團隊評分格式
|
||||
const requiredScores = ['innovation_score', 'technical_score', 'usability_score', 'presentation_score', 'impact_score'];
|
||||
const missingScores = requiredScores.filter(score => !(score in scores) || scores[score] < 1 || scores[score] > 10);
|
||||
|
||||
if (missingScores.length > 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '評分格式無效',
|
||||
error: `缺少或無效的評分項目: ${missingScores.join(', ')}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 計算總分
|
||||
const totalScore = (
|
||||
scores.innovation_score +
|
||||
scores.technical_score +
|
||||
scores.usability_score +
|
||||
scores.presentation_score +
|
||||
scores.impact_score
|
||||
) / 5;
|
||||
|
||||
// 團隊評分使用應用評分表
|
||||
result = await ScoringService.submitTeamScore({
|
||||
judge_id: judgeId,
|
||||
teamId: participantId,
|
||||
innovation_score: scores.innovation_score,
|
||||
technical_score: scores.technical_score,
|
||||
usability_score: scores.usability_score,
|
||||
presentation_score: scores.presentation_score,
|
||||
impact_score: scores.impact_score,
|
||||
total_score: totalScore,
|
||||
comments: comments || null
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分提交成功',
|
||||
data: result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('提交評分失敗:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '提交評分失敗',
|
||||
error: error instanceof Error ? error.message : '未知錯誤'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
39
app/api/admin/scoring/stats/route.ts
Normal file
39
app/api/admin/scoring/stats/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// =====================================================
|
||||
// 評分統計 API
|
||||
// =====================================================
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { ScoringService } from '@/lib/services/database-service';
|
||||
|
||||
// 獲取評分統計數據
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const competitionId = searchParams.get('competitionId');
|
||||
|
||||
if (!competitionId) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '缺少競賽ID',
|
||||
error: 'competitionId 參數是必需的'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 獲取評分統計
|
||||
const stats = await ScoringService.getCompetitionScoreStats(competitionId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分統計獲取成功',
|
||||
data: stats
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('獲取評分統計失敗:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '獲取評分統計失敗',
|
||||
error: error instanceof Error ? error.message : '未知錯誤'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
34
app/api/admin/scoring/summary/route.ts
Normal file
34
app/api/admin/scoring/summary/route.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { ScoringService } from '@/lib/services/database-service';
|
||||
|
||||
// 獲取評分完成度匯總
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const competitionId = searchParams.get('competitionId');
|
||||
|
||||
if (!competitionId) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '缺少競賽ID參數'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 獲取評分完成度匯總數據
|
||||
const summary = await ScoringService.getScoringSummary(competitionId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分完成度匯總獲取成功',
|
||||
data: summary
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('獲取評分完成度匯總失敗:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '獲取評分完成度匯總失敗',
|
||||
error: error instanceof Error ? error.message : '未知錯誤'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
30
app/api/competitions/[id]/rules/route.ts
Normal file
30
app/api/competitions/[id]/rules/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// =====================================================
|
||||
// 競賽規則 API
|
||||
// =====================================================
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { ScoringService } from '@/lib/services/database-service';
|
||||
|
||||
// 獲取競賽的評分規則
|
||||
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
// 獲取競賽規則
|
||||
const rules = await ScoringService.getCompetitionRules(id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '競賽規則獲取成功',
|
||||
data: rules
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('獲取競賽規則失敗:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '獲取競賽規則失敗',
|
||||
error: error instanceof Error ? error.message : '未知錯誤'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
@@ -143,7 +143,7 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri
|
||||
return {
|
||||
...team,
|
||||
members: allMembers,
|
||||
apps: teamApps.map(app => app.id),
|
||||
apps: appsWithDetails, // 返回完整的APP對象而不是ID
|
||||
appsDetails: appsWithDetails
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -175,22 +175,16 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri
|
||||
let totalLikes = 0;
|
||||
|
||||
// 獲取每個應用的真實數據
|
||||
for (const appId of team.apps) {
|
||||
for (const app of team.apps) {
|
||||
try {
|
||||
const appSql = 'SELECT likes_count, views_count FROM apps WHERE id = ? AND is_active = TRUE';
|
||||
const appResult = await db.query(appSql, [appId]);
|
||||
const likes = app.likes_count || 0;
|
||||
const views = app.views_count || 0;
|
||||
|
||||
if (appResult.length > 0) {
|
||||
const app = appResult[0];
|
||||
const likes = app.likes_count || 0;
|
||||
const views = app.views_count || 0;
|
||||
|
||||
maxLikes = Math.max(maxLikes, likes);
|
||||
totalViews += views;
|
||||
totalLikes += likes;
|
||||
}
|
||||
maxLikes = Math.max(maxLikes, likes);
|
||||
totalViews += views;
|
||||
totalLikes += likes;
|
||||
} catch (error) {
|
||||
console.error(`獲取應用 ${appId} 數據失敗:`, error);
|
||||
console.error(`處理應用 ${app.id} 數據失敗:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +222,21 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri
|
||||
name: member.name,
|
||||
role: member.role === '??????' ? '成員' : (member.role || '成員')
|
||||
})),
|
||||
apps: team.apps,
|
||||
apps: team.apps.map(app => ({
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
description: app.description,
|
||||
category: app.category,
|
||||
type: app.type,
|
||||
icon: app.icon,
|
||||
icon_color: app.icon_color,
|
||||
likes_count: app.likes_count,
|
||||
views_count: app.views_count,
|
||||
rating: app.rating,
|
||||
creator_name: app.creator_name,
|
||||
creator_department: app.creator_department,
|
||||
created_at: app.created_at
|
||||
})),
|
||||
appsDetails: team.appsDetails || [],
|
||||
popularityScore: team.popularityScore,
|
||||
maxLikes: team.maxLikes,
|
||||
|
33
app/api/competitions/scoring-progress/route.ts
Normal file
33
app/api/competitions/scoring-progress/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { ScoringService } from '@/lib/services/database-service';
|
||||
|
||||
// 獲取競賽評分進度
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const competitionId = searchParams.get('competitionId');
|
||||
|
||||
if (!competitionId) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '缺少競賽ID參數'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const progress = await ScoringService.getCompetitionScoringProgress(competitionId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分進度獲取成功',
|
||||
data: progress
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('獲取評分進度失敗:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '獲取評分進度失敗',
|
||||
error: error instanceof Error ? error.message : '未知錯誤'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
54
app/api/debug/env/route.ts
vendored
Normal file
54
app/api/debug/env/route.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
// =====================================================
|
||||
// 環境變數調試 API
|
||||
// =====================================================
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
console.log('🔍 檢查 Next.js 中的環境變數...');
|
||||
|
||||
// 檢查所有相關的環境變數
|
||||
const envVars = {
|
||||
DB_HOST: process.env.DB_HOST,
|
||||
DB_PORT: process.env.DB_PORT,
|
||||
DB_NAME: process.env.DB_NAME,
|
||||
DB_USER: process.env.DB_USER,
|
||||
DB_PASSWORD: process.env.DB_PASSWORD ? '***' : undefined,
|
||||
SLAVE_DB_HOST: process.env.SLAVE_DB_HOST,
|
||||
SLAVE_DB_PORT: process.env.SLAVE_DB_PORT,
|
||||
SLAVE_DB_NAME: process.env.SLAVE_DB_NAME,
|
||||
SLAVE_DB_USER: process.env.SLAVE_DB_USER,
|
||||
SLAVE_DB_PASSWORD: process.env.SLAVE_DB_PASSWORD ? '***' : undefined,
|
||||
DB_DUAL_WRITE_ENABLED: process.env.DB_DUAL_WRITE_ENABLED,
|
||||
DB_MASTER_PRIORITY: process.env.DB_MASTER_PRIORITY,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
};
|
||||
|
||||
console.log('📋 Next.js 環境變數檢查結果:');
|
||||
Object.entries(envVars).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
console.log(`✅ ${key}: ${value}`);
|
||||
} else {
|
||||
console.log(`❌ ${key}: undefined`);
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '環境變數檢查完成',
|
||||
data: {
|
||||
envVars,
|
||||
timestamp: new Date().toISOString(),
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ 環境變數檢查失敗:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '環境變數檢查失敗',
|
||||
error: error instanceof Error ? error.message : '未知錯誤'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
44
app/api/debug/simple-env/route.ts
Normal file
44
app/api/debug/simple-env/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// =====================================================
|
||||
// 簡單環境變數測試
|
||||
// =====================================================
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// 直接檢查環境變數
|
||||
const envCheck = {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
DB_HOST: process.env.DB_HOST,
|
||||
DB_PORT: process.env.DB_PORT,
|
||||
DB_NAME: process.env.DB_NAME,
|
||||
DB_USER: process.env.DB_USER,
|
||||
DB_PASSWORD: process.env.DB_PASSWORD ? '***' : undefined,
|
||||
// 檢查所有可能的環境變數
|
||||
ALL_ENV_KEYS: Object.keys(process.env).filter(key => key.startsWith('DB_')),
|
||||
};
|
||||
|
||||
console.log('🔍 環境變數檢查:');
|
||||
console.log('NODE_ENV:', process.env.NODE_ENV);
|
||||
console.log('DB_HOST:', process.env.DB_HOST);
|
||||
console.log('DB_PORT:', process.env.DB_PORT);
|
||||
console.log('DB_NAME:', process.env.DB_NAME);
|
||||
console.log('DB_USER:', process.env.DB_USER);
|
||||
console.log('DB_PASSWORD:', process.env.DB_PASSWORD ? '***' : 'undefined');
|
||||
console.log('所有 DB_ 開頭的環境變數:', Object.keys(process.env).filter(key => key.startsWith('DB_')));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '環境變數檢查完成',
|
||||
data: envCheck,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ 環境變數檢查失敗:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '環境變數檢查失敗',
|
||||
error: error instanceof Error ? error.message : '未知錯誤'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
63
app/api/judge/scoring-tasks/route.ts
Normal file
63
app/api/judge/scoring-tasks/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { ScoringService, JudgeService } from '@/lib/services/database-service';
|
||||
|
||||
// 獲取評審的評分任務
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const judgeId = searchParams.get('judgeId');
|
||||
const competitionId = searchParams.get('competitionId');
|
||||
|
||||
if (!judgeId) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '缺少評審ID',
|
||||
error: 'judgeId 為必填參數'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 獲取評審信息
|
||||
const judge = await JudgeService.getJudgeById(judgeId);
|
||||
if (!judge) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '評審不存在',
|
||||
error: '找不到指定的評審'
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
// 獲取評審的評分任務
|
||||
let scoringTasks = [];
|
||||
|
||||
if (competitionId) {
|
||||
// 獲取特定競賽的評分任務
|
||||
scoringTasks = await JudgeService.getJudgeScoringTasks(judgeId, competitionId);
|
||||
} else {
|
||||
// 獲取所有評分任務
|
||||
scoringTasks = await JudgeService.getJudgeScoringTasks(judgeId);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分任務獲取成功',
|
||||
data: {
|
||||
judge: {
|
||||
id: judge.id,
|
||||
name: judge.name,
|
||||
title: judge.title,
|
||||
department: judge.department,
|
||||
specialty: judge.specialty || '評審專家'
|
||||
},
|
||||
tasks: scoringTasks
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('獲取評分任務失敗:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '獲取評分任務失敗',
|
||||
error: error instanceof Error ? error.message : '未知錯誤'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
43
app/api/test-db/route.ts
Normal file
43
app/api/test-db/route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// =====================================================
|
||||
// 資料庫連接測試 API
|
||||
// =====================================================
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/database';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
console.log('🧪 開始測試資料庫連接...');
|
||||
|
||||
// 測試基本查詢
|
||||
const result = await db.query('SELECT 1 as test');
|
||||
console.log('✅ 基本查詢成功:', result);
|
||||
|
||||
// 測試競賽表
|
||||
const competitions = await db.query('SELECT id, name, type FROM competitions WHERE is_active = TRUE LIMIT 3');
|
||||
console.log('✅ 競賽查詢成功:', competitions);
|
||||
|
||||
// 測試評審表
|
||||
const judges = await db.query('SELECT id, name, title FROM judges WHERE is_active = TRUE LIMIT 3');
|
||||
console.log('✅ 評審查詢成功:', judges);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '資料庫連接測試成功',
|
||||
data: {
|
||||
basicQuery: result,
|
||||
competitions: competitions,
|
||||
judges: judges
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 資料庫連接測試失敗:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '資料庫連接測試失敗',
|
||||
error: error instanceof Error ? error.message : '未知錯誤',
|
||||
stack: error instanceof Error ? error.stack : undefined
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
181
app/debug-scoring/page.tsx
Normal file
181
app/debug-scoring/page.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export default function DebugScoringPage() {
|
||||
const [competitions, setCompetitions] = useState<any[]>([])
|
||||
const [selectedCompetition, setSelectedCompetition] = useState<any>(null)
|
||||
const [competitionJudges, setCompetitionJudges] = useState<any[]>([])
|
||||
const [competitionParticipants, setCompetitionParticipants] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
|
||||
const addLog = (message: string) => {
|
||||
setLogs(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`])
|
||||
console.log(message)
|
||||
}
|
||||
|
||||
// 載入競賽列表
|
||||
const loadCompetitions = async () => {
|
||||
try {
|
||||
addLog('🔄 開始載入競賽列表...')
|
||||
const response = await fetch('/api/competitions')
|
||||
const data = await response.json()
|
||||
addLog(`📋 競賽API回應: ${JSON.stringify(data)}`)
|
||||
|
||||
if (data.success && data.data) {
|
||||
setCompetitions(data.data)
|
||||
addLog(`✅ 載入 ${data.data.length} 個競賽`)
|
||||
} else {
|
||||
addLog(`❌ 競賽載入失敗: ${data.message}`)
|
||||
}
|
||||
} catch (error) {
|
||||
addLog(`❌ 競賽載入錯誤: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 載入競賽數據
|
||||
const loadCompetitionData = async (competitionId: string) => {
|
||||
if (!competitionId) return
|
||||
|
||||
setLoading(true)
|
||||
addLog(`🔍 開始載入競賽數據,ID: ${competitionId}`)
|
||||
|
||||
try {
|
||||
// 載入評審
|
||||
addLog('📋 載入評審...')
|
||||
const judgesResponse = await fetch(`/api/competitions/${competitionId}/judges`)
|
||||
const judgesData = await judgesResponse.json()
|
||||
addLog(`評審API回應: ${JSON.stringify(judgesData)}`)
|
||||
|
||||
if (judgesData.success && judgesData.data && judgesData.data.judges) {
|
||||
setCompetitionJudges(judgesData.data.judges)
|
||||
addLog(`✅ 載入 ${judgesData.data.judges.length} 個評審`)
|
||||
} else {
|
||||
addLog(`❌ 評審載入失敗: ${judgesData.message}`)
|
||||
setCompetitionJudges([])
|
||||
}
|
||||
|
||||
// 載入參賽者
|
||||
addLog('📱 載入參賽者...')
|
||||
const [appsResponse, teamsResponse] = await Promise.all([
|
||||
fetch(`/api/competitions/${competitionId}/apps`),
|
||||
fetch(`/api/competitions/${competitionId}/teams`)
|
||||
])
|
||||
|
||||
const appsData = await appsResponse.json()
|
||||
const teamsData = await teamsResponse.json()
|
||||
|
||||
addLog(`應用API回應: ${JSON.stringify(appsData)}`)
|
||||
addLog(`團隊API回應: ${JSON.stringify(teamsData)}`)
|
||||
|
||||
const participants = []
|
||||
|
||||
if (appsData.success && appsData.data && appsData.data.apps) {
|
||||
participants.push(...appsData.data.apps.map((app: any) => ({
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
type: 'individual',
|
||||
creator: app.creator
|
||||
})))
|
||||
addLog(`✅ 載入 ${appsData.data.apps.length} 個應用`)
|
||||
} else {
|
||||
addLog(`❌ 應用載入失敗: ${appsData.message}`)
|
||||
}
|
||||
|
||||
if (teamsData.success && teamsData.data && teamsData.data.teams) {
|
||||
participants.push(...teamsData.data.teams.map((team: any) => ({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
type: 'team',
|
||||
creator: team.members && team.members.find((m: any) => m.role === '隊長')?.name || '未知隊長'
|
||||
})))
|
||||
addLog(`✅ 載入 ${teamsData.data.teams.length} 個團隊`)
|
||||
} else {
|
||||
addLog(`❌ 團隊載入失敗: ${teamsData.message}`)
|
||||
}
|
||||
|
||||
setCompetitionParticipants(participants)
|
||||
addLog(`✅ 參賽者載入完成: ${participants.length} 個`)
|
||||
|
||||
} catch (error) {
|
||||
addLog(`❌ 載入失敗: ${error.message}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadCompetitions()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>評分表單調試頁面</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">選擇競賽:</label>
|
||||
<select
|
||||
value={selectedCompetition?.id || ""}
|
||||
onChange={(e) => {
|
||||
const competition = competitions.find(c => c.id === e.target.value)
|
||||
setSelectedCompetition(competition)
|
||||
if (competition) {
|
||||
loadCompetitionData(competition.id)
|
||||
}
|
||||
}}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
<option value="">選擇競賽</option>
|
||||
{competitions.map(comp => (
|
||||
<option key={comp.id} value={comp.id}>
|
||||
{comp.name} ({comp.type})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedCompetition && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">評審 ({competitionJudges.length})</h3>
|
||||
<div className="space-y-2">
|
||||
{competitionJudges.map(judge => (
|
||||
<div key={judge.id} className="p-2 bg-gray-100 rounded">
|
||||
{judge.name} - {judge.title} - {judge.department}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">參賽者 ({competitionParticipants.length})</h3>
|
||||
<div className="space-y-2">
|
||||
{competitionParticipants.map(participant => (
|
||||
<div key={participant.id} className="p-2 bg-gray-100 rounded">
|
||||
{participant.name} ({participant.type}) - {participant.creator}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">調試日誌</h3>
|
||||
<div className="bg-gray-100 p-4 rounded max-h-96 overflow-y-auto">
|
||||
{logs.map((log, index) => (
|
||||
<div key={index} className="text-sm font-mono">{log}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -42,67 +42,164 @@ export default function JudgeScoringPage() {
|
||||
const [error, setError] = useState("")
|
||||
const [success, setSuccess] = useState("")
|
||||
const [showAccessCode, setShowAccessCode] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [competitionRules, setCompetitionRules] = useState<any[]>([])
|
||||
|
||||
// Judge data - empty for production
|
||||
const mockJudges: Judge[] = []
|
||||
|
||||
// Scoring items - empty for production
|
||||
const mockScoringItems: ScoringItem[] = []
|
||||
|
||||
const handleLogin = () => {
|
||||
const handleLogin = async () => {
|
||||
setError("")
|
||||
setIsLoading(true)
|
||||
|
||||
if (!judgeId.trim() || !accessCode.trim()) {
|
||||
setError("請填寫評審ID和存取碼")
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (accessCode !== "judge2024") {
|
||||
setError("存取碼錯誤")
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const judge = mockJudges.find(j => j.id === judgeId)
|
||||
if (!judge) {
|
||||
setError("評審ID不存在")
|
||||
return
|
||||
try {
|
||||
// 獲取評審的評分任務
|
||||
const response = await fetch(`/api/judge/scoring-tasks?judgeId=${judgeId}`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setCurrentJudge(data.data.judge)
|
||||
setScoringItems(data.data.tasks)
|
||||
setIsLoggedIn(true)
|
||||
setSuccess("登入成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
|
||||
// 載入競賽規則
|
||||
await loadCompetitionRules()
|
||||
} else {
|
||||
setError(data.message || "登入失敗")
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('登入失敗:', err)
|
||||
setError("登入失敗,請重試")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
setCurrentJudge(judge)
|
||||
setScoringItems(mockScoringItems)
|
||||
setIsLoggedIn(true)
|
||||
setSuccess("登入成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
|
||||
const handleStartScoring = (item: ScoringItem) => {
|
||||
const loadCompetitionRules = async () => {
|
||||
try {
|
||||
// 使用正確的競賽ID
|
||||
const response = await fetch('/api/competitions/be47d842-91f1-11f0-8595-bd825523ae01/rules')
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setCompetitionRules(data.data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('載入競賽規則失敗:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartScoring = async (item: ScoringItem) => {
|
||||
setSelectedItem(item)
|
||||
setScores({})
|
||||
setComments("")
|
||||
|
||||
// 如果是重新評分,嘗試載入現有的評分數據
|
||||
if (item.status === "completed") {
|
||||
try {
|
||||
// 這裡可以添加載入現有評分數據的邏輯
|
||||
// 暫時使用默認值
|
||||
const initialScores: Record<string, number> = {}
|
||||
if (competitionRules && competitionRules.length > 0) {
|
||||
competitionRules.forEach((rule: any) => {
|
||||
initialScores[rule.name] = 0
|
||||
})
|
||||
} else {
|
||||
initialScores.innovation = 0
|
||||
initialScores.technical = 0
|
||||
initialScores.usability = 0
|
||||
initialScores.presentation = 0
|
||||
initialScores.impact = 0
|
||||
}
|
||||
|
||||
setScores(initialScores)
|
||||
setComments("")
|
||||
} catch (err) {
|
||||
console.error('載入現有評分數據失敗:', err)
|
||||
}
|
||||
} else {
|
||||
// 新評分,初始化為0
|
||||
const initialScores: Record<string, number> = {}
|
||||
if (competitionRules && competitionRules.length > 0) {
|
||||
competitionRules.forEach((rule: any) => {
|
||||
initialScores[rule.name] = 0
|
||||
})
|
||||
} else {
|
||||
initialScores.innovation = 0
|
||||
initialScores.technical = 0
|
||||
initialScores.usability = 0
|
||||
initialScores.presentation = 0
|
||||
initialScores.impact = 0
|
||||
}
|
||||
|
||||
setScores(initialScores)
|
||||
setComments("")
|
||||
}
|
||||
|
||||
setShowScoringDialog(true)
|
||||
}
|
||||
|
||||
const handleSubmitScore = async () => {
|
||||
if (!selectedItem) return
|
||||
if (!selectedItem || !currentJudge) return
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
// 模擬提交評分
|
||||
setTimeout(() => {
|
||||
setScoringItems(prev => prev.map(item =>
|
||||
item.id === selectedItem.id
|
||||
? { ...item, status: "completed", score: Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length, submittedAt: new Date().toISOString() }
|
||||
: item
|
||||
))
|
||||
try {
|
||||
// 計算總分 (1-10分制,轉換為100分制)
|
||||
const totalScore = (Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length) * 10
|
||||
|
||||
setShowScoringDialog(false)
|
||||
setSelectedItem(null)
|
||||
setScores({})
|
||||
setComments("")
|
||||
// 提交評分到 API
|
||||
const response = await fetch('/api/admin/scoring', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
judgeId: currentJudge.id,
|
||||
participantId: selectedItem.id,
|
||||
participantType: 'app',
|
||||
scores: scores,
|
||||
comments: comments.trim(),
|
||||
competitionId: 'be47d842-91f1-11f0-8595-bd825523ae01', // 正確的競賽ID
|
||||
isEdit: selectedItem.status === "completed", // 如果是重新評分,標記為編輯模式
|
||||
recordId: selectedItem.status === "completed" ? selectedItem.id : null
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
// 更新本地狀態
|
||||
setScoringItems(prev => prev.map(item =>
|
||||
item.id === selectedItem.id
|
||||
? { ...item, status: "completed", score: totalScore, submittedAt: new Date().toISOString() }
|
||||
: item
|
||||
))
|
||||
|
||||
setShowScoringDialog(false)
|
||||
setSelectedItem(null)
|
||||
setScores({})
|
||||
setComments("")
|
||||
setSuccess("評分提交成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
} else {
|
||||
setError(data.message || "評分提交失敗")
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('評分提交失敗:', err)
|
||||
setError("評分提交失敗,請重試")
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
setSuccess("評分提交成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const getProgress = () => {
|
||||
@@ -111,6 +208,23 @@ export default function JudgeScoringPage() {
|
||||
return { total, completed, percentage: total > 0 ? Math.round((completed / total) * 100) : 0 }
|
||||
}
|
||||
|
||||
const isFormValid = () => {
|
||||
// 檢查所有評分項目是否都已評分
|
||||
const rules = competitionRules && competitionRules.length > 0 ? competitionRules : [
|
||||
{ name: "創新性" }, { name: "技術性" }, { name: "實用性" },
|
||||
{ name: "展示效果" }, { name: "影響力" }
|
||||
]
|
||||
|
||||
const allScoresFilled = rules.every((rule: any) =>
|
||||
scores[rule.name] && scores[rule.name] > 0
|
||||
)
|
||||
|
||||
// 檢查評審意見是否填寫
|
||||
const commentsFilled = comments.trim().length > 0
|
||||
|
||||
return allScoresFilled && commentsFilled
|
||||
}
|
||||
|
||||
const progress = getProgress()
|
||||
|
||||
if (!isLoggedIn) {
|
||||
@@ -170,9 +284,19 @@ export default function JudgeScoringPage() {
|
||||
onClick={handleLogin}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<LogIn className="w-4 h-4 mr-2" />
|
||||
登入評分系統
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
登入中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LogIn className="w-4 h-4 mr-2" />
|
||||
登入評分系統
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="text-center text-sm text-gray-500">
|
||||
@@ -268,7 +392,7 @@ export default function JudgeScoringPage() {
|
||||
<User className="w-4 h-4 text-green-600" />
|
||||
</div>
|
||||
)}
|
||||
<span className="font-medium">{item.name}</span>
|
||||
<span className="font-medium">{item.display_name || item.name}</span>
|
||||
<Badge variant="outline">
|
||||
{item.type === "individual" ? "個人" : "團隊"}
|
||||
</Badge>
|
||||
@@ -277,10 +401,21 @@ export default function JudgeScoringPage() {
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{item.status === "completed" ? (
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-green-600">{item.score}</div>
|
||||
<div className="text-xs text-gray-500">/ 10</div>
|
||||
<div className="text-xs text-gray-500">{item.submittedAt}</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-green-600">{item.score}</div>
|
||||
<div className="text-xs text-gray-500">/ 100</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{item.submittedAt ? new Date(item.submittedAt).toLocaleDateString('zh-TW') : ''}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleStartScoring(item)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
重新評分
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
@@ -313,15 +448,18 @@ export default function JudgeScoringPage() {
|
||||
{/* 評分項目 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">評分項目</h3>
|
||||
{[
|
||||
{(competitionRules && competitionRules.length > 0 ? competitionRules : [
|
||||
{ name: "創新性", description: "創新程度和獨特性" },
|
||||
{ name: "技術性", description: "技術實現的複雜度和品質" },
|
||||
{ name: "實用性", description: "實際應用價值和用戶體驗" },
|
||||
{ name: "展示效果", description: "展示的清晰度和吸引力" },
|
||||
{ name: "影響力", description: "對行業或社會的潛在影響" }
|
||||
].map((criterion, index) => (
|
||||
]).map((criterion, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<Label>{criterion.name}</Label>
|
||||
<Label className="flex items-center space-x-1">
|
||||
<span>{criterion.name}</span>
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<p className="text-sm text-gray-600">{criterion.description}</p>
|
||||
<div className="flex space-x-2">
|
||||
{Array.from({ length: 10 }, (_, i) => i + 1).map((score) => (
|
||||
@@ -339,19 +477,29 @@ export default function JudgeScoringPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!scores[criterion.name] && (
|
||||
<p className="text-xs text-red-500">請為此項目打分</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 評審意見 */}
|
||||
<div className="space-y-2">
|
||||
<Label>評審意見</Label>
|
||||
<Label className="flex items-center space-x-1">
|
||||
<span>評審意見</span>
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="請詳細填寫評審意見、優點分析、改進建議等..."
|
||||
value={comments}
|
||||
onChange={(e) => setComments(e.target.value)}
|
||||
rows={4}
|
||||
className={!comments.trim() ? "border-red-300" : ""}
|
||||
/>
|
||||
{!comments.trim() && (
|
||||
<p className="text-xs text-red-500">請填寫評審意見</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 總分顯示 */}
|
||||
@@ -360,9 +508,9 @@ export default function JudgeScoringPage() {
|
||||
<span className="font-semibold">總分</span>
|
||||
<span className="text-2xl font-bold text-blue-600">
|
||||
{Object.values(scores).length > 0
|
||||
? Math.round(Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length)
|
||||
? Math.round(Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length * 10)
|
||||
: 0
|
||||
} / 10
|
||||
} / 100
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -377,7 +525,7 @@ export default function JudgeScoringPage() {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmitScore}
|
||||
disabled={isSubmitting || Object.keys(scores).length < 5 || !comments.trim()}
|
||||
disabled={isSubmitting || !isFormValid()}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
|
72
app/test-api/page.tsx
Normal file
72
app/test-api/page.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
export default function TestAPIPage() {
|
||||
const [competitionId, setCompetitionId] = useState('be47d842-91f1-11f0-8595-bd825523ae01')
|
||||
const [results, setResults] = useState<any>({})
|
||||
|
||||
const testAPI = async (endpoint: string, name: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/competitions/${competitionId}/${endpoint}`)
|
||||
const data = await response.json()
|
||||
setResults(prev => ({ ...prev, [name]: data }))
|
||||
console.log(`${name} API回應:`, data)
|
||||
} catch (error) {
|
||||
console.error(`${name} API錯誤:`, error)
|
||||
setResults(prev => ({ ...prev, [name]: { error: error.message } }))
|
||||
}
|
||||
}
|
||||
|
||||
const testAllAPIs = async () => {
|
||||
setResults({})
|
||||
await Promise.all([
|
||||
testAPI('judges', '評審'),
|
||||
testAPI('apps', '應用'),
|
||||
testAPI('teams', '團隊')
|
||||
])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API 測試頁面</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">競賽ID:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={competitionId}
|
||||
onChange={(e) => setCompetitionId(e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button onClick={testAllAPIs}>測試所有API</Button>
|
||||
<Button onClick={() => testAPI('judges', '評審')}>測試評審API</Button>
|
||||
<Button onClick={() => testAPI('apps', '應用')}>測試應用API</Button>
|
||||
<Button onClick={() => testAPI('teams', '團隊')}>測試團隊API</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{Object.entries(results).map(([name, data]) => (
|
||||
<Card key={name}>
|
||||
<CardHeader>
|
||||
<CardTitle>{name} API 結果</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="bg-gray-100 p-4 rounded overflow-auto text-sm">
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
112
app/test-manual-scoring/page.tsx
Normal file
112
app/test-manual-scoring/page.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { CompetitionProvider } from '@/contexts/competition-context'
|
||||
|
||||
export default function TestManualScoringPage() {
|
||||
const [competition, setCompetition] = useState<any>(null)
|
||||
const [teams, setTeams] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
loadCompetitionData()
|
||||
}, [])
|
||||
|
||||
const loadCompetitionData = async () => {
|
||||
try {
|
||||
console.log('🔍 開始載入競賽數據...')
|
||||
|
||||
// 載入競賽信息
|
||||
const competitionResponse = await fetch('/api/competitions/be4b0a71-91f1-11f0-bb38-4adff2d0e33e')
|
||||
const competitionData = await competitionResponse.json()
|
||||
|
||||
if (competitionData.success) {
|
||||
setCompetition(competitionData.data.competition)
|
||||
console.log('✅ 競賽載入成功:', competitionData.data.competition.name)
|
||||
}
|
||||
|
||||
// 載入團隊數據
|
||||
const teamsResponse = await fetch('/api/competitions/be4b0a71-91f1-11f0-bb38-4adff2d0e33e/teams')
|
||||
const teamsData = await teamsResponse.json()
|
||||
|
||||
if (teamsData.success) {
|
||||
setTeams(teamsData.data.teams)
|
||||
console.log('✅ 團隊載入成功:', teamsData.data.teams.length, '個團隊')
|
||||
teamsData.data.teams.forEach((team: any) => {
|
||||
console.log(` - ${team.name}: ${team.apps?.length || 0} 個APP`)
|
||||
if (team.apps && team.apps.length > 0) {
|
||||
team.apps.forEach((app: any) => {
|
||||
console.log(` * ${app.name} (${app.id})`)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 載入數據失敗:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-8">載入中...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<CompetitionProvider>
|
||||
<div className="p-8">
|
||||
<h1 className="text-2xl font-bold mb-4">測試手動評分數據載入</h1>
|
||||
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold mb-2">競賽信息</h2>
|
||||
{competition ? (
|
||||
<div className="bg-gray-100 p-4 rounded">
|
||||
<p><strong>名稱:</strong> {competition.name}</p>
|
||||
<p><strong>類型:</strong> {competition.type}</p>
|
||||
<p><strong>狀態:</strong> {competition.status}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-red-500">競賽數據載入失敗</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold mb-2">團隊數據</h2>
|
||||
{teams.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{teams.map((team) => (
|
||||
<div key={team.id} className="bg-gray-100 p-4 rounded">
|
||||
<h3 className="font-semibold">{team.name}</h3>
|
||||
<p><strong>ID:</strong> {team.id}</p>
|
||||
<p><strong>APP數量:</strong> {team.apps?.length || 0}</p>
|
||||
{team.apps && team.apps.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<h4 className="font-medium">APP列表:</h4>
|
||||
<ul className="ml-4">
|
||||
{team.apps.map((app: any) => (
|
||||
<li key={app.id} className="text-sm">
|
||||
• {app.name} ({app.id})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-red-500">團隊數據載入失敗</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold mb-2">手動評分測試</h2>
|
||||
<p className="text-gray-600">
|
||||
請檢查瀏覽器控制台的日誌,查看數據載入情況。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CompetitionProvider>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user