const database = require('../config/database'); const { v4: uuidv4 } = require('uuid'); class CustomTemplate { constructor(data = {}) { this.id = data.id; this.type = data.type; this.title = data.title; this.description = data.description; this.icon = data.icon; this.category = data.category; this.gradient = data.gradient; this.border = data.border; this.text = data.text; this.subtext = data.subtext; this.complexity = data.complexity; this.business_rules = data.business_rules; this.technical_requirements = data.technical_requirements; this.approved = data.approved; this.usage_count = data.usage_count; this.created_by_user_session = data.created_by_user_session; this.created_at = data.created_at; this.updated_at = data.updated_at; this.is_custom = data.is_custom ?? false; // Admin approval workflow fields this.status = data.status || 'pending'; this.admin_notes = data.admin_notes; this.admin_reviewed_at = data.admin_reviewed_at; this.admin_reviewed_by = data.admin_reviewed_by; this.canonical_template_id = data.canonical_template_id; this.similarity_score = data.similarity_score; this.user_id = data.user_id; } static async getById(id) { const result = await database.query('SELECT * FROM custom_templates WHERE id = $1', [id]); return result.rows.length ? new CustomTemplate(result.rows[0]) : null; } // Get custom template by ID with features static async getByIdWithFeatures(id) { const templateQuery = ` SELECT * FROM custom_templates WHERE id = $1 `; const featuresQuery = ` SELECT * FROM custom_features WHERE template_id = $1 ORDER BY created_at DESC `; const [templateResult, featuresResult] = await Promise.all([ database.query(templateQuery, [id]), database.query(featuresQuery, [id]) ]); if (templateResult.rows.length === 0) { return null; } const template = new CustomTemplate(templateResult.rows[0]); template.features = featuresResult.rows; return template; } // Check for duplicate custom templates based on title, type, category, and user_id static async checkForDuplicate(templateData) { const normalizedTitle = (templateData.title || '').toLowerCase(); console.log('[CustomTemplate.checkForDuplicate] Checking for duplicates:', { type: templateData.type, title: templateData.title, normalizedTitle, category: templateData.category, user_id: templateData.user_id }); // Check for exact type match (globally unique) const typeQuery = ` SELECT id, title, type, category, user_id FROM custom_templates WHERE type = $1 `; const typeResult = await database.query(typeQuery, [templateData.type]); if (typeResult.rows.length > 0) { console.log('[CustomTemplate.checkForDuplicate] Found duplicate by type:', typeResult.rows[0]); return typeResult.rows[0]; } // Check for same title for same user (category-agnostic) if (templateData.user_id) { const titleQuery = ` SELECT id, title, type, category, user_id FROM custom_templates WHERE LOWER(title) = LOWER($1) AND user_id = $2 `; const titleParams = [templateData.title, templateData.user_id]; console.log('[CustomTemplate.checkForDuplicate] title check params:', titleParams); const titleResult = await database.query(titleQuery, titleParams); if (titleResult.rows.length > 0) { const row = titleResult.rows[0]; const titleMatch = (row.title || '').toLowerCase() === normalizedTitle; console.log('[CustomTemplate.checkForDuplicate] Found duplicate by title+user:', { id: row.id, title: row.title, type: row.type, category: row.category, user_id: row.user_id, titleMatch }); return titleResult.rows[0]; } } // Also check if main templates already have the same title (case-insensitive) const mainTitleQuery = ` SELECT id, title, type, category FROM templates WHERE is_active = true AND LOWER(title) = LOWER($1) LIMIT 1 `; const mainTitleParams = [templateData.title]; console.log('[CustomTemplate.checkForDuplicate] main title check params:', mainTitleParams); const mainTitleResult = await database.query(mainTitleQuery, mainTitleParams); if (mainTitleResult.rows.length > 0) { const row = mainTitleResult.rows[0]; const titleMatch = (row.title || '').toLowerCase() === normalizedTitle; console.log('[CustomTemplate.checkForDuplicate] Found duplicate title in main templates:', { id: row.id, title: row.title, type: row.type, category: row.category, titleMatch }); return mainTitleResult.rows[0]; } console.log('[CustomTemplate.checkForDuplicate] No duplicates found'); return null; } // Check if template type exists in main templates table static async checkTypeInMainTemplates(type) { const query = ` SELECT id, title, type FROM templates WHERE type = $1 AND is_active = true `; const result = await database.query(query, [type]); return result.rows.length > 0 ? result.rows[0] : null; } static async create(data) { const id = uuidv4(); console.log('[CustomTemplate.create] start - id:', id); const query = ` INSERT INTO custom_templates ( id, type, title, description, icon, category, gradient, border, text, subtext, complexity, business_rules, technical_requirements, approved, usage_count, created_by_user_session, status, admin_notes, admin_reviewed_at, admin_reviewed_by, canonical_template_id, similarity_score, is_custom, user_id ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24) RETURNING * `; const values = [ id, data.type, data.title, data.description || null, data.icon || null, data.category, data.gradient || null, data.border || null, data.text || null, data.subtext || null, data.complexity, data.business_rules || null, data.technical_requirements || null, data.approved ?? false, data.usage_count ?? 1, data.created_by_user_session || null, data.status || 'pending', data.admin_notes || null, data.admin_reviewed_at || null, data.admin_reviewed_by || null, data.canonical_template_id || null, data.similarity_score || null, data.is_custom ?? false, data.user_id || null, ]; console.log('[CustomTemplate.create] values prepared (truncated):', { id: values[0], type: values[1], title: values[2], is_custom: values[22], user_id: values[23] }); const result = await database.query(query, values); console.log('[CustomTemplate.create] insert done - row id:', result.rows[0]?.id, 'user_id:', result.rows[0]?.user_id); const customTemplate = new CustomTemplate(result.rows[0]); // Automatically trigger tech stack analysis for new custom template try { console.log(`🤖 [CustomTemplate.create] Triggering auto tech stack analysis for custom template: ${customTemplate.title}`); // Use dynamic import to avoid circular dependency const autoTechStackAnalyzer = require('../services/auto_tech_stack_analyzer'); autoTechStackAnalyzer.queueForAnalysis(customTemplate.id, 'custom', 1); // High priority for new templates } catch (error) { console.error(`⚠️ [CustomTemplate.create] Failed to queue tech stack analysis:`, error.message); // Don't fail template creation if auto-analysis fails } return customTemplate; } static async update(id, updates) { const fields = []; const values = []; let idx = 1; const allowed = [ 'title', 'description', 'icon', 'category', 'gradient', 'border', 'text', 'subtext', 'complexity', 'business_rules', 'technical_requirements', 'approved', 'usage_count', 'status', 'admin_notes', 'admin_reviewed_at', 'admin_reviewed_by', 'canonical_template_id', 'similarity_score', 'user_id' ]; for (const k of allowed) { if (updates[k] !== undefined) { fields.push(`${k} = $${idx++}`); values.push(updates[k]); } } if (fields.length === 0) return await CustomTemplate.getById(id); const query = `UPDATE custom_templates SET ${fields.join(', ')}, updated_at = NOW() WHERE id = $${idx} RETURNING *`; values.push(id); const result = await database.query(query, values); const updatedTemplate = result.rows.length ? new CustomTemplate(result.rows[0]) : null; // Automatically trigger tech stack analysis for updated custom template if (updatedTemplate) { try { console.log(`🤖 [CustomTemplate.update] Triggering auto tech stack analysis for updated custom template: ${updatedTemplate.title}`); // Use dynamic import to avoid circular dependency const autoTechStackAnalyzer = require('../services/auto_tech_stack_analyzer'); autoTechStackAnalyzer.queueForAnalysis(updatedTemplate.id, 'custom', 2); // Normal priority for updates } catch (error) { console.error(`⚠️ [CustomTemplate.update] Failed to queue tech stack analysis:`, error.message); // Don't fail template update if auto-analysis fails } } return updatedTemplate; } static async delete(id) { const result = await database.query('DELETE FROM custom_templates WHERE id = $1', [id]); return result.rowCount > 0; } // Admin workflow methods static async getPendingTemplates(limit = 50, offset = 0) { const query = ` SELECT * FROM custom_templates WHERE status = 'pending' ORDER BY created_at ASC LIMIT $1 OFFSET $2 `; const result = await database.query(query, [limit, offset]); return result.rows.map(r => new CustomTemplate(r)); } static async getTemplatesByStatus(status, limit = 50, offset = 0) { const query = ` SELECT * FROM custom_templates WHERE status = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3 `; const result = await database.query(query, [status, limit, offset]); return result.rows.map(r => new CustomTemplate(r)); } // Get custom templates created by a specific user session static async getByCreatorSession(sessionKey, limit = 100, offset = 0, status = null) { if (!sessionKey) return []; let query = ` SELECT * FROM custom_templates WHERE created_by_user_session = $1 `; const values = [sessionKey]; if (status) { query += ` AND status = $2`; values.push(status); } query += ` ORDER BY created_at DESC LIMIT ${status ? '$3' : '$2'} OFFSET ${status ? '$4' : '$3'}`; values.push(limit, offset); const result = await database.query(query, values); return result.rows.map(r => new CustomTemplate(r)); } static async getTemplateStats() { const query = ` SELECT status, COUNT(*) as count FROM custom_templates GROUP BY status `; const result = await database.query(query); return result.rows; } // Get custom templates by authenticated user id static async getByUserId(userId, limit = 100, offset = 0, status = null) { if (!userId) return []; let query = ` SELECT * FROM custom_templates WHERE user_id = $1 `; const values = [userId]; if (status) { query += ` AND status = $2`; values.push(status); } query += ` ORDER BY created_at DESC LIMIT ${status ? '$3' : '$2'} OFFSET ${status ? '$4' : '$3'}`; values.push(limit, offset); const result = await database.query(query, values); return result.rows.map(r => new CustomTemplate(r)); } static async reviewTemplate(id, reviewData) { const { status, admin_notes, canonical_template_id, admin_reviewed_by } = reviewData; const updates = { status, admin_notes, admin_reviewed_at: new Date(), admin_reviewed_by }; // Maintain the legacy boolean flag alongside the status for easier filtering if (status === 'approved') { updates.approved = true; } else if (status === 'rejected' || status === 'duplicate') { updates.approved = false; } if (canonical_template_id) { updates.canonical_template_id = canonical_template_id; } return await CustomTemplate.update(id, updates); } // Get all custom templates static async getAll(limit = 100, offset = 0) { const query = ` SELECT * FROM custom_templates ORDER BY created_at DESC LIMIT $1 OFFSET $2 `; const result = await database.query(query, [limit, offset]); return result.rows.map(r => new CustomTemplate(r)); } // Search custom templates static async search(searchTerm, limit = 20) { const query = ` SELECT * FROM custom_templates WHERE (title ILIKE $1 OR description ILIKE $1 OR category ILIKE $1) ORDER BY usage_count DESC, created_at DESC LIMIT $2 `; const result = await database.query(query, [`%${searchTerm}%`, limit]); return result.rows.map(r => new CustomTemplate(r)); } // Get statistics for admin dashboard static async getStats() { const query = ` SELECT status, COUNT(*) as count FROM custom_templates GROUP BY status `; const result = await database.query(query); return result.rows.map(row => ({ status: row.status, count: parseInt(row.count) || 0 })); } // Alias for getAll method to match admin route expectations static async getAllTemplates(limit = 50, offset = 0) { return await CustomTemplate.getAll(limit, offset); } } module.exports = CustomTemplate;