const mongoose = require('mongoose'); const Schema = mongoose.Schema; // Sub-schema: Content const PosterContentSchema = new Schema({ title: { type: String, maxlength: 500 }, subtitle: { type: String, maxlength: 500 }, eventDate: { type: String, maxlength: 200 }, eventTime: { type: String, maxlength: 100 }, location: { type: String, maxlength: 500 }, contacts: { type: String, maxlength: 1000 }, extraText: [{ type: String }], customFields: { type: Map, of: String } }, { _id: false }); // Sub-schema: Asset AI Params (embedded) const EmbeddedAiParamsSchema = new Schema({ prompt: { type: String }, negativePrompt: { type: String }, provider: { type: String }, model: { type: String }, seed: { type: Number }, steps: { type: Number }, cfg: { type: Number }, size: { type: String }, generatedAt: { type: Date } }, { _id: false }); // Sub-schema: Poster Asset Reference const PosterAssetSchema = new Schema({ id: { type: String }, assetId: { type: Schema.Types.ObjectId, ref: 'Asset' }, slotId: { type: String }, // per loghi sourceType: { type: String, enum: ['upload', 'ai', 'library', 'url'] }, url: { type: String }, thumbnailUrl: { type: String }, originalName: { type: String }, mimeType: { type: String }, size: { type: Number }, dimensions: { width: { type: Number }, height: { type: Number } }, aiParams: EmbeddedAiParamsSchema }, { _id: false }); // Sub-schema: Assets Container const PosterAssetsSchema = new Schema({ backgroundImage: PosterAssetSchema, mainImage: PosterAssetSchema, logos: [PosterAssetSchema] }, { _id: false }); // Sub-schema: Layer Override Style const LayerOverrideStyleSchema = new Schema({ fontSize: { type: Number }, color: { type: String }, fontWeight: { type: Number }, opacity: { type: Number }, // altri override possibili }, { _id: false }); // Sub-schema: Layer Override const LayerOverrideSchema = new Schema({ position: { x: { type: Number }, y: { type: Number }, w: { type: Number }, h: { type: Number } }, visible: { type: Boolean }, style: LayerOverrideStyleSchema }, { _id: false }); // Sub-schema: Render Output File const RenderOutputFileSchema = new Schema({ path: { type: String, required: true }, url: { type: String }, size: { type: Number }, quality: { type: Number } }, { _id: false }); // Sub-schema: Render Output const RenderOutputSchema = new Schema({ png: RenderOutputFileSchema, jpg: RenderOutputFileSchema, webp: RenderOutputFileSchema, pdf: RenderOutputFileSchema, dimensions: { width: { type: Number }, height: { type: Number } }, renderedAt: { type: Date } }, { _id: false }); // Sub-schema: History Entry const HistoryEntrySchema = new Schema({ action: { type: String, required: true, enum: ['created', 'updated', 'ai_background_generated', 'ai_main_generated', 'rendered', 'downloaded', 'shared', 'deleted'] }, timestamp: { type: Date, default: Date.now }, userId: { type: Schema.Types.ObjectId, ref: 'User' }, details: { type: Schema.Types.Mixed } }, { _id: false }); // Sub-schema: Poster Metadata const PosterMetadataSchema = new Schema({ userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, projectId: { type: Schema.Types.ObjectId, ref: 'Project' }, tags: [{ type: String }], isPublic: { type: Boolean, default: false }, isFavorite: { type: Boolean, default: false }, viewCount: { type: Number, default: 0 }, downloadCount: { type: Number, default: 0 } }, { _id: false }); // MAIN SCHEMA: Poster const PosterSchema = new Schema({ templateId: { type: Schema.Types.ObjectId, ref: 'Template', required: true, index: true }, templateSnapshot: { type: Schema.Types.Mixed }, // copia del template al momento della creazione name: { type: String, required: true, trim: true, maxlength: 300 }, description: { type: String, maxlength: 1000 }, status: { type: String, enum: ['draft', 'processing', 'completed', 'error'], default: 'draft', index: true }, content: { type: PosterContentSchema, required: true }, assets: { type: PosterAssetsSchema, default: () => ({}) }, layerOverrides: { type: Map, of: LayerOverrideSchema, default: () => new Map() }, renderOutput: RenderOutputSchema, renderEngineVersion: { type: String, default: '1.0.0' }, history: [HistoryEntrySchema], metadata: { type: PosterMetadataSchema, required: true }, errorMessage: { type: String }, // Campi dalla tua bozza originale originalPrompt: { type: String }, // prompt completo usato styleUsed: { type: String }, aspectRatio: { type: String }, provider: { type: String } }, { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }); // Indexes PosterSchema.index({ 'metadata.userId': 1, status: 1 }); PosterSchema.index({ 'metadata.tags': 1 }); PosterSchema.index({ 'metadata.isFavorite': 1, 'metadata.userId': 1 }); PosterSchema.index({ createdAt: -1 }); PosterSchema.index({ name: 'text', description: 'text' }); // Virtual: isCompleted PosterSchema.virtual('isCompleted').get(function() { return this.status === 'completed' && this.renderOutput?.png?.path; }); // Virtual: downloadUrl PosterSchema.virtual('downloadUrl').get(function() { if (this.renderOutput?.png?.path) { return `/api/posters/${this._id}/download/png`; } return null; }); // Pre-save: aggiorna history PosterSchema.pre('save', function(next) { if (this.isNew) { this.history = this.history || []; this.history.push({ action: 'created', timestamp: new Date(), userId: this.metadata.userId }); } next(); }); // Methods PosterSchema.methods.addHistory = function(action, details = {}) { this.history.push({ action, timestamp: new Date(), userId: this.metadata.userId, details }); return this; }; PosterSchema.methods.setRenderOutput = function(outputData) { this.renderOutput = { ...outputData, renderedAt: new Date() }; this.status = 'completed'; this.addHistory('rendered', { duration: outputData.duration }); return this; }; PosterSchema.methods.setError = function(errorMessage) { this.status = 'error'; this.errorMessage = errorMessage; return this; }; PosterSchema.methods.incrementDownload = async function() { this.metadata.downloadCount = (this.metadata.downloadCount || 0) + 1; this.addHistory('downloaded'); return this.save(); }; PosterSchema.methods.toggleFavorite = async function() { this.metadata.isFavorite = !this.metadata.isFavorite; return this.save(); }; // Statics PosterSchema.statics.findByUser = function(userId, options = {}) { const query = { 'metadata.userId': userId }; if (options.status) query.status = options.status; if (options.isFavorite) query['metadata.isFavorite'] = true; return this.find(query) .populate('templateId', 'name templateType thumbnailUrl') .sort({ createdAt: -1 }) .limit(options.limit || 50); }; PosterSchema.statics.findFavorites = function(userId) { return this.find({ 'metadata.userId': userId, 'metadata.isFavorite': true }).sort({ updatedAt: -1 }); }; PosterSchema.statics.findRecent = function(userId, limit = 10) { return this.find({ 'metadata.userId': userId, status: 'completed' }) .sort({ createdAt: -1 }) .limit(limit) .select('name renderOutput.png.url thumbnailUrl createdAt'); }; module.exports = mongoose.model('Poster', PosterSchema);