- Fixed unused function parameters by prefixing with underscore - Removed unused imports and variables - Applied eslint --fix for automatic style fixes - Property shorthand - String template literals - Prefer const over let where appropriate - Spacing and formatting Reduces lint errors from 108+ to 78 (61 unused vars, 17 other issues) Related to CI lint failures in previous commit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
597 lines
15 KiB
JavaScript
597 lines
15 KiB
JavaScript
/**
|
|
* Submissions Controller
|
|
* API endpoints for submission tracking
|
|
*/
|
|
|
|
const SubmissionTracking = require('../models/SubmissionTracking.model');
|
|
const BlogPost = require('../models/BlogPost.model');
|
|
const publicationTargets = require('../config/publication-targets.config');
|
|
|
|
/**
|
|
* POST /api/submissions
|
|
* Create new submission tracking entry
|
|
*/
|
|
async function createSubmission(req, res) {
|
|
try {
|
|
const {
|
|
blogPostId,
|
|
publicationId,
|
|
submissionMethod,
|
|
submissionEmail,
|
|
submissionUrl,
|
|
notes
|
|
} = req.body;
|
|
|
|
// Validate blog post exists
|
|
const blogPost = await BlogPost.findById(blogPostId);
|
|
if (!blogPost) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Blog post not found'
|
|
});
|
|
}
|
|
|
|
// Validate publication exists
|
|
const publication = publicationTargets[publicationId];
|
|
if (!publication) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Publication not found'
|
|
});
|
|
}
|
|
|
|
// Create submission tracking entry
|
|
const submission = await SubmissionTracking.create({
|
|
blogPostId,
|
|
publicationId,
|
|
publicationName: publication.name,
|
|
title: blogPost.title,
|
|
wordCount: blogPost.content ? blogPost.content.split(/\s+/).length : 0,
|
|
contentType: publication.type,
|
|
submissionMethod: submissionMethod || publication.submission?.method,
|
|
submissionEmail: submissionEmail || publication.submission?.email,
|
|
submissionUrl: submissionUrl || publication.submission?.url,
|
|
expectedResponseDays: publication.submission?.responseTime?.max || null,
|
|
createdBy: req.user._id,
|
|
notes: notes ? [{ content: notes, author: req.user._id }] : []
|
|
});
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: submission
|
|
});
|
|
} catch (error) {
|
|
console.error('[Submissions] Create submission error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to create submission tracking'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/submissions
|
|
* Get all submissions with optional filtering
|
|
*/
|
|
async function getSubmissions(req, res) {
|
|
console.log('[SUBMISSIONS DEBUG] getSubmissions called, query:', req.query);
|
|
try {
|
|
const { status, publicationId, limit = 50, offset = 0 } = req.query;
|
|
|
|
const query = {};
|
|
if (status) query.status = status;
|
|
if (publicationId) query.publicationId = publicationId;
|
|
|
|
// NOTE: BlogPost and User are native MongoDB classes, not Mongoose models,
|
|
// so we can't use .populate() for them. Fetch data separately if needed.
|
|
const submissions = await SubmissionTracking.find(query)
|
|
.sort({ submittedAt: -1, createdAt: -1 })
|
|
.limit(parseInt(limit, 10))
|
|
.skip(parseInt(offset, 10));
|
|
|
|
// Manually fetch blog post titles for submissions that have blogPostId
|
|
const submissionsWithBlogData = await Promise.all(
|
|
submissions.map(async submission => {
|
|
const submissionObj = submission.toObject();
|
|
|
|
if (submissionObj.blogPostId) {
|
|
try {
|
|
const blogPost = await BlogPost.findById(submissionObj.blogPostId);
|
|
if (blogPost) {
|
|
submissionObj.blogPost = {
|
|
title: blogPost.title,
|
|
slug: blogPost.slug
|
|
};
|
|
}
|
|
} catch (err) {
|
|
console.error(`[Submissions] Error fetching blog post ${submissionObj.blogPostId}:`, err.message);
|
|
}
|
|
}
|
|
|
|
return submissionObj;
|
|
})
|
|
);
|
|
|
|
const total = await SubmissionTracking.countDocuments(query);
|
|
|
|
res.json({
|
|
success: true,
|
|
count: submissionsWithBlogData.length,
|
|
total,
|
|
data: submissionsWithBlogData
|
|
});
|
|
} catch (error) {
|
|
console.error('[Submissions] Get submissions error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch submissions'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/submissions/:id
|
|
* Get specific submission by ID
|
|
*/
|
|
async function getSubmissionById(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const submission = await SubmissionTracking.findById(id);
|
|
|
|
if (!submission) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Submission not found'
|
|
});
|
|
}
|
|
|
|
const submissionObj = submission.toObject();
|
|
|
|
// Manually fetch blog post data if blogPostId exists
|
|
if (submissionObj.blogPostId) {
|
|
try {
|
|
const blogPost = await BlogPost.findById(submissionObj.blogPostId);
|
|
if (blogPost) {
|
|
submissionObj.blogPost = {
|
|
title: blogPost.title,
|
|
slug: blogPost.slug,
|
|
content: blogPost.content
|
|
};
|
|
}
|
|
} catch (err) {
|
|
console.error(`[Submissions] Error fetching blog post:`, err.message);
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: submissionObj
|
|
});
|
|
} catch (error) {
|
|
console.error('[Submissions] Get submission by ID error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch submission'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PUT /api/submissions/:id/status
|
|
* Update submission status
|
|
*/
|
|
async function updateSubmissionStatus(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { status, feedback, reasonForRejection, publishedUrl, publishedTitle } = req.body;
|
|
|
|
const submission = await SubmissionTracking.findById(id);
|
|
if (!submission) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Submission not found'
|
|
});
|
|
}
|
|
|
|
// Update status with automatic timestamp management
|
|
await submission.updateStatus(status, req.user._id);
|
|
|
|
// Update additional fields if provided
|
|
if (feedback) submission.feedback = feedback;
|
|
if (reasonForRejection) submission.reasonForRejection = reasonForRejection;
|
|
if (publishedUrl) submission.publishedUrl = publishedUrl;
|
|
if (publishedTitle) submission.publishedTitle = publishedTitle;
|
|
|
|
await submission.save();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: submission
|
|
});
|
|
} catch (error) {
|
|
console.error('[Submissions] Update status error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to update submission status'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/submissions/:id/notes
|
|
* Add note to submission
|
|
*/
|
|
async function addSubmissionNote(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { content } = req.body;
|
|
|
|
if (!content) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Note content is required'
|
|
});
|
|
}
|
|
|
|
const submission = await SubmissionTracking.findById(id);
|
|
if (!submission) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Submission not found'
|
|
});
|
|
}
|
|
|
|
await submission.addNote(content, req.user._id);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: submission
|
|
});
|
|
} catch (error) {
|
|
console.error('[Submissions] Add note error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to add note'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/submissions/statistics
|
|
* Get submission statistics
|
|
*/
|
|
async function getSubmissionStatistics(req, res) {
|
|
try {
|
|
const stats = await SubmissionTracking.getStatistics();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: stats
|
|
});
|
|
} catch (error) {
|
|
console.error('[Submissions] Get statistics error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch statistics'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/submissions/publication/:publicationId
|
|
* Get submissions for specific publication with acceptance rate
|
|
*/
|
|
async function getSubmissionsByPublication(req, res) {
|
|
try {
|
|
const { publicationId } = req.params;
|
|
|
|
const submissions = await SubmissionTracking.getByPublication(publicationId);
|
|
const acceptanceRate = await SubmissionTracking.getAcceptanceRate(publicationId);
|
|
const avgResponseTime = await SubmissionTracking.getAverageResponseTime(publicationId);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
submissions,
|
|
acceptanceRate: Math.round(acceptanceRate * 10) / 10,
|
|
avgResponseTimeHours: avgResponseTime
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('[Submissions] Get by publication error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch submissions for publication'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/submissions/by-blog-post/:blogPostId
|
|
* Get submission by blog post ID
|
|
*/
|
|
async function getSubmissionByBlogPost(req, res) {
|
|
try {
|
|
const { blogPostId } = req.params;
|
|
|
|
const submission = await SubmissionTracking.findOne({ blogPostId });
|
|
|
|
if (!submission) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'No submission found for this blog post'
|
|
});
|
|
}
|
|
|
|
const submissionObj = submission.toObject();
|
|
|
|
// Manually fetch blog post data
|
|
try {
|
|
const blogPost = await BlogPost.findById(blogPostId);
|
|
if (blogPost) {
|
|
submissionObj.blogPost = {
|
|
title: blogPost.title,
|
|
slug: blogPost.slug,
|
|
content: blogPost.content
|
|
};
|
|
}
|
|
} catch (err) {
|
|
console.error(`[Submissions] Error fetching blog post:`, err.message);
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: submissionObj
|
|
});
|
|
} catch (error) {
|
|
console.error('[Submissions] Get by blog post error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch submission'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PUT /api/submissions/:id
|
|
* Update submission entry
|
|
*/
|
|
async function updateSubmission(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const updateData = req.body;
|
|
|
|
const submission = await SubmissionTracking.findById(id);
|
|
if (!submission) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Submission not found'
|
|
});
|
|
}
|
|
|
|
// Update documents if provided
|
|
if (updateData.documents) {
|
|
for (const [docType, docData] of Object.entries(updateData.documents)) {
|
|
if (docData.versions && docData.versions.length > 0) {
|
|
const version = docData.versions[0];
|
|
await submission.setDocumentVersion(
|
|
docType,
|
|
version.language,
|
|
version.content,
|
|
{
|
|
translatedBy: version.translatedBy,
|
|
approved: version.approved
|
|
}
|
|
);
|
|
}
|
|
}
|
|
delete updateData.documents; // Remove from update data since it's handled separately
|
|
}
|
|
|
|
// Update other fields
|
|
Object.assign(submission, updateData);
|
|
submission.lastUpdatedBy = req.user._id;
|
|
|
|
await submission.save();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: submission
|
|
});
|
|
} catch (error) {
|
|
console.error('[Submissions] Update submission error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to update submission'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/submissions/:id/export
|
|
* Export submission package
|
|
*/
|
|
async function exportSubmission(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { format = 'json' } = req.query;
|
|
|
|
const submission = await SubmissionTracking.findById(id);
|
|
|
|
if (!submission) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Submission not found'
|
|
});
|
|
}
|
|
|
|
const language = req.query.language || 'en';
|
|
const packageData = submission.exportPackage(language);
|
|
|
|
// Add blog post data if blogPostId exists
|
|
if (submission.blogPostId) {
|
|
try {
|
|
const blogPost = await BlogPost.findById(submission.blogPostId);
|
|
if (blogPost) {
|
|
packageData.blogPost = {
|
|
title: blogPost.title,
|
|
content: blogPost.content
|
|
};
|
|
}
|
|
} catch (err) {
|
|
console.error(`[Submissions] Error fetching blog post:`, err.message);
|
|
}
|
|
}
|
|
|
|
if (format === 'json') {
|
|
res.json({
|
|
success: true,
|
|
data: packageData
|
|
});
|
|
} else if (format === 'text') {
|
|
// For now, return JSON with instructions
|
|
// In future, could zip individual text files
|
|
res.json({
|
|
success: true,
|
|
data: packageData,
|
|
note: 'Text export feature coming soon. Please use JSON export for now.'
|
|
});
|
|
} else {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: 'Invalid format. Use "json" or "text"'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('[Submissions] Export submission error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to export submission'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/submissions/:id
|
|
* Delete submission tracking entry
|
|
*/
|
|
async function deleteSubmission(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const submission = await SubmissionTracking.findByIdAndDelete(id);
|
|
if (!submission) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Submission not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Submission tracking deleted'
|
|
});
|
|
} catch (error) {
|
|
console.error('[Submissions] Delete submission error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to delete submission'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/submissions/:id/translate
|
|
* Translate document using DeepL
|
|
*/
|
|
async function translateDocument(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { docType, fromLang, toLang, text } = req.body;
|
|
|
|
// Validate inputs
|
|
if (!docType || !fromLang || !toLang || !text) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Missing required fields: docType, fromLang, toLang, text'
|
|
});
|
|
}
|
|
|
|
// Load translation service
|
|
const translationService = require('../services/Translation.service').getInstance();
|
|
|
|
// Check if service is available
|
|
const status = translationService.getStatus();
|
|
if (!status.available) {
|
|
return res.status(503).json({
|
|
success: false,
|
|
error: 'Translation service not available. Please configure DEEPL_API_KEY in .env file.'
|
|
});
|
|
}
|
|
|
|
console.log(`[Translation] ${fromLang} → ${toLang} for ${docType} (${text.length} chars)`);
|
|
|
|
// Perform translation
|
|
const result = await translationService.translate(text, fromLang, toLang);
|
|
|
|
if (result.error) {
|
|
return res.status(500).json({
|
|
success: false,
|
|
error: result.errorMessage || 'Translation failed'
|
|
});
|
|
}
|
|
|
|
// Save translation to submission
|
|
const submission = await SubmissionTracking.findById(id);
|
|
if (!submission) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Submission not found'
|
|
});
|
|
}
|
|
|
|
// Add/update the translated version
|
|
await submission.setDocumentVersion(
|
|
docType,
|
|
toLang,
|
|
result.translatedText,
|
|
{
|
|
translatedBy: 'deepl',
|
|
approved: false
|
|
}
|
|
);
|
|
|
|
console.log(`[Translation] Saved ${toLang} version of ${docType} to submission ${id}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
translatedText: result.translatedText,
|
|
cached: result.cached,
|
|
detectedLang: result.detectedLang,
|
|
service: 'DeepL'
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('[Translation] Error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message || 'Translation failed'
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
createSubmission,
|
|
getSubmissions,
|
|
getSubmissionById,
|
|
getSubmissionByBlogPost,
|
|
updateSubmission,
|
|
updateSubmissionStatus,
|
|
addSubmissionNote,
|
|
getSubmissionStatistics,
|
|
getSubmissionsByPublication,
|
|
exportSubmission,
|
|
deleteSubmission,
|
|
translateDocument
|
|
};
|