1243 lines
49 KiB
JavaScript
1243 lines
49 KiB
JavaScript
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;
|