545 lines
18 KiB
JavaScript
545 lines
18 KiB
JavaScript
const express = require('express');
|
||
const router = express.Router();
|
||
const Template = require('../models/template');
|
||
const Feature = require('../models/feature');
|
||
const database = require('../config/database');
|
||
|
||
// GET /api/admin/templates - Get all templates for admin management
|
||
router.get('/', async (req, res) => {
|
||
try {
|
||
console.log('🔧 [ADMIN-TEMPLATES] Fetching all templates for admin management...');
|
||
|
||
const limit = parseInt(req.query.limit) || 50;
|
||
const offset = parseInt(req.query.offset) || 0;
|
||
const category = req.query.category || null;
|
||
const search = req.query.search || null;
|
||
|
||
console.log('📋 [ADMIN-TEMPLATES] Query parameters:', { limit, offset, category, search });
|
||
|
||
// Build the query with optional filters
|
||
let whereClause = 'WHERE t.is_active = true AND t.type != \'_migration_test\'';
|
||
let queryParams = [];
|
||
let paramIndex = 1;
|
||
|
||
if (category && category !== 'all') {
|
||
whereClause += ` AND t.category = $${paramIndex}`;
|
||
queryParams.push(category);
|
||
paramIndex++;
|
||
}
|
||
|
||
if (search) {
|
||
whereClause += ` AND (LOWER(t.title) LIKE LOWER($${paramIndex}) OR LOWER(t.description) LIKE LOWER($${paramIndex + 1}))`;
|
||
queryParams.push(`%${search}%`, `%${search}%`);
|
||
paramIndex += 2;
|
||
}
|
||
|
||
// Add pagination
|
||
const limitClause = `LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
||
queryParams.push(limit, offset);
|
||
|
||
const query = `
|
||
SELECT
|
||
t.*,
|
||
COUNT(tf.id) as feature_count,
|
||
AVG(tf.user_rating) as avg_rating
|
||
FROM templates t
|
||
LEFT JOIN template_features tf ON t.id = tf.template_id
|
||
${whereClause}
|
||
GROUP BY t.id
|
||
ORDER BY t.created_at DESC, t.title
|
||
${limitClause}
|
||
`;
|
||
|
||
console.log('🔍 [ADMIN-TEMPLATES] Executing query:', query);
|
||
console.log('📊 [ADMIN-TEMPLATES] Query params:', queryParams);
|
||
|
||
const result = await database.query(query, queryParams);
|
||
const templates = result.rows.map(row => ({
|
||
id: row.id,
|
||
type: row.type,
|
||
title: row.title,
|
||
description: row.description,
|
||
icon: row.icon,
|
||
category: row.category,
|
||
gradient: row.gradient,
|
||
border: row.border,
|
||
text: row.text,
|
||
subtext: row.subtext,
|
||
is_active: row.is_active,
|
||
created_at: row.created_at,
|
||
updated_at: row.updated_at,
|
||
feature_count: parseInt(row.feature_count) || 0,
|
||
avg_rating: parseFloat(row.avg_rating) || 0
|
||
}));
|
||
|
||
// Get total count for pagination
|
||
let countWhereClause = 'WHERE is_active = true AND type != \'_migration_test\'';
|
||
let countParams = [];
|
||
let countParamIndex = 1;
|
||
|
||
if (category && category !== 'all') {
|
||
countWhereClause += ` AND category = $${countParamIndex}`;
|
||
countParams.push(category);
|
||
countParamIndex++;
|
||
}
|
||
|
||
if (search) {
|
||
countWhereClause += ` AND (LOWER(title) LIKE LOWER($${countParamIndex}) OR LOWER(description) LIKE LOWER($${countParamIndex + 1}))`;
|
||
countParams.push(`%${search}%`, `%${search}%`);
|
||
}
|
||
|
||
const countQuery = `SELECT COUNT(*) as total FROM templates ${countWhereClause}`;
|
||
const countResult = await database.query(countQuery, countParams);
|
||
const total = parseInt(countResult.rows[0].total);
|
||
|
||
console.log('✅ [ADMIN-TEMPLATES] Found templates:', {
|
||
returned: templates.length,
|
||
total,
|
||
hasMore: offset + templates.length < total
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
data: templates,
|
||
pagination: {
|
||
total,
|
||
limit,
|
||
offset,
|
||
hasMore: offset + templates.length < total
|
||
},
|
||
message: `Found ${templates.length} templates for admin management`
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ [ADMIN-TEMPLATES] Error fetching templates:', error.message);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Failed to fetch admin templates',
|
||
message: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// GET /api/admin/templates/stats - Get template statistics for admin
|
||
router.get('/stats', async (req, res) => {
|
||
try {
|
||
console.log('📊 [ADMIN-TEMPLATES] Fetching template statistics...');
|
||
|
||
const statsQuery = `
|
||
SELECT
|
||
COUNT(*) as total_templates,
|
||
COUNT(DISTINCT category) as total_categories,
|
||
AVG(feature_counts.feature_count) as avg_features_per_template,
|
||
COUNT(CASE WHEN feature_counts.feature_count = 0 THEN 1 END) as templates_without_features,
|
||
COUNT(CASE WHEN feature_counts.feature_count > 0 THEN 1 END) as templates_with_features
|
||
FROM (
|
||
SELECT
|
||
t.id,
|
||
t.category,
|
||
COUNT(tf.id) as feature_count
|
||
FROM templates t
|
||
LEFT JOIN template_features tf ON t.id = tf.template_id
|
||
WHERE t.is_active = true AND t.type != '_migration_test'
|
||
GROUP BY t.id, t.category
|
||
) feature_counts
|
||
`;
|
||
|
||
const categoryStatsQuery = `
|
||
SELECT
|
||
category,
|
||
COUNT(*) as template_count,
|
||
AVG(feature_counts.feature_count) as avg_features
|
||
FROM (
|
||
SELECT
|
||
t.id,
|
||
t.category,
|
||
COUNT(tf.id) as feature_count
|
||
FROM templates t
|
||
LEFT JOIN template_features tf ON t.id = tf.template_id
|
||
WHERE t.is_active = true AND t.type != '_migration_test'
|
||
GROUP BY t.id, t.category
|
||
) feature_counts
|
||
GROUP BY category
|
||
ORDER BY template_count DESC
|
||
`;
|
||
|
||
const [statsResult, categoryStatsResult] = await Promise.all([
|
||
database.query(statsQuery),
|
||
database.query(categoryStatsQuery)
|
||
]);
|
||
|
||
const stats = {
|
||
...statsResult.rows[0],
|
||
avg_features_per_template: parseFloat(statsResult.rows[0].avg_features_per_template) || 0,
|
||
categories: categoryStatsResult.rows.map(row => ({
|
||
category: row.category,
|
||
template_count: parseInt(row.template_count),
|
||
avg_features: parseFloat(row.avg_features) || 0
|
||
}))
|
||
};
|
||
|
||
console.log('✅ [ADMIN-TEMPLATES] Template statistics:', stats);
|
||
|
||
res.json({
|
||
success: true,
|
||
data: stats,
|
||
message: 'Template statistics retrieved successfully'
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ [ADMIN-TEMPLATES] Error fetching template stats:', error.message);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Failed to fetch template statistics',
|
||
message: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// GET /api/admin/templates/:id/features - Get features for a template
|
||
router.get('/:id/features', async (req, res) => {
|
||
try {
|
||
const { id } = req.params;
|
||
|
||
console.log('🔍 [ADMIN-TEMPLATES] Fetching features for template:', id);
|
||
|
||
// Validate template exists
|
||
const template = await Template.getByIdWithFeatures(id);
|
||
if (!template) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
error: 'Template not found',
|
||
message: `Template with ID ${id} does not exist`
|
||
});
|
||
}
|
||
|
||
// Get features for the template with business rules
|
||
const featuresQuery = `
|
||
SELECT
|
||
tf.*,
|
||
fbr.business_rules as stored_business_rules
|
||
FROM template_features tf
|
||
LEFT JOIN feature_business_rules fbr ON CAST(tf.id AS TEXT) = fbr.feature_id
|
||
WHERE tf.template_id = $1
|
||
ORDER BY tf.display_order, tf.created_at
|
||
`;
|
||
|
||
const result = await database.query(featuresQuery, [id]);
|
||
const features = result.rows.map(row => ({
|
||
id: row.id,
|
||
template_id: row.template_id,
|
||
feature_id: row.feature_id,
|
||
name: row.name,
|
||
description: row.description,
|
||
feature_type: row.feature_type || 'suggested',
|
||
complexity: row.complexity || 'medium',
|
||
display_order: row.display_order,
|
||
usage_count: row.usage_count || 0,
|
||
user_rating: row.user_rating || 0,
|
||
is_default: row.is_default || false,
|
||
created_by_user: row.created_by_user || false,
|
||
created_at: row.created_at,
|
||
updated_at: row.updated_at,
|
||
business_rules: row.stored_business_rules || row.business_rules,
|
||
technical_requirements: row.stored_business_rules || row.technical_requirements
|
||
}));
|
||
|
||
console.log('✅ [ADMIN-TEMPLATES] Found features:', features.length);
|
||
|
||
res.json({
|
||
success: true,
|
||
data: features,
|
||
message: `Found ${features.length} features for template '${template.title}'`
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ [ADMIN-TEMPLATES] Error fetching template features:', error);
|
||
console.error('❌ [ADMIN-TEMPLATES] Full error stack:', error.stack);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Failed to fetch template features',
|
||
message: error.message,
|
||
details: error.stack
|
||
});
|
||
}
|
||
});
|
||
|
||
// POST /api/admin/templates/:id/features - Add feature to template
|
||
router.post('/:id/features', async (req, res) => {
|
||
try {
|
||
const { id } = req.params;
|
||
const featureData = req.body;
|
||
|
||
console.log('➕ [ADMIN-TEMPLATES] Adding feature to template:', id);
|
||
console.log('📋 [ADMIN-TEMPLATES] Feature data:', featureData);
|
||
|
||
// Validate template exists
|
||
const template = await Template.getByIdWithFeatures(id);
|
||
if (!template) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
error: 'Template not found',
|
||
message: `Template with ID ${id} does not exist`
|
||
});
|
||
}
|
||
|
||
// Use Feature.create() method to ensure business rules are stored
|
||
const displayOrder = template.features ? template.features.length + 1 : 1;
|
||
const feature = await Feature.create({
|
||
template_id: id,
|
||
name: featureData.name,
|
||
description: featureData.description || '',
|
||
feature_type: featureData.feature_type || 'custom',
|
||
complexity: featureData.complexity || 'medium',
|
||
display_order: displayOrder,
|
||
is_default: featureData.is_default || false,
|
||
created_by_user: featureData.created_by_user || true,
|
||
logic_rules: featureData.logic_rules,
|
||
business_rules: featureData.business_rules
|
||
});
|
||
|
||
console.log('✅ [ADMIN-TEMPLATES] Feature created:', feature.id);
|
||
|
||
res.status(201).json({
|
||
success: true,
|
||
data: feature,
|
||
message: `Feature '${feature.name}' added to template '${template.title}'`
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ [ADMIN-TEMPLATES] Error adding feature:', error.message);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Failed to add feature to template',
|
||
message: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// PUT /api/admin/templates/:templateId/features/:featureId - Update feature
|
||
router.put('/:templateId/features/:featureId', async (req, res) => {
|
||
try {
|
||
const { templateId, featureId } = req.params;
|
||
const updateData = req.body;
|
||
|
||
console.log('✏️ [ADMIN-TEMPLATES] Updating feature:', featureId, 'in template:', templateId);
|
||
console.log('📦 [ADMIN-TEMPLATES] Raw request body:', JSON.stringify(req.body, null, 2));
|
||
console.log('📦 [ADMIN-TEMPLATES] Request headers:', req.headers['content-type']);
|
||
console.log('📦 [ADMIN-TEMPLATES] Content-Length:', req.headers['content-length']);
|
||
console.log('📦 [ADMIN-TEMPLATES] Request method:', req.method);
|
||
console.log('📦 [ADMIN-TEMPLATES] Request URL:', req.url);
|
||
console.log('📦 [ADMIN-TEMPLATES] Update data keys:', Object.keys(updateData || {}));
|
||
console.log('📦 [ADMIN-TEMPLATES] Body type:', typeof req.body);
|
||
console.log('📦 [ADMIN-TEMPLATES] Body constructor:', req.body?.constructor?.name);
|
||
|
||
// Validate template exists
|
||
const template = await Template.getByIdWithFeatures(templateId);
|
||
if (!template) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
error: 'Template not found',
|
||
message: `Template with ID ${templateId} does not exist`
|
||
});
|
||
}
|
||
|
||
// Update the feature in template_features table
|
||
const updateQuery = `
|
||
UPDATE template_features
|
||
SET name = $1, description = $2, complexity = $3, updated_at = NOW()
|
||
WHERE id = $4 AND template_id = $5
|
||
RETURNING *
|
||
`;
|
||
|
||
// Validate required fields
|
||
if (!updateData.name || updateData.name.trim() === '') {
|
||
console.error('❌ [ADMIN-TEMPLATES] Validation failed: Feature name is required');
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: 'Validation failed',
|
||
message: 'Feature name is required',
|
||
received_data: updateData
|
||
});
|
||
}
|
||
|
||
console.log('📝 [ADMIN-TEMPLATES] Update data received:', JSON.stringify(updateData, null, 2));
|
||
|
||
const result = await database.query(updateQuery, [
|
||
updateData.name.trim(),
|
||
updateData.description || '',
|
||
updateData.complexity || 'medium',
|
||
featureId,
|
||
templateId
|
||
]);
|
||
|
||
// Update or insert business rules in feature_business_rules table
|
||
if (updateData.business_rules) {
|
||
const businessRulesQuery = `
|
||
INSERT INTO feature_business_rules (template_id, feature_id, business_rules)
|
||
VALUES ($1, $2, $3)
|
||
ON CONFLICT (template_id, feature_id)
|
||
DO UPDATE SET business_rules = $3, updated_at = NOW()
|
||
`;
|
||
|
||
// Convert business_rules to JSON string if it's an array/object
|
||
const businessRulesData = typeof updateData.business_rules === 'string'
|
||
? updateData.business_rules
|
||
: JSON.stringify(updateData.business_rules);
|
||
|
||
await database.query(businessRulesQuery, [templateId, featureId, businessRulesData]);
|
||
}
|
||
|
||
if (result.rows.length === 0) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
error: 'Feature not found',
|
||
message: `Feature with ID ${featureId} does not exist in template ${templateId}`
|
||
});
|
||
}
|
||
|
||
const updatedFeature = result.rows[0];
|
||
|
||
console.log('✅ [ADMIN-TEMPLATES] Feature updated:', updatedFeature.id);
|
||
|
||
res.json({
|
||
success: true,
|
||
data: updatedFeature,
|
||
message: `Feature '${updatedFeature.name}' updated successfully`
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ [ADMIN-TEMPLATES] Error updating feature:', error);
|
||
console.error('❌ [ADMIN-TEMPLATES] Full error stack:', error.stack);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Failed to update feature',
|
||
message: error.message,
|
||
details: error.stack
|
||
});
|
||
}
|
||
});
|
||
|
||
// DELETE /api/admin/templates/:templateId/features/:featureId - Remove feature
|
||
router.delete('/:templateId/features/:featureId', async (req, res) => {
|
||
try {
|
||
const { templateId, featureId } = req.params;
|
||
|
||
console.log('🗑️ [ADMIN-TEMPLATES] Removing feature:', featureId, 'from template:', templateId);
|
||
|
||
// Validate template exists
|
||
const template = await Template.getByIdWithFeatures(templateId);
|
||
if (!template) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
error: 'Template not found',
|
||
message: `Template with ID ${templateId} does not exist`
|
||
});
|
||
}
|
||
|
||
// Delete the feature from template_features table
|
||
const deleteQuery = `
|
||
DELETE FROM template_features
|
||
WHERE id = $1 AND template_id = $2
|
||
RETURNING id
|
||
`;
|
||
|
||
const result = await database.query(deleteQuery, [featureId, templateId]);
|
||
|
||
if (result.rows.length === 0) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
error: 'Feature not found',
|
||
message: `Feature with ID ${featureId} does not exist in template ${templateId}`
|
||
});
|
||
}
|
||
|
||
console.log('✅ [ADMIN-TEMPLATES] Feature deleted:', featureId);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Feature removed successfully'
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ [ADMIN-TEMPLATES] Error removing feature:', error.message);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Failed to remove feature',
|
||
message: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// POST /api/admin/templates/:id/features/bulk - Bulk add features to template
|
||
router.post('/:id/features/bulk', async (req, res) => {
|
||
try {
|
||
const { id } = req.params;
|
||
const { features } = req.body;
|
||
|
||
console.log('📦 [ADMIN-TEMPLATES] Bulk adding features to template:', id);
|
||
console.log('📋 [ADMIN-TEMPLATES] Features count:', features?.length || 0);
|
||
|
||
if (!features || !Array.isArray(features) || features.length === 0) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: 'Invalid features data',
|
||
message: 'Features array is required and must not be empty'
|
||
});
|
||
}
|
||
|
||
// Validate template exists
|
||
const template = await Template.getByIdWithFeatures(id);
|
||
if (!template) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
error: 'Template not found',
|
||
message: `Template with ID ${id} does not exist`
|
||
});
|
||
}
|
||
|
||
// Create all features in template_features table
|
||
const createdFeatures = [];
|
||
let displayOrder = template.features ? template.features.length + 1 : 1;
|
||
|
||
for (const featureData of features) {
|
||
try {
|
||
// Use Feature.create() method to ensure business rules are stored
|
||
const feature = await Feature.create({
|
||
template_id: id,
|
||
name: featureData.name,
|
||
description: featureData.description || '',
|
||
feature_type: featureData.feature_type || 'custom',
|
||
complexity: featureData.complexity || 'medium',
|
||
display_order: displayOrder++,
|
||
is_default: featureData.is_default || false,
|
||
created_by_user: featureData.created_by_user || true,
|
||
logic_rules: featureData.logic_rules,
|
||
business_rules: featureData.business_rules
|
||
});
|
||
|
||
createdFeatures.push(feature);
|
||
} catch (featureError) {
|
||
console.error('⚠️ [ADMIN-TEMPLATES] Error creating feature:', featureData.name, featureError.message);
|
||
// Continue with other features instead of failing completely
|
||
}
|
||
}
|
||
|
||
console.log('✅ [ADMIN-TEMPLATES] Bulk features created:', createdFeatures.length, 'out of', features.length);
|
||
|
||
res.status(201).json({
|
||
success: true,
|
||
data: createdFeatures,
|
||
message: `${createdFeatures.length} features added to template '${template.title}'`
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ [ADMIN-TEMPLATES] Error bulk adding features:', error.message);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Failed to bulk add features',
|
||
message: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
module.exports = router;
|