const express = require('express'); const router = express.Router(); const Template = require('../models/template'); const CustomTemplate = require('../models/custom_template'); const Feature = require('../models/feature'); const CustomFeature = require('../models/custom_feature'); const AdminNotification = require('../models/admin_notification'); const database = require('../config/database'); // GET /api/templates - Get all templates grouped by category router.get('/', async (req, res) => { try { console.log('📂 Fetching all templates by category...'); const templates = await Template.getAllByCategory(); res.json({ success: true, data: templates, message: `Found templates in ${Object.keys(templates).length} categories` }); } catch (error) { console.error('❌ Error fetching templates:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch templates', message: error.message }); } }); // GET /api/templates/stats - Get template statistics router.get('/stats', async (req, res) => { try { console.log('📊 Fetching template statistics...'); const stats = await Template.getStats(); res.json({ success: true, data: stats, message: 'Template statistics retrieved successfully' }); } catch (error) { console.error('❌ Error fetching template stats:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch template statistics', message: error.message }); } }); // GET /api/templates/combined - Built-in templates + current user's custom templates (paginated) // Query: userId (required for user customs), status (optional for customs), limit, offset router.get('/combined', async (req, res) => { try { const userId = req.query.userId || req.query.userid || req.query.user_id || null; const limit = parseInt(req.query.limit) || 6; const offset = parseInt(req.query.offset) || 0; const status = req.query.status || null; // optional filter for custom templates // Fetch built-in (admin) templates grouped by category, then flatten const defaultByCategory = await Template.getAllByCategory(); const adminTemplates = Object.values(defaultByCategory).flat().map(t => ({ id: t.id, type: t.type, title: t.title, description: t.description, icon: t.icon, category: t.category, gradient: t.gradient, border: t.border, text: t.text, subtext: t.subtext, created_at: t.created_at, updated_at: t.updated_at, is_custom: false, source: 'admin' })); // Fetch current user's custom templates (if userId provided), else empty let userCustomTemplates = []; if (userId) { const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (!uuidV4Regex.test(userId)) { return res.status(400).json({ success: false, error: 'Invalid userId', message: 'userId must be a valid UUID v4' }); } const customs = await CustomTemplate.getByUserId(userId, 1000, 0, status); userCustomTemplates = customs.map(ct => ({ id: ct.id, type: ct.type, title: ct.title, description: ct.description, icon: ct.icon, category: ct.category, gradient: ct.gradient, border: ct.border, text: ct.text, subtext: ct.subtext, created_at: ct.created_at, updated_at: ct.updated_at, is_custom: true, status: ct.status, user_id: ct.user_id, source: 'user' })); } // Combine and sort by created_at desc (fallback title) const combined = [...adminTemplates, ...userCustomTemplates].sort((a, b) => { const aTime = a.created_at ? new Date(a.created_at).getTime() : 0; const bTime = b.created_at ? new Date(b.created_at).getTime() : 0; if (aTime === bTime) return (a.title || '').localeCompare(b.title || ''); return bTime - aTime; }); const total = combined.length; const slice = combined.slice(offset, offset + limit); const hasMore = offset + slice.length < total; return res.json({ success: true, data: slice, count: slice.length, pagination: { total, limit, offset, hasMore }, message: `Returned ${slice.length} of ${total} templates (combined admin + user)` }); } catch (error) { console.error('❌ Error fetching combined templates:', error.message); return res.status(500).json({ success: false, error: 'Failed to fetch combined templates', message: error.message }); } }); // GET /api/templates/merged - Get paginated, filtered templates (default + custom) router.get('/merged', async (req, res) => { try { console.log('🚀 [MERGED-TEMPLATES] Starting template fetch operation...'); console.log('📋 [MERGED-TEMPLATES] Request parameters:', { limit: req.query.limit || 'default: 10', offset: req.query.offset || 'default: 0', category: req.query.category || 'all categories', search: req.query.search || 'no search query' }); const limit = parseInt(req.query.limit) || 10; const offset = parseInt(req.query.offset) || 0; const categoryFilter = req.query.category || null; const searchQuery = req.query.search ? req.query.search.toLowerCase() : null; console.log("req.query __", req.query) console.log('âš™ī¸ [MERGED-TEMPLATES] Parsed parameters:', { limit, offset, categoryFilter, searchQuery }); // Get all default templates console.log('đŸ—ī¸ [MERGED-TEMPLATES] Fetching default templates by category...'); const defaultTemplatesByCat = await Template.getAllByCategory(); console.log('📊 [MERGED-TEMPLATES] Default templates by category structure:', Object.keys(defaultTemplatesByCat)); let defaultTemplates = []; for (const cat in defaultTemplatesByCat) { const catTemplates = defaultTemplatesByCat[cat]; console.log(`📁 [MERGED-TEMPLATES] Category "${cat}": ${catTemplates.length} templates`); defaultTemplates = defaultTemplates.concat(catTemplates); } console.log('✅ [MERGED-TEMPLATES] Total default templates collected:', defaultTemplates.length); console.log('🔍 [MERGED-TEMPLATES] Sample default template:', defaultTemplates[0] ? { id: defaultTemplates[0].id, title: defaultTemplates[0].title, category: defaultTemplates[0].category, type: defaultTemplates[0].type } : 'No default templates found'); // Get all custom templates for the current user console.log('🎨 [MERGED-TEMPLATES] Fetching custom templates...'); console.log('🔍 [MERGED-TEMPLATES] Request userId:', req.query.userId); console.log('🔍 [MERGED-TEMPLATES] Request includeOthers:', req.query.includeOthers); let customTemplates = []; let userOwnCustomCount = 0; let approvedOthersCustomCount = 0; if (req.query.userId) { // Validate UUID v4 for userId to avoid DB errors like "invalid input syntax for type uuid" const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (!uuidV4Regex.test(req.query.userId)) { console.warn('âš ī¸ [MERGED-TEMPLATES] Invalid userId provided:', req.query.userId); // Don't return error, just skip user-specific templates and continue with approved ones console.log('âš ī¸ [MERGED-TEMPLATES] Continuing with approved templates only due to invalid userId'); customTemplates = await CustomTemplate.getTemplatesByStatus('approved'); approvedOthersCustomCount = customTemplates.length; console.log('📈 [MERGED-TEMPLATES] Approved custom templates (invalid userId fallback):', approvedOthersCustomCount); } else { // Get ALL custom templates for this user (all statuses - approved, pending, rejected, etc.) console.log('✅ [MERGED-TEMPLATES] Valid userId provided, fetching ALL user templates...'); customTemplates = await CustomTemplate.getByUserId(req.query.userId, 1000, 0); userOwnCustomCount = customTemplates.length; console.log('📈 [MERGED-TEMPLATES] ALL custom templates for THIS user:', userOwnCustomCount); // Optionally include ALL custom templates from other users if explicitly requested const includeOthers = String(req.query.includeOthers || '').toLowerCase() === 'true'; if (includeOthers) { const allOtherCustomTemplates = await CustomTemplate.getAll(1000, 0); const otherUsersTemplates = allOtherCustomTemplates.filter(t => t.user_id !== req.query.userId); approvedOthersCustomCount = otherUsersTemplates.length; console.log('📈 [MERGED-TEMPLATES] ALL custom templates from OTHER users (included by query):', approvedOthersCustomCount); // Combine user's templates + all templates from others customTemplates = [...customTemplates, ...otherUsersTemplates]; } else { console.log('â„šī¸ [MERGED-TEMPLATES] Skipping custom templates from other users (includeOthers not set).'); } } } else { // If no userId, get ALL custom templates regardless of status console.log('â„šī¸ [MERGED-TEMPLATES] No userId provided, fetching ALL custom templates'); customTemplates = await CustomTemplate.getAll(1000, 0); approvedOthersCustomCount = customTemplates.length; console.log('📈 [MERGED-TEMPLATES] All custom templates (no user specified):', approvedOthersCustomCount); } console.log('📈 [MERGED-TEMPLATES] Totals → userOwn:', userOwnCustomCount, ', approvedOthers:', approvedOthersCustomCount, ', combinedCustoms:', customTemplates.length); if (customTemplates.length > 0) { console.log('🔍 [MERGED-TEMPLATES] Sample custom template:', { id: customTemplates[0].id, title: customTemplates[0].title, category: customTemplates[0].category, status: customTemplates[0].status }); } // Convert customs to standard template format and merge into flat array console.log('🔄 [MERGED-TEMPLATES] Converting custom templates to standard format...'); const convertedCustomTemplates = customTemplates.map(customTemplate => ({ id: customTemplate.id, type: customTemplate.type, title: customTemplate.title, description: customTemplate.description, icon: customTemplate.icon, category: customTemplate.category, gradient: customTemplate.gradient, border: customTemplate.border, text: customTemplate.text, subtext: customTemplate.subtext, is_active: true, created_at: customTemplate.created_at, updated_at: customTemplate.updated_at, is_custom: true, complexity: customTemplate.complexity, business_rules: customTemplate.business_rules, technical_requirements: customTemplate.technical_requirements })); console.log('✅ [MERGED-TEMPLATES] Custom templates converted:', convertedCustomTemplates.length); let allTemplates = defaultTemplates.concat(convertedCustomTemplates); console.log('🔗 [MERGED-TEMPLATES] Combined templates total:', allTemplates.length); // Apply category filter if specified if (categoryFilter && categoryFilter !== 'all') { console.log(`đŸŽ¯ [MERGED-TEMPLATES] Applying category filter: "${categoryFilter}"`); const beforeFilter = allTemplates.length; allTemplates = allTemplates.filter(t => t.category === categoryFilter); const afterFilter = allTemplates.length; console.log(`📊 [MERGED-TEMPLATES] Category filter result: ${beforeFilter} → ${afterFilter} templates`); } // Apply search filter if specified if (searchQuery) { console.log(`🔍 [MERGED-TEMPLATES] Applying search filter: "${searchQuery}"`); const beforeSearch = allTemplates.length; allTemplates = allTemplates.filter(t => t.title.toLowerCase().includes(searchQuery) || t.description.toLowerCase().includes(searchQuery) ); const afterSearch = allTemplates.length; console.log(`📊 [MERGED-TEMPLATES] Search filter result: ${beforeSearch} → ${afterSearch} templates`); } // Sort by created_at descending console.log('📅 [MERGED-TEMPLATES] Sorting templates by creation date...'); allTemplates.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); console.log('✅ [MERGED-TEMPLATES] Templates sorted successfully'); // Paginate const total = allTemplates.length; console.log('📊 [MERGED-TEMPLATES] Final template count before pagination:', total); console.log('📄 [MERGED-TEMPLATES] Pagination parameters:', { offset, limit, total }); const paginatedTemplates = allTemplates.slice(offset, offset + limit); console.log('📋 [MERGED-TEMPLATES] Paginated result:', { requested: limit, returned: paginatedTemplates.length, startIndex: offset, endIndex: offset + paginatedTemplates.length - 1 }); // Add feature counts to each template console.log('đŸ”ĸ [MERGED-TEMPLATES] Fetching feature counts for templates...'); // Separate default and custom templates for feature counting const defaultTemplateIds = paginatedTemplates.filter(t => !t.is_custom).map(t => t.id); const customTemplateIds = paginatedTemplates.filter(t => t.is_custom).map(t => t.id); console.log('📊 [MERGED-TEMPLATES] Template ID breakdown:', { defaultTemplates: defaultTemplateIds.length, customTemplates: customTemplateIds.length }); // Fetch feature counts for both types let defaultFeatureCounts = {}; let customFeatureCounts = {}; if (defaultTemplateIds.length > 0) { console.log('🔍 [MERGED-TEMPLATES] Fetching default template feature counts...'); defaultFeatureCounts = await Feature.countByTemplateIds(defaultTemplateIds); console.log('✅ [MERGED-TEMPLATES] Default feature counts:', Object.keys(defaultFeatureCounts).length, 'templates'); } if (customTemplateIds.length > 0) { console.log('🔍 [MERGED-TEMPLATES] Fetching custom template feature counts...'); customFeatureCounts = await CustomFeature.countByTemplateIds(customTemplateIds); console.log('✅ [MERGED-TEMPLATES] Custom feature counts:', Object.keys(customFeatureCounts).length, 'templates'); } // Add feature counts to each template const templatesWithFeatureCounts = paginatedTemplates.map(template => ({ ...template, feature_count: template.is_custom ? (customFeatureCounts[template.id] || 0) : (defaultFeatureCounts[template.id] || 0) })); console.log('đŸŽ¯ [MERGED-TEMPLATES] Feature counts added to all templates'); // Log sample of returned templates with feature counts if (templatesWithFeatureCounts.length > 0) { console.log('🔍 [MERGED-TEMPLATES] First template in result:', { id: templatesWithFeatureCounts[0].id, title: templatesWithFeatureCounts[0].title, category: templatesWithFeatureCounts[0].category, is_custom: templatesWithFeatureCounts[0].is_custom, feature_count: templatesWithFeatureCounts[0].feature_count }); if (templatesWithFeatureCounts.length > 1) { console.log('🔍 [MERGED-TEMPLATES] Last template in result:', { id: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].id, title: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].title, category: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].category, is_custom: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].is_custom, feature_count: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].feature_count }); } } const responseData = { success: true, data: templatesWithFeatureCounts, pagination: { total, offset, limit, hasMore: offset + limit < total }, message: `Found ${templatesWithFeatureCounts.length} templates (out of ${total}) with feature counts` }; console.log('🎉 [MERGED-TEMPLATES] Response prepared successfully:', { success: responseData.success, dataCount: responseData.data.length, pagination: responseData.pagination, message: responseData.message, sampleFeatureCounts: templatesWithFeatureCounts.slice(0, 3).map(t => ({ title: t.title, feature_count: t.feature_count, is_custom: t.is_custom })) }); res.json(responseData); } catch (error) { console.error('đŸ’Ĩ [MERGED-TEMPLATES] Critical error occurred:', error.message); console.error('📚 [MERGED-TEMPLATES] Error stack:', error.stack); console.error('🔍 [MERGED-TEMPLATES] Error details:', { name: error.name, code: error.code, sqlMessage: error.sqlMessage }); res.status(500).json({ success: false, error: 'Failed to fetch merged templates', message: error.message }); } }); router.get('/all-templates-without-pagination', async (req, res) => { try { console.log('📂 [ALL-TEMPLATES] Fetching all templates with features and business rules...'); // Fetch templates (using your custom class methods) const templatesQuery = 'SELECT * FROM templates WHERE is_active = true'; const customTemplatesQuery = 'SELECT * FROM custom_templates'; const [templatesResult, customTemplatesResult] = await Promise.all([ database.query(templatesQuery), database.query(customTemplatesQuery) ]); const templates = templatesResult.rows || []; const customTemplates = customTemplatesResult.rows || []; console.log(`📊 [ALL-TEMPLATES] Found ${templates.length} default templates and ${customTemplates.length} custom templates`); // Merge both arrays const allTemplates = [...templates, ...customTemplates]; // Sort by created_at (descending) allTemplates.sort((a, b) => { return new Date(b.created_at) - new Date(a.created_at); }); // Fetch features and business rules for each template console.log('🔍 [ALL-TEMPLATES] Fetching features and business rules for all templates...'); const templatesWithFeatures = await Promise.all( allTemplates.map(async (template) => { try { // Check if this is a default template or custom template const isCustomTemplate = !template.is_active; // custom templates don't have is_active field let features = []; let businessRules = {}; if (isCustomTemplate) { // For custom templates, get features from custom_features table const customFeaturesQuery = ` SELECT cf.id, cf.template_id, cf.name, cf.description, cf.complexity, cf.business_rules, cf.technical_requirements, 'custom' as feature_type, cf.created_at, cf.updated_at, cf.status, cf.approved, cf.usage_count, 0 as user_rating, false as is_default, true as created_by_user FROM custom_features cf WHERE cf.template_id = $1 ORDER BY cf.created_at DESC `; const customFeaturesResult = await database.query(customFeaturesQuery, [template.id]); features = customFeaturesResult.rows || []; // Extract business rules from custom features features.forEach(feature => { if (feature.business_rules) { businessRules[feature.id] = feature.business_rules; } }); } else { // For default templates, get features from template_features table const defaultFeaturesQuery = ` SELECT tf.*, fbr.business_rules AS additional_business_rules FROM template_features tf LEFT JOIN feature_business_rules fbr ON tf.template_id = fbr.template_id AND ( fbr.feature_id = (tf.id::text) OR fbr.feature_id = tf.feature_id ) WHERE tf.template_id = $1 ORDER BY CASE tf.feature_type WHEN 'essential' THEN 1 WHEN 'suggested' THEN 2 WHEN 'custom' THEN 3 END, tf.display_order, tf.usage_count DESC, tf.name `; const defaultFeaturesResult = await database.query(defaultFeaturesQuery, [template.id]); features = defaultFeaturesResult.rows || []; // Extract business rules from feature_business_rules table features.forEach(feature => { if (feature.additional_business_rules) { businessRules[feature.id] = feature.additional_business_rules; } }); } return { ...template, features: features, business_rules: businessRules, feature_count: features.length, is_custom: isCustomTemplate }; } catch (featureError) { console.error(`âš ī¸ [ALL-TEMPLATES] Error fetching features for template ${template.id}:`, featureError.message); return { ...template, features: [], business_rules: {}, feature_count: 0, is_custom: !template.is_active, error: `Failed to fetch features: ${featureError.message}` }; } }) ); console.log(`✅ [ALL-TEMPLATES] Successfully processed ${templatesWithFeatures.length} templates with features and business rules`); // Log sample data for debugging if (templatesWithFeatures.length > 0) { const sampleTemplate = templatesWithFeatures[0]; console.log('🔍 [ALL-TEMPLATES] Sample template data:', { id: sampleTemplate.id, title: sampleTemplate.title, is_custom: sampleTemplate.is_custom, feature_count: sampleTemplate.feature_count, business_rules_count: Object.keys(sampleTemplate.business_rules || {}).length, features_sample: sampleTemplate.features.slice(0, 2).map(f => ({ name: f.name, type: f.feature_type, has_business_rules: !!f.business_rules || !!f.additional_business_rules })) }); } res.json({ success: true, data: templatesWithFeatures, message: `Found ${templatesWithFeatures.length} templates with features and business rules`, summary: { total_templates: templatesWithFeatures.length, default_templates: templatesWithFeatures.filter(t => !t.is_custom).length, custom_templates: templatesWithFeatures.filter(t => t.is_custom).length, total_features: templatesWithFeatures.reduce((sum, t) => sum + t.feature_count, 0), templates_with_business_rules: templatesWithFeatures.filter(t => Object.keys(t.business_rules || {}).length > 0).length } }); } catch (error) { console.error('❌ Error fetching all templates without pagination:', error); res.status(500).json({ success: false, error: 'Failed to fetch all templates without pagination', message: error.message }); } }); // GET /api/templates/type/:type - Get template by type router.get('/type/:type', async (req, res) => { try { const { type } = req.params; console.log(`🔍 Fetching template by type: ${type}`); const template = await Template.getByType(type); if (!template) { return res.status(404).json({ success: false, error: 'Template not found', message: `Template with type ${type} does not exist` }); } // Get features for this template const features = await Feature.getByTemplateId(template.id); template.features = features; res.json({ success: true, data: template, message: `Template ${template.title} retrieved successfully` }); } catch (error) { console.error('❌ Error fetching template by type:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch template', message: error.message }); } }); // GET /api/templates/:id - Get specific template with features (UUID constrained) router.get('/:id([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})', async (req, res) => { try { const { id } = req.params; console.log(`🔍 Fetching template: ${id}`); // Extra guard: ensure UUID v4 to avoid DB errors if route matching misfires const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (!uuidV4Regex.test(id)) { return res.status(400).json({ success: false, error: 'Invalid template id', message: 'id must be a valid UUID v4' }); } // First try to find in default templates let template = await Template.getByIdWithFeatures(id); let templateType = 'default'; // If not found in default templates, try custom templates if (!template) { const CustomTemplate = require('../models/custom_template'); template = await CustomTemplate.getByIdWithFeatures(id); templateType = 'custom'; } if (!template) { return res.status(404).json({ success: false, error: 'Template not found', message: `Template with ID ${id} does not exist` }); } // Add template type information to response const responseData = { ...template, template_type: templateType, is_custom: templateType === 'custom' }; res.json({ success: true, data: responseData, message: `Template ${template.title} retrieved successfully` }); } catch (error) { console.error('❌ Error fetching template:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch template', message: error.message }); } }); // GET /api/templates/:id/features - Get features for a template (UUID constrained) router.get('/:id([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/features', async (req, res) => { try { const { id } = req.params; console.log(`đŸŽ¯ Fetching features for template: ${id}`); // Check if template exists in either templates or custom_templates table console.log(`🔍 Searching for template ID: ${id}`); // First check templates table const defaultTemplateCheck = await database.query(` SELECT id, title, 'default' as template_type FROM templates WHERE id = $1 AND is_active = true `, [id]); console.log(`📊 Default templates found: ${defaultTemplateCheck.rows.length}`); // Then check custom_templates table const customTemplateCheck = await database.query(` SELECT id, title, 'custom' as template_type FROM custom_templates WHERE id = $1 `, [id]); console.log(`📊 Custom templates found: ${customTemplateCheck.rows.length}`); // Combine results const templateCheck = { rows: [...defaultTemplateCheck.rows, ...customTemplateCheck.rows] }; if (templateCheck.rows.length === 0) { console.log(`❌ Template not found in either table: ${id}`); return res.status(404).json({ success: false, error: 'Template not found', message: `Template with ID ${id} does not exist in templates or custom_templates` }); } console.log(`✅ Template found: ${templateCheck.rows[0].title} (${templateCheck.rows[0].template_type})`); // Fetch features from both tables for proper separation console.log('📋 Fetching features from both template_features and custom_features tables'); // Get default/suggested features from template_features table // Include aggregated business rules from feature_business_rules when available const defaultFeaturesQuery = ` SELECT tf.*, fbr.business_rules AS additional_business_rules FROM template_features tf LEFT JOIN feature_business_rules fbr ON tf.template_id = fbr.template_id AND ( fbr.feature_id = (tf.id::text) OR fbr.feature_id = tf.feature_id ) WHERE tf.template_id = $1 ORDER BY CASE tf.feature_type WHEN 'essential' THEN 1 WHEN 'suggested' THEN 2 WHEN 'custom' THEN 3 END, tf.display_order, tf.usage_count DESC, tf.name `; const defaultFeaturesResult = await database.query(defaultFeaturesQuery, [id]); const defaultFeatures = defaultFeaturesResult.rows; console.log(`📊 Found ${defaultFeatures.length} template features (all types)`); console.log(`📋 Template features for ${id}:`, defaultFeatures.map(f => ({ name: f.name, type: f.feature_type, id: f.id }))); // Get custom features from custom_features table with business rules (if table exists) // Some environments may not have run the feature_business_rules migration yet. Probe first. const fbrExistsProbe = await database.query("SELECT to_regclass('public.feature_business_rules') AS tbl"); const hasFbrTable = !!(fbrExistsProbe.rows && fbrExistsProbe.rows[0] && fbrExistsProbe.rows[0].tbl); const customFeaturesQuery = hasFbrTable ? ` SELECT cf.id, cf.template_id, cf.name, cf.description, cf.complexity, cf.business_rules, cf.technical_requirements, 'custom' as feature_type, cf.created_at, cf.updated_at, cf.status, cf.approved, cf.usage_count, 0 as user_rating, false as is_default, true as created_by_user, fbr.business_rules as additional_business_rules FROM custom_features cf LEFT JOIN feature_business_rules fbr ON cf.template_id = fbr.template_id AND ( fbr.feature_id = (cf.id::text) OR fbr.feature_id = ('custom_' || cf.id::text) ) WHERE cf.template_id = $1 ORDER BY cf.created_at DESC ` : ` SELECT cf.id, cf.template_id, cf.name, cf.description, cf.complexity, cf.business_rules, cf.technical_requirements, 'custom' as feature_type, cf.created_at, cf.updated_at, cf.status, cf.approved, cf.usage_count, 0 as user_rating, false as is_default, true as created_by_user, NULL::jsonb as additional_business_rules FROM custom_features cf WHERE cf.template_id = $1 ORDER BY cf.created_at DESC `; const customFeaturesResult = await database.query(customFeaturesQuery, [id]); const customFeatures = customFeaturesResult.rows; console.log(`📊 Found ${customFeatures.length} custom features`); // Combine both types of features const features = [...defaultFeatures, ...customFeatures]; res.json({ success: true, data: features, count: features.length, defaultFeaturesCount: defaultFeatures.length, customFeaturesCount: customFeatures.length, message: `Found ${defaultFeatures.length} default/suggested features and ${customFeatures.length} custom features`, templateInfo: templateCheck.rows[0] }); } catch (error) { console.error('❌ Error fetching template features:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch template features', message: error.message }); } }); // POST /api/templates - Create new template router.post('/', async (req, res) => { try { const templateData = req.body; const debugPayload = { raw: templateData, normalized: { title: (templateData.title || '').toLowerCase(), type: templateData.type, category: templateData.category, isCustom: templateData.isCustom ?? templateData.is_custom ?? false, user_id: templateData.user_id || templateData.userId || null } }; console.log('đŸ—ī¸ Creating new template - incoming body:', JSON.stringify(debugPayload)); // Validate required fields const requiredFields = ['type', 'title', 'category']; for (const field of requiredFields) { if (!templateData[field]) { return res.status(400).json({ success: false, error: 'Validation error', message: `Field '${field}' is required` }); } } // Check for duplicates in regular templates first const existingTemplate = await Template.checkForDuplicate(templateData); if (existingTemplate) { const isTitleDuplicate = (existingTemplate.title || '').toLowerCase() === (templateData.title || '').toLowerCase(); const isTypeDuplicate = (existingTemplate.type || '') === (templateData.type || ''); console.log('[POST /api/templates] duplicate detected in main templates:', { existingTemplate, isTitleDuplicate, isTypeDuplicate }); const message = isTitleDuplicate ? `A template with this name already exists: "${existingTemplate.title}"` : `A template with this type already exists: "${existingTemplate.title}" (type: ${existingTemplate.type})`; return res.status(409).json({ success: false, error: isTitleDuplicate ? 'Template name already exists' : 'Template type already exists', message, existing_template: { id: existingTemplate.id, title: existingTemplate.title, type: existingTemplate.type, category: existingTemplate.category } }); } // If flagged as a custom template, store in custom_templates instead if (templateData.isCustom === true || templateData.is_custom === true || templateData.source === 'custom') { try { const validComplexity = ['low', 'medium', 'high']; const complexity = templateData.complexity || 'medium'; if (!validComplexity.includes(complexity)) { return res.status(400).json({ success: false, error: 'Invalid complexity', message: `Complexity must be one of: ${validComplexity.join(', ')}` }); } // Check for duplicates in both regular and custom templates const existingRegularTemplate = await CustomTemplate.checkTypeInMainTemplates(templateData.type); if (existingRegularTemplate) { return res.status(409).json({ success: false, error: 'Template type already exists in main templates', message: `A main template with type '${templateData.type}' already exists: "${existingRegularTemplate.title}"`, existing_template: { id: existingRegularTemplate.id, title: existingRegularTemplate.title, type: existingRegularTemplate.type, source: 'main_templates' } }); } const incomingUserId = templateData.user_id || templateData.userId || (req.user && (req.user.id || req.user.user_id)) || null; // Check for duplicates in custom templates for this user const duplicatePayload = { type: templateData.type, title: templateData.title, category: templateData.category, user_id: incomingUserId }; console.log('[POST /api/templates - custom] duplicate payload:', duplicatePayload); const existingCustomTemplate = await CustomTemplate.checkForDuplicate(duplicatePayload); if (existingCustomTemplate) { const isTitleDuplicate = (existingCustomTemplate.title || '').toLowerCase() === (templateData.title || '').toLowerCase(); const isTypeDuplicate = (existingCustomTemplate.type || '') === (templateData.type || ''); console.log('[POST /api/templates - custom] duplicate detected in custom/main:', { existingCustomTemplate, isTitleDuplicate, isTypeDuplicate }); const message = isTitleDuplicate ? `You already have a template with this name: "${existingCustomTemplate.title}"` : `You already have a template with this type: "${existingCustomTemplate.title}" (type: ${existingCustomTemplate.type})`; return res.status(409).json({ success: false, error: isTitleDuplicate ? 'Template name already exists' : 'Template type already exists', message, existing_template: { id: existingCustomTemplate.id, title: existingCustomTemplate.title, type: existingCustomTemplate.type, category: existingCustomTemplate.category, user_id: existingCustomTemplate.user_id, source: 'custom_templates' } }); } // Validate user_id format if provided if (incomingUserId) { const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (!uuidV4Regex.test(incomingUserId)) { return res.status(400).json({ success: false, error: 'Invalid user_id', message: 'user_id must be a valid UUID v4' }); } } const isCustomValue = (templateData.is_custom !== undefined ? templateData.is_custom : (templateData.isCustom !== undefined ? templateData.isCustom : true)); const payloadToCreate = { type: templateData.type, title: templateData.title, description: templateData.description, icon: templateData.icon, category: templateData.category, gradient: templateData.gradient, border: templateData.border, text: templateData.text, subtext: templateData.subtext, complexity, business_rules: templateData.business_rules, technical_requirements: templateData.technical_requirements, approved: false, usage_count: 1, created_by_user_session: templateData.created_by_user_session, status: 'pending', is_custom: isCustomValue, user_id: incomingUserId }; console.log('[Templates Route -> custom] user identification:', { body_user_id: templateData.user_id, body_userId: templateData.userId, req_user: req.user ? (req.user.id || req.user.user_id) : null }); console.log('[Templates Route -> custom] payload for create:', JSON.stringify(payloadToCreate)); const created = await CustomTemplate.create(payloadToCreate); console.log('[Templates Route -> custom] created record summary:', { id: created.id, type: created.type, user_id: created.user_id, status: created.status }); // Create admin notification for new custom template try { console.log('[Templates Route -> custom] creating admin notification for template:', created.id, created.title); const notif = await AdminNotification.notifyNewTemplate(created.id, created.title); console.log('[Templates Route -> custom] admin notification created:', notif?.id); } catch (notificationError) { console.error('âš ī¸ Failed to create admin notification:', notificationError.message); } return res.status(201).json({ success: true, data: created, message: `Custom template '${created.title}' created successfully and submitted for admin review` }); } catch (customErr) { console.error('❌ Error creating custom template via templates route:', customErr.message); return res.status(500).json({ success: false, error: 'Failed to create custom template', message: customErr.message }); } } const template = await Template.create(templateData); // Link back to custom_templates when approving from a custom if (templateData.approved_from_custom) { try { const customId = templateData.approved_from_custom; const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (uuidV4Regex.test(customId)) { await CustomTemplate.update(customId, { approved: true, status: 'approved', canonical_template_id: template.id, admin_reviewed_at: new Date(), admin_reviewed_by: 'system_auto' }); } else { console.warn('[POST /api/templates] approved_from_custom is not a valid UUID v4'); } } catch (linkErr) { console.error('âš ī¸ Failed to set approved=true on custom_templates:', linkErr.message); } } res.status(201).json({ success: true, data: template, message: `Template '${template.title}' created successfully` }); } catch (error) { console.error('❌ Error creating template:', error.message); // Handle unique constraint violation if (error.code === '23505') { return res.status(409).json({ success: false, error: 'Template already exists', message: 'A template with this type already exists' }); } res.status(500).json({ success: false, error: 'Failed to create template', message: error.message }); } }); // POST /api/templates/approve-custom - Create main template and approve a custom template in one atomic flow router.post('/approve-custom', async (req, res) => { try { const { custom_template_id, template } = req.body || {}; const customId = custom_template_id || req.body?.customTemplateId || req.body?.id; const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (!customId || !uuidV4Regex.test(customId)) { return res.status(400).json({ success: false, error: 'Invalid custom_template_id', message: 'Provide a valid UUID v4 for custom_template_id' }); } // Load custom template to mirror missing fields if needed const existingCustom = await CustomTemplate.getById(customId); if (!existingCustom) { return res.status(404).json({ success: false, error: 'Custom template not found', message: `No custom template with id ${customId}` }); } const payload = { type: template?.type || existingCustom.type, title: template?.title || existingCustom.title, description: template?.description ?? existingCustom.description, icon: template?.icon ?? existingCustom.icon, category: template?.category || existingCustom.category, gradient: template?.gradient ?? existingCustom.gradient, border: template?.border ?? existingCustom.border, text: template?.text ?? existingCustom.text, subtext: template?.subtext ?? existingCustom.subtext, approved_from_custom: customId }; // Create in main templates const created = await Template.create(payload); // Mark custom template as approved and link canonical_template_id await CustomTemplate.update(customId, { approved: true, status: 'approved', canonical_template_id: created.id, admin_reviewed_at: new Date(), admin_reviewed_by: (req.user && (req.user.username || req.user.email)) || 'admin' }); return res.status(201).json({ success: true, data: { template: created, custom_template_id: customId }, message: `Template '${created.title}' created and custom template approved` }); } catch (error) { console.error('❌ Error approving custom template:', error.message); return res.status(500).json({ success: false, error: 'Failed to approve custom template', message: error.message }); } }); // PUT /api/templates/:id - Update template or custom template based on isCustom flag router.put('/:id', async (req, res) => { try { const { id } = req.params; const updateData = req.body; const isCustomParam = (req.query.isCustom || req.query.is_custom || '').toString().toLowerCase(); const isCustom = isCustomParam === 'true' || isCustomParam === '1' || isCustomParam === 'yes'; console.log('📝 [PUT /api/templates/:id] start', { id, isCustom, bodyKeys: Object.keys(updateData || {}) }); if (isCustom) { console.log('🔎 Looking up custom template by id'); const custom = await CustomTemplate.getById(id); console.log('🔎 Lookup result (custom):', { found: !!custom }); if (!custom) { return res.status(404).json({ success: false, error: 'Template not found', message: `Custom template with ID ${id} does not exist` }); } // Validate allowed fields for custom templates to avoid no-op updates 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' ]; const providedKeys = Object.keys(updateData || {}); const updatableKeys = providedKeys.filter(k => allowed.includes(k)); console.log('🧮 Update keys (custom):', { providedKeys, updatableKeys }); if (updatableKeys.length === 0) { return res.status(400).json({ success: false, error: 'No updatable fields', message: 'Provide at least one updatable field' }); } console.log('📝 Updating custom template...'); const updated = await CustomTemplate.update(id, updateData); console.log('📝 Update result (custom):', { updated: !!updated }); return res.json({ success: true, data: updated, message: `Custom template '${updated?.title || updated?.id}' updated successfully` }); } console.log('🔎 Looking up default template by id'); const template = await Template.getByIdWithFeatures(id); console.log('🔎 Lookup result (default):', { found: !!template }); if (!template) { return res.status(404).json({ success: false, error: 'Template not found', message: `Template with ID ${id} does not exist` }); } console.log('📝 Updating default template...'); const updatedTemplate = await template.update(updateData); console.log('📝 Update result (default):', { updated: !!updatedTemplate }); res.json({ success: true, data: updatedTemplate, message: `Template '${updatedTemplate.title}' updated successfully` }); } catch (error) { console.error('❌ Error updating template:', { message: error.message, stack: error.stack }); res.status(500).json({ success: false, error: 'Failed to update template', message: error.message }); } }); // DELETE /api/templates/:id - Delete template or custom template based on isCustom flag router.delete('/:id', async (req, res) => { try { const { id } = req.params; const isCustomParam = (req.query.isCustom || req.query.is_custom || '').toString().toLowerCase(); const isCustom = isCustomParam === 'true' || isCustomParam === '1' || isCustomParam === 'yes'; console.log('đŸ—‘ī¸ [DELETE /api/templates/:id] start', { id, query: req.query, isCustomParam, isCustom }); if (isCustom) { console.log('🔎 Looking up custom template by id'); const custom = await CustomTemplate.getById(id); console.log('🔎 Lookup result (custom):', { found: !!custom }); if (!custom) { console.warn('âš ī¸ Custom template not found', { id }); return res.status(404).json({ success: false, error: 'Template not found', message: `Custom template with ID ${id} does not exist` }); } console.log('đŸ—‘ī¸ Deleting custom template...'); const deleted = await CustomTemplate.delete(id); console.log('đŸ—‘ī¸ Delete result (custom):', { deleted }); if (!deleted) { return res.status(500).json({ success: false, error: 'Failed to delete template', message: `Failed to delete custom template with ID ${id}` }); } return res.json({ success: true, message: `Custom template '${custom.title || custom.id}' deleted successfully` }); } console.log('🔎 Looking up default template by id'); const template = await Template.getByIdWithFeatures(id); console.log('🔎 Lookup result (default):', { found: !!template }); if (!template) { console.warn('âš ī¸ Default template not found', { id }); return res.status(404).json({ success: false, error: 'Template not found', message: `Template with ID ${id} does not exist` }); } console.log('đŸ—‘ī¸ Deleting default template...'); await Template.delete(id); console.log('đŸ—‘ī¸ Delete done (default)'); res.json({ success: true, message: `Template '${template.title}' deleted successfully` }); } catch (error) { console.error('❌ Error deleting template:', { message: error.message, stack: error.stack }); res.status(500).json({ success: false, error: 'Failed to delete template', message: error.message }); } }); module.exports = router;