實作完整分享、刪除、下載報告功能

This commit is contained in:
2025-09-24 00:01:37 +08:00
parent 69c9323038
commit 99ff9654d9
35 changed files with 4779 additions and 45 deletions

View File

@@ -0,0 +1,114 @@
import { NextRequest, NextResponse } from 'next/server';
import { EvaluationService, ProjectService, ProjectFileService, ProjectWebsiteService } from '@/lib/services/database';
import { unlink } from 'fs/promises';
import { join } from 'path';
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const evaluationId = parseInt(params.id);
if (isNaN(evaluationId)) {
return NextResponse.json(
{ success: false, error: '無效的評審ID' },
{ status: 400 }
);
}
console.log(`🗑️ 開始刪除評審記錄: ID=${evaluationId}`);
// 獲取評審記錄以找到對應的專案
const evaluation = await EvaluationService.findById(evaluationId);
if (!evaluation) {
return NextResponse.json(
{ success: false, error: '找不到指定的評審記錄' },
{ status: 404 }
);
}
const projectId = evaluation.project_id;
console.log(`📋 找到對應專案: ID=${projectId}`);
// 獲取專案信息
const project = await ProjectService.findById(projectId);
if (!project) {
return NextResponse.json(
{ success: false, error: '找不到對應的專案' },
{ status: 404 }
);
}
// 獲取專案文件列表
const projectFiles = await ProjectFileService.findByProjectId(projectId);
console.log(`📁 找到 ${projectFiles.length} 個專案文件`);
// 開始事務性刪除
try {
// 1. 刪除評審相關數據(這些會因為外鍵約束自動刪除)
// - evaluation_scores (CASCADE)
// - evaluation_feedback (CASCADE)
await EvaluationService.delete(evaluationId);
console.log(`✅ 已刪除評審記錄: ID=${evaluationId}`);
// 2. 刪除專案網站記錄
const projectWebsites = await ProjectWebsiteService.findByProjectId(projectId);
for (const website of projectWebsites) {
await ProjectWebsiteService.delete(website.id);
}
console.log(`✅ 已刪除 ${projectWebsites.length} 個專案網站記錄`);
// 3. 刪除專案文件記錄和實際文件
for (const file of projectFiles) {
try {
// 刪除實際文件
const filePath = join(process.cwd(), file.file_path);
await unlink(filePath);
console.log(`🗑️ 已刪除文件: ${file.original_name}`);
} catch (fileError) {
console.warn(`⚠️ 刪除文件失敗: ${file.original_name}`, fileError);
// 繼續刪除其他文件,不中斷整個流程
}
// 刪除文件記錄
await ProjectFileService.delete(file.id);
}
console.log(`✅ 已刪除 ${projectFiles.length} 個專案文件記錄`);
// 4. 刪除專案記錄
await ProjectService.delete(projectId);
console.log(`✅ 已刪除專案記錄: ID=${projectId}`);
console.log(`🎉 成功刪除評審報告: 專案=${project.title}`);
return NextResponse.json({
success: true,
message: '評審報告已成功刪除',
data: {
projectId: projectId,
projectTitle: project.title,
deletedFiles: projectFiles.length,
deletedWebsites: projectWebsites.length
}
});
} catch (deleteError) {
console.error('❌ 刪除過程中發生錯誤:', deleteError);
return NextResponse.json(
{
success: false,
error: '刪除過程中發生錯誤,請稍後再試'
},
{ status: 500 }
);
}
} catch (error) {
console.error('❌ 刪除評審記錄失敗:', error);
return NextResponse.json(
{ success: false, error: '刪除評審記錄失敗' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,236 @@
import { NextRequest, NextResponse } from 'next/server';
import { EvaluationService } from '@/lib/services/database';
import { generateHTMLPDFReport, type PDFReportData } from '@/lib/utils/html-pdf-generator';
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const evaluationId = parseInt(params.id);
if (isNaN(evaluationId)) {
return NextResponse.json(
{ success: false, error: '無效的評審ID' },
{ status: 400 }
);
}
console.log(`📄 開始生成 PDF 報告: ID=${evaluationId}`);
// 獲取評審詳細數據
const evaluationWithDetails = await EvaluationService.findWithDetails(evaluationId);
if (!evaluationWithDetails) {
return NextResponse.json(
{ success: false, error: '找不到指定的評審記錄' },
{ status: 404 }
);
}
console.log(`✅ 成功獲取評審數據: 專案=${evaluationWithDetails.project?.title}`);
// 處理反饋數據,按類型分組
const feedbackByType = {
criteria: evaluationWithDetails.feedback?.filter(f => f.feedback_type === 'criteria') || [],
strength: evaluationWithDetails.feedback?.filter(f => f.feedback_type === 'strength') || [],
improvement: evaluationWithDetails.feedback?.filter(f => f.feedback_type === 'improvement') || [],
overall: evaluationWithDetails.feedback?.filter(f => f.feedback_type === 'overall') || []
};
// 為每個評分標準獲取對應的 AI 評語、優點和改進建議
const criteriaWithFeedback = evaluationWithDetails.scores?.map(score => {
const criteriaId = score.criteria_item_id;
const criteriaName = score.criteria_item_name || '未知項目';
// 獲取該評分標準的 AI 評語
const criteriaFeedback = feedbackByType.criteria
.filter(f => f.criteria_item_id === criteriaId)
.map(f => f.content)
.filter((content, index, arr) => arr.indexOf(content) === index)[0] || '無評語';
// 獲取該評分標準的優點
const strengths = feedbackByType.strength
.filter(f => f.criteria_item_id === criteriaId)
.map(f => f.content)
.filter((content, index, arr) => arr.indexOf(content) === index);
// 獲取該評分標準的改進建議
const improvements = feedbackByType.improvement
.filter(f => f.criteria_item_id === criteriaId)
.map(f => f.content)
.filter((content, index, arr) => arr.indexOf(content) === index);
return {
name: criteriaName,
score: Number(score.score) || 0,
maxScore: Number(score.max_score) || 10,
weight: Number(score.weight) || 1,
weightedScore: Number(score.weighted_score) || 0,
percentage: Number(score.percentage) || 0,
feedback: criteriaFeedback,
strengths: strengths,
improvements: improvements
};
}) || [];
// 構建 PDF 報告數據
const pdfData: PDFReportData = {
projectTitle: evaluationWithDetails.project?.title || '未知專案',
overallScore: Number(evaluationWithDetails.overall_score) || 0,
totalPossible: Number(evaluationWithDetails.max_possible_score) || 100,
grade: evaluationWithDetails.grade || 'N/A',
analysisDate: evaluationWithDetails.created_at ? new Date(evaluationWithDetails.created_at).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
criteria: criteriaWithFeedback,
overview: {
excellentItems: evaluationWithDetails.excellent_items || 0,
improvementItems: evaluationWithDetails.improvement_items || 0,
overallPerformance: Number(evaluationWithDetails.overall_score) || 0
},
improvementSuggestions: {
overallSuggestions: feedbackByType.overall[0]?.content || '無詳細分析',
maintainStrengths: feedbackByType.strength
.filter(f => f.criteria_item_id === null)
.map(f => {
const content = f.content || '無描述';
const colonIndex = content.indexOf(':');
const title = colonIndex > -1 ? content.substring(0, colonIndex + 1) : '系統優勢:';
const description = colonIndex > -1 ? content.substring(colonIndex + 1).trim() : content;
return {
title: title,
description: description
};
}),
keyImprovements: (() => {
const allImprovementData = feedbackByType.improvement
.filter(f => f.criteria_item_id === null);
const improvementData = allImprovementData.length >= 5
? allImprovementData.slice(1, -3)
: allImprovementData;
const tempGroups = [];
let currentGroup = null;
for (const item of improvementData) {
const content = item.content || '';
const colonIndex = content.indexOf(':');
if (colonIndex > -1) {
const title = content.substring(0, colonIndex + 1);
const remainingContent = content.substring(colonIndex + 1).trim();
const suggestionIndex = remainingContent.indexOf('建議:');
if (suggestionIndex > -1) {
if (currentGroup) {
tempGroups.push(currentGroup);
}
currentGroup = {
title: title,
subtitle: '建議:',
suggestions: []
};
const suggestionsText = remainingContent.substring(suggestionIndex + 3).trim();
const suggestions = suggestionsText.split('\n')
.map(s => s.trim())
.filter(s => s.startsWith('•'))
.map(s => s.substring(1).trim());
currentGroup.suggestions = suggestions;
} else if (remainingContent.startsWith('•')) {
if (currentGroup) {
const suggestion = remainingContent.substring(1).trim();
currentGroup.suggestions.push(suggestion);
}
} else {
if (currentGroup) {
tempGroups.push(currentGroup);
}
currentGroup = {
title: title,
subtitle: '',
suggestions: [remainingContent]
};
}
} else {
if (currentGroup) {
tempGroups.push(currentGroup);
}
if (content.startsWith('•')) {
if (currentGroup) {
const suggestion = content.substring(1).trim();
currentGroup.suggestions.push(suggestion);
} else {
currentGroup = {
title: '改進方向:',
subtitle: '',
suggestions: [content.substring(1).trim()]
};
}
} else {
currentGroup = {
title: '改進方向:',
subtitle: '',
description: content,
suggestions: []
};
}
}
}
if (currentGroup) {
tempGroups.push(currentGroup);
}
return tempGroups;
})(),
actionPlan: feedbackByType.improvement
.filter(f => f.criteria_item_id === null)
.slice(-3)
.map((f, index) => {
const content = f.content || '無描述';
const colonIndex = content.indexOf(':');
const phase = colonIndex > -1 ? content.substring(0, colonIndex + 1) : `階段 ${index + 1}:`;
const description = colonIndex > -1 ? content.substring(colonIndex + 1).trim() : content;
return {
phase: phase,
description: description
};
})
}
};
console.log(`📋 開始生成 PDF 報告: 專案=${pdfData.projectTitle}`);
// 生成 PDF
const pdfBlob = await generateHTMLPDFReport(pdfData);
console.log(`✅ PDF 報告生成完成: 大小=${pdfBlob.size} bytes`);
// 返回 PDF 文件
const fileName = `評審報告_${pdfData.projectTitle}_${pdfData.analysisDate}.pdf`;
return new NextResponse(pdfBlob, {
status: 200,
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
'Content-Length': pdfBlob.size.toString(),
},
});
} catch (error) {
console.error('❌ 生成 PDF 報告失敗:', error);
return NextResponse.json(
{ success: false, error: '生成 PDF 報告失敗' },
{ status: 500 }
);
}
}