完成評審評分機制

This commit is contained in:
2025-09-18 18:34:31 +08:00
parent 2101767690
commit ffa1e45f63
54 changed files with 5730 additions and 709 deletions

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View File

@@ -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,

View 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
View 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 });
}
}

View 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 });
}
}

View 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
View 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
View 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>
)
}

View File

@@ -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
View 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>
)
}

View 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>
)
}