/** * Submission Tracking Model * Track submission lifecycle for publications */ const mongoose = require('mongoose'); const SubmissionTrackingSchema = new mongoose.Schema({ // Submission identification blogPostId: { type: mongoose.Schema.Types.ObjectId, ref: 'BlogPost', required: false, // Optional for standalone submission packages index: true }, publicationId: { type: String, required: true, index: true }, publicationName: { type: String, required: true }, // Content details title: { type: String, required: true }, wordCount: { type: Number }, contentType: { type: String, enum: ['letter', 'oped', 'essay', 'social'], required: true }, // Submission lifecycle status: { type: String, enum: [ 'drafted', // Content created, not yet submitted 'ready', // Ready to submit 'submitted', // Submitted to publication 'under_review', // Acknowledged by publication 'revision_requested', // Publication requested changes 'revised', // Changes made, awaiting re-submission 'accepted', // Accepted for publication 'rejected', // Rejected by publication 'published', // Successfully published 'withdrawn' // Submission withdrawn ], default: 'drafted', index: true }, // Timeline draftedAt: { type: Date, default: Date.now }, submittedAt: { type: Date }, reviewStartedAt: { type: Date }, acceptedAt: { type: Date }, rejectedAt: { type: Date }, publishedAt: { type: Date }, // Publication details publishedUrl: { type: String }, publishedTitle: { type: String }, // May differ from submitted title edits: [{ type: { type: String, enum: ['minor', 'moderate', 'major'] }, description: String, date: { type: Date, default: Date.now } }], // Submission method submissionMethod: { type: String, enum: ['email', 'form', 'website', 'self-publish'] }, submissionEmail: { type: String }, submissionUrl: { type: String }, // Response tracking responseTimeHours: { type: Number }, // Time from submission to response expectedResponseDays: { type: Number }, // Editorial feedback editorName: { type: String }, editorContact: { type: String }, feedback: { type: String }, reasonForRejection: { type: String }, // Internal notes notes: [{ content: String, author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, createdAt: { type: Date, default: Date.now } }], // Submission package checklist submissionPackage: { coverLetter: { completed: { type: Boolean, default: false }, content: String, lastUpdated: Date }, notesToEditor: { completed: { type: Boolean, default: false }, content: String, lastUpdated: Date }, authorBio: { completed: { type: Boolean, default: false }, content: String, lastUpdated: Date }, pitchEmail: { completed: { type: Boolean, default: false }, content: String, lastUpdated: Date }, supportingMaterials: [{ name: String, description: String, url: String, completed: { type: Boolean, default: false } }] }, // Multilingual document storage for complete submission packages documents: { coverLetter: { primaryLanguage: { type: String, default: 'en' }, versions: [{ language: { type: String, required: true, enum: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'ar', 'mi'] }, content: String, wordCount: Number, lastUpdated: { type: Date, default: Date.now }, translatedBy: { type: String, enum: ['human', 'deepl', 'claude', 'google', 'manual'] }, approved: { type: Boolean, default: false } }], completed: { type: Boolean, default: false } }, mainArticle: { primaryLanguage: { type: String, default: 'en' }, versions: [{ language: String, content: String, wordCount: Number, lastUpdated: { type: Date, default: Date.now }, translatedBy: String, approved: { type: Boolean, default: false } }], completed: { type: Boolean, default: false } }, authorBio: { primaryLanguage: { type: String, default: 'en' }, versions: [{ language: String, content: String, wordCount: Number, lastUpdated: { type: Date, default: Date.now }, translatedBy: String, approved: { type: Boolean, default: false } }], completed: { type: Boolean, default: false } }, technicalBrief: { primaryLanguage: { type: String, default: 'en' }, versions: [{ language: String, content: String, wordCount: Number, lastUpdated: { type: Date, default: Date.now }, translatedBy: String, approved: { type: Boolean, default: false } }], completed: { type: Boolean, default: false } } }, // Metadata createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, lastUpdatedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } }, { timestamps: true }); // Indexes for performance SubmissionTrackingSchema.index({ status: 1, submittedAt: -1 }); SubmissionTrackingSchema.index({ publicationId: 1, status: 1 }); SubmissionTrackingSchema.index({ createdBy: 1, status: 1 }); // Virtual for submission duration SubmissionTrackingSchema.virtual('submissionDurationDays').get(function() { if (!this.submittedAt) return null; const endDate = this.publishedAt || this.rejectedAt || new Date(); return Math.floor((endDate - this.submittedAt) / (1000 * 60 * 60 * 24)); }); // Static methods /** * Get submissions by status * NOTE: BlogPost is a native MongoDB class, not Mongoose model, so we can't populate it */ SubmissionTrackingSchema.statics.getByStatus = async function(status) { return await this.find({ status }) .populate('createdBy', 'email') .sort({ submittedAt: -1 }); }; /** * Get submissions for a specific publication * NOTE: BlogPost is a native MongoDB class, not Mongoose model, so we can't populate it */ SubmissionTrackingSchema.statics.getByPublication = async function(publicationId) { return await this.find({ publicationId }) .sort({ submittedAt: -1 }); }; /** * Get acceptance rate for a publication */ SubmissionTrackingSchema.statics.getAcceptanceRate = async function(publicationId) { const total = await this.countDocuments({ publicationId, status: { $in: ['accepted', 'rejected', 'published'] } }); const accepted = await this.countDocuments({ publicationId, status: { $in: ['accepted', 'published'] } }); return total > 0 ? (accepted / total) * 100 : 0; }; /** * Get average response time for a publication */ SubmissionTrackingSchema.statics.getAverageResponseTime = async function(publicationId) { const result = await this.aggregate([ { $match: { publicationId, responseTimeHours: { $exists: true, $gt: 0 } } }, { $group: { _id: null, avgResponseTime: { $avg: '$responseTimeHours' } } } ]); return result.length > 0 ? Math.round(result[0].avgResponseTime) : null; }; /** * Get submission statistics */ SubmissionTrackingSchema.statics.getStatistics = async function() { const total = await this.countDocuments(); const byStatus = await this.aggregate([ { $group: { _id: '$status', count: { $sum: 1 } } } ]); const byPublication = await this.aggregate([ { $group: { _id: '$publicationId', count: { $sum: 1 }, accepted: { $sum: { $cond: [{ $in: ['$status', ['accepted', 'published']] }, 1, 0] } } } }, { $sort: { count: -1 } }, { $limit: 10 } ]); return { total, byStatus: byStatus.reduce((acc, item) => { acc[item._id] = item.count; return acc; }, {}), topPublications: byPublication }; }; // Instance methods /** * Update status with automatic timestamp management */ SubmissionTrackingSchema.methods.updateStatus = async function(newStatus, userId) { this.status = newStatus; this.lastUpdatedBy = userId; // Set appropriate timestamps const now = new Date(); switch (newStatus) { case 'submitted': if (!this.submittedAt) this.submittedAt = now; break; case 'under_review': if (!this.reviewStartedAt) this.reviewStartedAt = now; // Calculate response time if not already set if (this.submittedAt && !this.responseTimeHours) { this.responseTimeHours = Math.round((now - this.submittedAt) / (1000 * 60 * 60)); } break; case 'accepted': if (!this.acceptedAt) this.acceptedAt = now; if (this.submittedAt && !this.responseTimeHours) { this.responseTimeHours = Math.round((now - this.submittedAt) / (1000 * 60 * 60)); } break; case 'rejected': if (!this.rejectedAt) this.rejectedAt = now; if (this.submittedAt && !this.responseTimeHours) { this.responseTimeHours = Math.round((now - this.submittedAt) / (1000 * 60 * 60)); } break; case 'published': if (!this.publishedAt) this.publishedAt = now; break; } return await this.save(); }; /** * Add note */ SubmissionTrackingSchema.methods.addNote = async function(content, authorId) { this.notes.push({ content, author: authorId, createdAt: new Date() }); return await this.save(); }; /** * Get document in specific language (with fallback) */ SubmissionTrackingSchema.methods.getDocument = function(docType, language, fallbackToDefault = true) { const doc = this.documents?.[docType]; if (!doc) return null; // Find version in requested language let version = doc.versions.find(v => v.language === language); // Fallback to primary language if requested if (!version && fallbackToDefault) { version = doc.versions.find(v => v.language === doc.primaryLanguage); } return version; }; /** * Add or update document version */ SubmissionTrackingSchema.methods.setDocumentVersion = async function(docType, language, content, metadata = {}) { if (!this.documents) { this.documents = {}; } if (!this.documents[docType]) { this.documents[docType] = { primaryLanguage: language, versions: [], completed: false }; } // Find existing version or create new const existingIndex = this.documents[docType].versions.findIndex(v => v.language === language); const versionData = { language, content, wordCount: content.split(/\s+/).length, lastUpdated: new Date(), ...metadata }; if (existingIndex >= 0) { this.documents[docType].versions[existingIndex] = { ...this.documents[docType].versions[existingIndex], ...versionData }; } else { this.documents[docType].versions.push(versionData); } return await this.save(); }; /** * Get all available languages for a document type */ SubmissionTrackingSchema.methods.getAvailableLanguages = function(docType) { const doc = this.documents?.[docType]; if (!doc) return []; return doc.versions.map(v => v.language); }; /** * Check if submission package is complete for a language */ SubmissionTrackingSchema.methods.isPackageComplete = function(language) { const requiredDocs = ['coverLetter', 'mainArticle', 'authorBio']; return requiredDocs.every(docType => { const doc = this.documents?.[docType]; if (!doc) return false; return doc.versions.some(v => v.language === language); }); }; /** * Export package for submission */ SubmissionTrackingSchema.methods.exportPackage = function(language) { const packageData = { publication: { id: this.publicationId, name: this.publicationName }, metadata: { title: this.title, wordCount: this.wordCount, language, exportedAt: new Date() }, documents: {} }; ['coverLetter', 'mainArticle', 'authorBio', 'technicalBrief'].forEach(docType => { const version = this.getDocument(docType, language, true); if (version) { packageData.documents[docType] = { content: version.content, wordCount: version.wordCount, language: version.language }; } }); return packageData; }; const SubmissionTracking = mongoose.model('SubmissionTracking', SubmissionTrackingSchema); module.exports = SubmissionTracking;