add changes
This commit is contained in:
481
controllers/category.controller.js
Normal file
481
controllers/category.controller.js
Normal file
@@ -0,0 +1,481 @@
|
||||
const { Category, Question } = require('../models');
|
||||
|
||||
/**
|
||||
* @desc Get all active categories
|
||||
* @route GET /api/categories
|
||||
* @access Public
|
||||
*/
|
||||
exports.getAllCategories = async (req, res) => {
|
||||
try {
|
||||
// Check if request is from guest or authenticated user
|
||||
const isGuest = !req.user; // If no user attached, it's a guest/public request
|
||||
|
||||
// Build query conditions
|
||||
const whereConditions = {
|
||||
isActive: true
|
||||
};
|
||||
|
||||
// If guest, only show guest-accessible categories
|
||||
if (isGuest) {
|
||||
whereConditions.guestAccessible = true;
|
||||
}
|
||||
|
||||
// Fetch categories
|
||||
const categories = await Category.findAll({
|
||||
where: whereConditions,
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
'icon',
|
||||
'color',
|
||||
'questionCount',
|
||||
'displayOrder',
|
||||
'guestAccessible'
|
||||
],
|
||||
order: [
|
||||
['displayOrder', 'ASC'],
|
||||
['name', 'ASC']
|
||||
]
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: categories.length,
|
||||
data: categories,
|
||||
message: isGuest
|
||||
? `${categories.length} guest-accessible categories available`
|
||||
: `${categories.length} categories available`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching categories',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Get category details by ID
|
||||
* @route GET /api/categories/:id
|
||||
* @access Public (with optional auth for access control)
|
||||
*/
|
||||
exports.getCategoryById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const isGuest = !req.user;
|
||||
|
||||
// Validate ID (accepts UUID or numeric)
|
||||
if (!id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid category ID'
|
||||
});
|
||||
}
|
||||
|
||||
// UUID format validation (basic check)
|
||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
||||
const isNumeric = !isNaN(id) && Number.isInteger(Number(id));
|
||||
|
||||
if (!isUUID && !isNumeric) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid category ID format'
|
||||
});
|
||||
}
|
||||
|
||||
// Find category
|
||||
const category = await Category.findByPk(id, {
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
'icon',
|
||||
'color',
|
||||
'questionCount',
|
||||
'displayOrder',
|
||||
'guestAccessible',
|
||||
'isActive'
|
||||
]
|
||||
});
|
||||
|
||||
// Check if category exists
|
||||
if (!category) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Category not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if category is active
|
||||
if (!category.isActive) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Category not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check guest access
|
||||
if (isGuest && !category.guestAccessible) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'This category requires authentication. Please register or login to access.',
|
||||
requiresAuth: true
|
||||
});
|
||||
}
|
||||
|
||||
// Get question preview (first 5 questions)
|
||||
const questionPreview = await Question.findAll({
|
||||
where: {
|
||||
categoryId: id,
|
||||
isActive: true
|
||||
},
|
||||
attributes: [
|
||||
'id',
|
||||
'questionText',
|
||||
'questionType',
|
||||
'difficulty',
|
||||
'points',
|
||||
'timesAttempted',
|
||||
'timesCorrect'
|
||||
],
|
||||
order: [['createdAt', 'ASC']],
|
||||
limit: 5
|
||||
});
|
||||
|
||||
// Calculate category stats
|
||||
const allQuestions = await Question.findAll({
|
||||
where: {
|
||||
categoryId: id,
|
||||
isActive: true
|
||||
},
|
||||
attributes: ['difficulty', 'timesAttempted', 'timesCorrect']
|
||||
});
|
||||
|
||||
const stats = {
|
||||
totalQuestions: allQuestions.length,
|
||||
questionsByDifficulty: {
|
||||
easy: allQuestions.filter(q => q.difficulty === 'easy').length,
|
||||
medium: allQuestions.filter(q => q.difficulty === 'medium').length,
|
||||
hard: allQuestions.filter(q => q.difficulty === 'hard').length
|
||||
},
|
||||
totalAttempts: allQuestions.reduce((sum, q) => sum + (q.timesAttempted || 0), 0),
|
||||
totalCorrect: allQuestions.reduce((sum, q) => sum + (q.timesCorrect || 0), 0)
|
||||
};
|
||||
|
||||
// Calculate average accuracy
|
||||
stats.averageAccuracy = stats.totalAttempts > 0
|
||||
? Math.round((stats.totalCorrect / stats.totalAttempts) * 100)
|
||||
: 0;
|
||||
|
||||
// Prepare response
|
||||
const categoryData = {
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
slug: category.slug,
|
||||
description: category.description,
|
||||
icon: category.icon,
|
||||
color: category.color,
|
||||
questionCount: category.questionCount,
|
||||
displayOrder: category.displayOrder,
|
||||
guestAccessible: category.guestAccessible
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
category: categoryData,
|
||||
questionPreview: questionPreview.map(q => ({
|
||||
id: q.id,
|
||||
questionText: q.questionText,
|
||||
questionType: q.questionType,
|
||||
difficulty: q.difficulty,
|
||||
points: q.points,
|
||||
accuracy: q.timesAttempted > 0
|
||||
? Math.round((q.timesCorrect / q.timesAttempted) * 100)
|
||||
: 0
|
||||
})),
|
||||
stats
|
||||
},
|
||||
message: `Category details retrieved successfully`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching category details:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching category details',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Create new category (Admin only)
|
||||
* @route POST /api/categories
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.createCategory = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
icon,
|
||||
color,
|
||||
guestAccessible,
|
||||
displayOrder
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Category name is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if category with same name exists
|
||||
const existingByName = await Category.findOne({
|
||||
where: { name }
|
||||
});
|
||||
|
||||
if (existingByName) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'A category with this name already exists'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if custom slug provided and if it exists
|
||||
if (slug) {
|
||||
const existingBySlug = await Category.findOne({
|
||||
where: { slug }
|
||||
});
|
||||
|
||||
if (existingBySlug) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'A category with this slug already exists'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create category (slug will be auto-generated by model hook if not provided)
|
||||
const category = await Category.create({
|
||||
name,
|
||||
slug,
|
||||
description: description || null,
|
||||
icon: icon || null,
|
||||
color: color || '#3B82F6',
|
||||
guestAccessible: guestAccessible !== undefined ? guestAccessible : false,
|
||||
displayOrder: displayOrder || 0,
|
||||
isActive: true,
|
||||
questionCount: 0,
|
||||
quizCount: 0
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: {
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
slug: category.slug,
|
||||
description: category.description,
|
||||
icon: category.icon,
|
||||
color: category.color,
|
||||
guestAccessible: category.guestAccessible,
|
||||
displayOrder: category.displayOrder,
|
||||
questionCount: category.questionCount,
|
||||
isActive: category.isActive
|
||||
},
|
||||
message: 'Category created successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating category:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error creating category',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Update category (Admin only)
|
||||
* @route PUT /api/categories/:id
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.updateCategory = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
icon,
|
||||
color,
|
||||
guestAccessible,
|
||||
displayOrder,
|
||||
isActive
|
||||
} = req.body;
|
||||
|
||||
// Validate ID
|
||||
if (!id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Category ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Find category
|
||||
const category = await Category.findByPk(id);
|
||||
|
||||
if (!category) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Category not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if new name conflicts with existing category
|
||||
if (name && name !== category.name) {
|
||||
const existingByName = await Category.findOne({
|
||||
where: { name }
|
||||
});
|
||||
|
||||
if (existingByName) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'A category with this name already exists'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if new slug conflicts with existing category
|
||||
if (slug && slug !== category.slug) {
|
||||
const existingBySlug = await Category.findOne({
|
||||
where: { slug }
|
||||
});
|
||||
|
||||
if (existingBySlug) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'A category with this slug already exists'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update category
|
||||
const updateData = {};
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (slug !== undefined) updateData.slug = slug;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (icon !== undefined) updateData.icon = icon;
|
||||
if (color !== undefined) updateData.color = color;
|
||||
if (guestAccessible !== undefined) updateData.guestAccessible = guestAccessible;
|
||||
if (displayOrder !== undefined) updateData.displayOrder = displayOrder;
|
||||
if (isActive !== undefined) updateData.isActive = isActive;
|
||||
|
||||
await category.update(updateData);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
slug: category.slug,
|
||||
description: category.description,
|
||||
icon: category.icon,
|
||||
color: category.color,
|
||||
guestAccessible: category.guestAccessible,
|
||||
displayOrder: category.displayOrder,
|
||||
questionCount: category.questionCount,
|
||||
isActive: category.isActive
|
||||
},
|
||||
message: 'Category updated successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating category:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error updating category',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Delete category (soft delete - Admin only)
|
||||
* @route DELETE /api/categories/:id
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.deleteCategory = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Validate ID
|
||||
if (!id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Category ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Find category
|
||||
const category = await Category.findByPk(id);
|
||||
|
||||
if (!category) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Category not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if already deleted
|
||||
if (!category.isActive) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Category is already deleted'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if category has questions
|
||||
const questionCount = await Question.count({
|
||||
where: {
|
||||
categoryId: id,
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
|
||||
// Soft delete - set isActive to false
|
||||
await category.update({ isActive: false });
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
questionCount: questionCount
|
||||
},
|
||||
message: questionCount > 0
|
||||
? `Category deleted successfully. ${questionCount} questions are still associated with this category.`
|
||||
: 'Category deleted successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting category:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error deleting category',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user