first commit
This commit is contained in:
260
src/controllers/admin/categoryController.js
Normal file
260
src/controllers/admin/categoryController.js
Normal file
@@ -0,0 +1,260 @@
|
||||
const { prisma } = require('../../config/database');
|
||||
|
||||
exports.getAllCategories = async (req, res, next) => {
|
||||
try {
|
||||
const categories = await prisma.category.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
// res.json({ success: true, data: categories });
|
||||
res.status(200).json({
|
||||
// statusCode: 200,
|
||||
status: true,
|
||||
message: 'Categories fetched successfully',
|
||||
data: categories,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.createCategory = async (req, res, next) => {
|
||||
try {
|
||||
const { name, description, image, parentId, metaTitle, metaDescription } =
|
||||
req.body;
|
||||
|
||||
// Generate slug from name
|
||||
let slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
|
||||
// Check if slug already exists with the same parent
|
||||
const existing = await prisma.category.findFirst({
|
||||
where: {
|
||||
slug,
|
||||
parentId: parentId || null, // Handle both subcategories and root categories
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// If exists under same parent, append timestamp to make it unique
|
||||
slug = `${slug}-${Date.now()}`;
|
||||
}
|
||||
|
||||
const category = await prisma.category.create({
|
||||
data: {
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
image,
|
||||
parentId,
|
||||
metaTitle,
|
||||
metaDescription,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
statusCode: 201,
|
||||
status: true,
|
||||
message: 'Category created successfully',
|
||||
data: category,
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle Prisma duplicate error explicitly
|
||||
if (error.code === 'P2002') {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Duplicate field value entered',
|
||||
});
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateCategory = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, description, image, parentId, metaTitle, metaDescription } =
|
||||
req.body;
|
||||
|
||||
let slug;
|
||||
if (name) {
|
||||
slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
}
|
||||
|
||||
const category = await prisma.category.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
image,
|
||||
parentId,
|
||||
metaTitle,
|
||||
metaDescription,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
status: true,
|
||||
message: 'Category updated successfully',
|
||||
data: category,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.deleteCategory = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
await prisma.category.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
res.json({
|
||||
status: true,
|
||||
message: 'Category deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.toggleCategoryStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { isActive } = req.body;
|
||||
|
||||
// 1️⃣ Update parent category
|
||||
const parentCategory = await prisma.category.update({
|
||||
where: { id },
|
||||
data: { isActive },
|
||||
});
|
||||
|
||||
// 2️⃣ If parent is being deactivated, also deactivate all children recursively
|
||||
if (!isActive) {
|
||||
const deactivateChildren = async parentId => {
|
||||
const children = await prisma.category.findMany({
|
||||
where: { parentId },
|
||||
});
|
||||
|
||||
for (const child of children) {
|
||||
await prisma.category.update({
|
||||
where: { id: child.id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
// Recursive call for nested subcategories
|
||||
await deactivateChildren(child.id);
|
||||
}
|
||||
};
|
||||
|
||||
await deactivateChildren(id);
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: true,
|
||||
message: `Category ${isActive ? 'activated' : 'deactivated'} successfully`,
|
||||
data: parentCategory,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.reorderCategories = async (req, res) => {
|
||||
const { orders } = req.body;
|
||||
|
||||
await Promise.all(
|
||||
orders.map(item =>
|
||||
prisma.category.update({
|
||||
where: { id: item.id },
|
||||
data: { sequence: item.sequence },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
res.json({
|
||||
status: true,
|
||||
message: 'Category order updated',
|
||||
});
|
||||
};
|
||||
|
||||
exports.getCategoryHierarchy = async (req, res, next) => {
|
||||
try {
|
||||
// 1. Fetch all categories
|
||||
const categories = await prisma.category.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
|
||||
// 2. Convert array to a lookup map
|
||||
const lookup = {};
|
||||
categories.forEach(cat => {
|
||||
lookup[cat.id] = { ...cat, children: [] };
|
||||
});
|
||||
|
||||
const hierarchy = [];
|
||||
|
||||
// 3. Build hierarchical structure
|
||||
categories.forEach(cat => {
|
||||
if (cat.parentId) {
|
||||
lookup[cat.parentId].children.push(lookup[cat.id]);
|
||||
} else {
|
||||
hierarchy.push(lookup[cat.id]);
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
// statusCode: 200,
|
||||
status: true,
|
||||
message: 'Category hierarchy fetched successfully',
|
||||
data: hierarchy,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getCategoryById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Category ID is required',
|
||||
});
|
||||
}
|
||||
|
||||
const category = await prisma.category.findUnique({
|
||||
where: {
|
||||
id: id, // ✅ PASS THE ACTUAL STRING VALUE
|
||||
},
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Category not found',
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Category details fetched successfully',
|
||||
data: category,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get category by id error:', error);
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching category',
|
||||
});
|
||||
}
|
||||
};
|
||||
463
src/controllers/admin/couponController.js
Normal file
463
src/controllers/admin/couponController.js
Normal file
@@ -0,0 +1,463 @@
|
||||
// const { prisma } = require('../../config/database');
|
||||
|
||||
// exports.getAllCoupons = async (req, res, next) => {
|
||||
// try {
|
||||
// const coupons = await prisma.coupon.findMany({
|
||||
// orderBy: { createdAt: 'desc' },
|
||||
// });
|
||||
// // res.json({ success: true, data: coupons });
|
||||
// res.status(200).json({
|
||||
// // statusCode: 200,
|
||||
// status: true,
|
||||
// message: 'Coupons fetched successfully',
|
||||
// data: coupons,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// exports.createCoupon = async (req, res, next) => {
|
||||
// try {
|
||||
// const {
|
||||
// code,
|
||||
// description,
|
||||
// type,
|
||||
// value,
|
||||
// minOrderAmount,
|
||||
// maxUses,
|
||||
// validFrom,
|
||||
// validUntil,
|
||||
// } = req.body;
|
||||
// const coupon = await prisma.coupon.create({
|
||||
// data: {
|
||||
// code,
|
||||
// description,
|
||||
// type,
|
||||
// value,
|
||||
// minOrderAmount,
|
||||
// maxUses,
|
||||
// validFrom: new Date(validFrom),
|
||||
// validUntil: new Date(validUntil),
|
||||
// },
|
||||
// });
|
||||
|
||||
// res.status(201).json({
|
||||
// // statusCode: 201,
|
||||
// status: true,
|
||||
// message: 'Coupon created successfully',
|
||||
// data: coupon,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// controllers/admin/couponController.js - ENHANCED VERSION
|
||||
|
||||
const { prisma } = require('../../config/database');
|
||||
|
||||
// ==========================================
|
||||
// ADMIN COUPON MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* @desc Get all coupons (with filters)
|
||||
* @route GET /api/admin/coupons
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.getAllCoupons = async (req, res, next) => {
|
||||
try {
|
||||
const { isActive, type, search, page = 1, limit = 20 } = req.query;
|
||||
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
// Build filter
|
||||
const where = {};
|
||||
|
||||
if (isActive !== undefined) {
|
||||
where.isActive = isActive === 'true';
|
||||
}
|
||||
|
||||
if (type) {
|
||||
where.type = type;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ code: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
const [coupons, total] = await Promise.all([
|
||||
prisma.coupon.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: parseInt(limit),
|
||||
}),
|
||||
prisma.coupon.count({ where }),
|
||||
]);
|
||||
|
||||
// Add usage statistics
|
||||
const couponsWithStats = coupons.map(coupon => ({
|
||||
...coupon,
|
||||
usagePercentage: coupon.maxUses
|
||||
? Math.round((coupon.usedCount / coupon.maxUses) * 100)
|
||||
: 0,
|
||||
isExpired: new Date() > new Date(coupon.validUntil),
|
||||
isNotStarted: new Date() < new Date(coupon.validFrom),
|
||||
remainingUses: coupon.maxUses ? coupon.maxUses - coupon.usedCount : null,
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Coupons fetched successfully',
|
||||
data: {
|
||||
coupons: couponsWithStats,
|
||||
pagination: {
|
||||
total,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
pages: Math.ceil(total / parseInt(limit)),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Get single coupon by ID
|
||||
* @route GET /api/admin/coupons/:id
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.getCouponById = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const coupon = await prisma.coupon.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!coupon) {
|
||||
return res.status(404).json({
|
||||
status: false,
|
||||
message: 'Coupon not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Get usage statistics
|
||||
const usageStats = {
|
||||
...coupon,
|
||||
usagePercentage: coupon.maxUses
|
||||
? Math.round((coupon.usedCount / coupon.maxUses) * 100)
|
||||
: 0,
|
||||
isExpired: new Date() > new Date(coupon.validUntil),
|
||||
isNotStarted: new Date() < new Date(coupon.validFrom),
|
||||
remainingUses: coupon.maxUses ? coupon.maxUses - coupon.usedCount : null,
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Coupon fetched successfully',
|
||||
data: usageStats,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Create new coupon
|
||||
* @route POST /api/admin/coupons
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.createCoupon = async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
code,
|
||||
description,
|
||||
type,
|
||||
value,
|
||||
minOrderAmount,
|
||||
maxUses,
|
||||
validFrom,
|
||||
validUntil,
|
||||
isActive = true,
|
||||
} = req.body;
|
||||
|
||||
// Validate code uniqueness
|
||||
const existingCoupon = await prisma.coupon.findUnique({
|
||||
where: { code: code.toUpperCase() },
|
||||
});
|
||||
|
||||
if (existingCoupon) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: 'Coupon code already exists',
|
||||
});
|
||||
}
|
||||
|
||||
// Validate dates
|
||||
const fromDate = new Date(validFrom);
|
||||
const untilDate = new Date(validUntil);
|
||||
|
||||
if (fromDate >= untilDate) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: 'Valid until date must be after valid from date',
|
||||
});
|
||||
}
|
||||
|
||||
// Validate value based on type
|
||||
if (type === 'PERCENTAGE' && (value < 0 || value > 100)) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: 'Percentage value must be between 0 and 100',
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'FIXED_AMOUNT' && value < 0) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: 'Fixed amount must be greater than 0',
|
||||
});
|
||||
}
|
||||
|
||||
const coupon = await prisma.coupon.create({
|
||||
data: {
|
||||
code: code.toUpperCase(),
|
||||
description,
|
||||
type,
|
||||
value: parseFloat(value),
|
||||
minOrderAmount: minOrderAmount ? parseFloat(minOrderAmount) : null,
|
||||
maxUses: maxUses ? parseInt(maxUses) : null,
|
||||
validFrom: fromDate,
|
||||
validUntil: untilDate,
|
||||
isActive,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
status: true,
|
||||
message: 'Coupon created successfully',
|
||||
data: coupon,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create coupon error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Update coupon
|
||||
* @route PUT /api/admin/coupons/:id
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.updateCoupon = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
code,
|
||||
description,
|
||||
type,
|
||||
value,
|
||||
minOrderAmount,
|
||||
maxUses,
|
||||
validFrom,
|
||||
validUntil,
|
||||
isActive,
|
||||
} = req.body;
|
||||
|
||||
// Check if coupon exists
|
||||
const existingCoupon = await prisma.coupon.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!existingCoupon) {
|
||||
return res.status(404).json({
|
||||
status: false,
|
||||
message: 'Coupon not found',
|
||||
});
|
||||
}
|
||||
|
||||
// If code is being changed, check uniqueness
|
||||
if (code && code.toUpperCase() !== existingCoupon.code) {
|
||||
const duplicateCoupon = await prisma.coupon.findUnique({
|
||||
where: { code: code.toUpperCase() },
|
||||
});
|
||||
|
||||
if (duplicateCoupon) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: 'Coupon code already exists',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validate dates if provided
|
||||
const fromDate = validFrom ? new Date(validFrom) : existingCoupon.validFrom;
|
||||
const untilDate = validUntil
|
||||
? new Date(validUntil)
|
||||
: existingCoupon.validUntil;
|
||||
|
||||
if (fromDate >= untilDate) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: 'Valid until date must be after valid from date',
|
||||
});
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (code) updateData.code = code.toUpperCase();
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (type) updateData.type = type;
|
||||
if (value !== undefined) updateData.value = parseFloat(value);
|
||||
if (minOrderAmount !== undefined)
|
||||
updateData.minOrderAmount = minOrderAmount
|
||||
? parseFloat(minOrderAmount)
|
||||
: null;
|
||||
if (maxUses !== undefined)
|
||||
updateData.maxUses = maxUses ? parseInt(maxUses) : null;
|
||||
if (validFrom) updateData.validFrom = fromDate;
|
||||
if (validUntil) updateData.validUntil = untilDate;
|
||||
if (isActive !== undefined) updateData.isActive = isActive;
|
||||
|
||||
const coupon = await prisma.coupon.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Coupon updated successfully',
|
||||
data: coupon,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Delete coupon
|
||||
* @route DELETE /api/admin/coupons/:id
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.deleteCoupon = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const coupon = await prisma.coupon.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!coupon) {
|
||||
return res.status(404).json({
|
||||
status: false,
|
||||
message: 'Coupon not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if coupon has been used
|
||||
if (coupon.usedCount > 0) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message:
|
||||
'Cannot delete a coupon that has been used. Consider deactivating it instead.',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.coupon.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Coupon deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Toggle coupon active status
|
||||
* @route PATCH /api/admin/coupons/:id/toggle
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.toggleCouponStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const coupon = await prisma.coupon.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!coupon) {
|
||||
return res.status(404).json({
|
||||
status: false,
|
||||
message: 'Coupon not found',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedCoupon = await prisma.coupon.update({
|
||||
where: { id },
|
||||
data: { isActive: !coupon.isActive },
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: `Coupon ${updatedCoupon.isActive ? 'activated' : 'deactivated'} successfully`,
|
||||
data: updatedCoupon,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Get coupon statistics
|
||||
* @route GET /api/admin/coupons/stats/overview
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.getCouponStats = async (req, res, next) => {
|
||||
try {
|
||||
const [totalCoupons, activeCoupons, expiredCoupons, totalRedemptions] =
|
||||
await Promise.all([
|
||||
prisma.coupon.count(),
|
||||
prisma.coupon.count({ where: { isActive: true } }),
|
||||
prisma.coupon.count({
|
||||
where: {
|
||||
validUntil: { lt: new Date() },
|
||||
},
|
||||
}),
|
||||
prisma.coupon.aggregate({
|
||||
_sum: { usedCount: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
// Get most used coupons
|
||||
const mostUsed = await prisma.coupon.findMany({
|
||||
where: { usedCount: { gt: 0 } },
|
||||
orderBy: { usedCount: 'desc' },
|
||||
take: 5,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Coupon statistics fetched successfully',
|
||||
data: {
|
||||
totalCoupons,
|
||||
activeCoupons,
|
||||
expiredCoupons,
|
||||
totalRedemptions: totalRedemptions._sum.usedCount || 0,
|
||||
mostUsedCoupons: mostUsed,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
826
src/controllers/admin/dashboardController.js
Normal file
826
src/controllers/admin/dashboardController.js
Normal file
@@ -0,0 +1,826 @@
|
||||
// const { prisma } = require('../../config/database');
|
||||
// const Product = require('../../models/mongodb/Product');
|
||||
|
||||
// exports.getDashboardStats = async (req, res, next) => {
|
||||
// try {
|
||||
// const [
|
||||
// totalUsers,
|
||||
// totalOrders,
|
||||
// totalProducts,
|
||||
// totalRevenue,
|
||||
// recentOrders,
|
||||
// topProducts,
|
||||
// ] = await Promise.all([
|
||||
// prisma.user.count(),
|
||||
// prisma.order.count(),
|
||||
// Product.countDocuments(),
|
||||
// prisma.order.aggregate({
|
||||
// _sum: { totalAmount: true },
|
||||
// where: { paymentStatus: 'PAID' },
|
||||
// }),
|
||||
// prisma.order.findMany({
|
||||
// take: 5,
|
||||
// orderBy: { createdAt: 'desc' },
|
||||
// include: {
|
||||
// user: {
|
||||
// select: { id: true, email: true, firstName: true, lastName: true },
|
||||
// },
|
||||
// items: true,
|
||||
// },
|
||||
// }),
|
||||
// Product.find({ status: 'active' })
|
||||
// .sort({ purchaseCount: -1 })
|
||||
// .limit(5)
|
||||
// .lean()
|
||||
// .then(products =>
|
||||
// products.map(product => {
|
||||
// // ✅ Fixed: Check gallery first since that's where your images are
|
||||
// let image =
|
||||
// (product.images?.gallery && product.images.gallery.length > 0
|
||||
// ? product.images.gallery[0]
|
||||
// : null) ||
|
||||
// product.images?.primary ||
|
||||
// (product.variants?.[0]?.images && product.variants[0].images.length > 0
|
||||
// ? product.variants[0].images[0]
|
||||
// : null) ||
|
||||
// 'https://via.placeholder.com/300';
|
||||
|
||||
// return {
|
||||
// ...product,
|
||||
// displayImage: image,
|
||||
// };
|
||||
// })
|
||||
// ),
|
||||
// ]);
|
||||
|
||||
// // ✅ NEW: Fetch product details for recent orders
|
||||
// const orderProductIds = recentOrders
|
||||
// .flatMap(order => order.items.map(item => item.productId))
|
||||
// .filter(Boolean);
|
||||
|
||||
// const orderProducts = await Product.find({
|
||||
// _id: { $in: orderProductIds }
|
||||
// }).lean();
|
||||
|
||||
// // ✅ Create a map of productId -> product for quick lookup
|
||||
// const productMap = {};
|
||||
// orderProducts.forEach(product => {
|
||||
// const image =
|
||||
// (product.images?.gallery && product.images.gallery.length > 0
|
||||
// ? product.images.gallery[0]
|
||||
// : null) ||
|
||||
// product.images?.primary ||
|
||||
// (product.variants?.[0]?.images && product.variants[0].images.length > 0
|
||||
// ? product.variants[0].images[0]
|
||||
// : null) ||
|
||||
// 'https://via.placeholder.com/300';
|
||||
|
||||
// productMap[product._id.toString()] = {
|
||||
// ...product,
|
||||
// displayImage: image,
|
||||
// };
|
||||
// });
|
||||
|
||||
// // ✅ Enhance recent orders with product images
|
||||
// const enhancedRecentOrders = recentOrders.map(order => ({
|
||||
// ...order,
|
||||
// items: order.items.map(item => ({
|
||||
// ...item,
|
||||
// productImage: productMap[item.productId]?.displayImage || 'https://via.placeholder.com/300',
|
||||
// productDetails: productMap[item.productId] || null,
|
||||
// })),
|
||||
// }));
|
||||
|
||||
// res.status(200).json({
|
||||
// statusCode: 200,
|
||||
// status: true,
|
||||
// message: 'Dashboard stats fetched successfully',
|
||||
// data: {
|
||||
// totalUsers,
|
||||
// totalOrders,
|
||||
// totalProducts,
|
||||
// totalRevenue: totalRevenue._sum.totalAmount || 0,
|
||||
// recentOrders: enhancedRecentOrders,
|
||||
// topProducts,
|
||||
// },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
|
||||
// controllers/admin/dashboardController.js - WITH ORDER OVERVIEW GRAPHS
|
||||
|
||||
const { prisma } = require('../../config/database');
|
||||
const Product = require('../../models/mongodb/Product');
|
||||
const { getLowStockProducts, getInventoryStats } = require('../../services/inventoryService');
|
||||
|
||||
/**
|
||||
* @desc Get dashboard stats with order overview graphs
|
||||
* @route GET /api/admin/dashboard/stats
|
||||
* @access Private/Admin
|
||||
*/
|
||||
// exports.getDashboardStats = async (req, res, next) => {
|
||||
// try {
|
||||
// const [
|
||||
// totalUsers,
|
||||
// totalOrders,
|
||||
// totalProducts,
|
||||
// totalRevenue,
|
||||
// recentOrders,
|
||||
// topProducts,
|
||||
// orderOverview,
|
||||
// revenueOverview,
|
||||
// ordersByStatus,
|
||||
// monthlyComparison,
|
||||
// ] = await Promise.all([
|
||||
// // Total Users
|
||||
// prisma.user.count(),
|
||||
|
||||
// // Total Orders
|
||||
// prisma.order.count(),
|
||||
|
||||
// // Total Products
|
||||
// Product.countDocuments(),
|
||||
|
||||
// // Total Revenue
|
||||
// prisma.order.aggregate({
|
||||
// _sum: { totalAmount: true },
|
||||
// where: { paymentStatus: 'PAID' },
|
||||
// }),
|
||||
|
||||
// // Recent Orders
|
||||
// prisma.order.findMany({
|
||||
// take: 5,
|
||||
// orderBy: { createdAt: 'desc' },
|
||||
// include: {
|
||||
// user: {
|
||||
// select: { id: true, email: true, firstName: true, lastName: true },
|
||||
// },
|
||||
// items: true,
|
||||
// },
|
||||
// }),
|
||||
|
||||
// // Top Products
|
||||
// Product.find({ status: 'active' })
|
||||
// .sort({ purchaseCount: -1 })
|
||||
// .limit(5)
|
||||
// .lean()
|
||||
// .then(products =>
|
||||
// products.map(product => {
|
||||
// let image =
|
||||
// (product.images?.gallery && product.images.gallery.length > 0
|
||||
// ? product.images.gallery[0]
|
||||
// : null) ||
|
||||
// product.images?.primary ||
|
||||
// (product.variants?.[0]?.images && product.variants[0].images.length > 0
|
||||
// ? product.variants[0].images[0]
|
||||
// : null) ||
|
||||
// 'https://via.placeholder.com/300';
|
||||
|
||||
// return {
|
||||
// ...product,
|
||||
// displayImage: image,
|
||||
// };
|
||||
// })
|
||||
// ),
|
||||
|
||||
// // ✅ Order Overview (Last 30 Days)
|
||||
// getOrderOverview(),
|
||||
|
||||
// // ✅ Revenue Overview (Last 30 Days)
|
||||
// getRevenueOverview(),
|
||||
|
||||
// // ✅ Orders by Status
|
||||
// getOrdersByStatus(),
|
||||
|
||||
// // ✅ Monthly Comparison (Current vs Previous Month)
|
||||
// getMonthlyComparison(),
|
||||
// ]);
|
||||
|
||||
// // Enhance recent orders with product images
|
||||
// const orderProductIds = recentOrders
|
||||
// .flatMap(order => order.items.map(item => item.productId))
|
||||
// .filter(Boolean);
|
||||
|
||||
// const orderProducts = await Product.find({
|
||||
// _id: { $in: orderProductIds }
|
||||
// }).lean();
|
||||
|
||||
// const productMap = {};
|
||||
// orderProducts.forEach(product => {
|
||||
// const image =
|
||||
// (product.images?.gallery && product.images.gallery.length > 0
|
||||
// ? product.images.gallery[0]
|
||||
// : null) ||
|
||||
// product.images?.primary ||
|
||||
// (product.variants?.[0]?.images && product.variants[0].images.length > 0
|
||||
// ? product.variants[0].images[0]
|
||||
// : null) ||
|
||||
// 'https://via.placeholder.com/300';
|
||||
|
||||
// productMap[product._id.toString()] = {
|
||||
// ...product,
|
||||
// displayImage: image,
|
||||
// };
|
||||
// });
|
||||
|
||||
// const enhancedRecentOrders = recentOrders.map(order => ({
|
||||
// ...order,
|
||||
// items: order.items.map(item => ({
|
||||
// ...item,
|
||||
// productImage: productMap[item.productId]?.displayImage || 'https://via.placeholder.com/300',
|
||||
// productDetails: productMap[item.productId] || null,
|
||||
// })),
|
||||
// }));
|
||||
|
||||
// res.status(200).json({
|
||||
// statusCode: 200,
|
||||
// status: true,
|
||||
// message: 'Dashboard stats fetched successfully',
|
||||
// data: {
|
||||
// // Summary Stats
|
||||
// totalUsers,
|
||||
// totalOrders,
|
||||
// totalProducts,
|
||||
// totalRevenue: parseFloat(totalRevenue._sum.totalAmount || 0),
|
||||
|
||||
// // Lists
|
||||
// recentOrders: enhancedRecentOrders,
|
||||
// topProducts,
|
||||
|
||||
// // ✅ Graph Data
|
||||
// charts: {
|
||||
// orderOverview,
|
||||
// revenueOverview,
|
||||
// ordersByStatus,
|
||||
// monthlyComparison,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.error('Dashboard stats error:', error);
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// ==========================================
|
||||
// HELPER FUNCTIONS FOR GRAPHS
|
||||
// ==========================================
|
||||
|
||||
|
||||
|
||||
exports.getDashboardStats = async (req, res, next) => {
|
||||
try {
|
||||
const [
|
||||
totalUsers,
|
||||
totalOrders,
|
||||
totalProducts,
|
||||
totalRevenue,
|
||||
recentOrders,
|
||||
topSellingProducts,
|
||||
lowStockProducts,
|
||||
inventoryStats,
|
||||
orderOverview,
|
||||
revenueOverview,
|
||||
ordersByStatus,
|
||||
monthlyComparison,
|
||||
] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.order.count(),
|
||||
Product.countDocuments({ status: 'active' }),
|
||||
prisma.order.aggregate({
|
||||
_sum: { totalAmount: true },
|
||||
where: { paymentStatus: 'PAID' },
|
||||
}),
|
||||
prisma.order.findMany({
|
||||
take: 5,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
user: {
|
||||
select: { id: true, email: true, firstName: true, lastName: true },
|
||||
},
|
||||
items: true,
|
||||
},
|
||||
}),
|
||||
getTopSellingProducts(), // ✅ Real top sellers
|
||||
getLowStockProducts(10), // ✅ Low stock alerts
|
||||
getInventoryStats(), // ✅ Inventory overview
|
||||
getOrderOverview(),
|
||||
getRevenueOverview(),
|
||||
getOrdersByStatus(),
|
||||
getMonthlyComparison(),
|
||||
]);
|
||||
|
||||
// Enhance recent orders with product images
|
||||
const orderProductIds = recentOrders
|
||||
.flatMap(order => order.items.map(item => item.productId))
|
||||
.filter(Boolean);
|
||||
|
||||
const orderProducts = await Product.find({
|
||||
_id: { $in: orderProductIds }
|
||||
}).lean();
|
||||
|
||||
const productMap = {};
|
||||
orderProducts.forEach(product => {
|
||||
productMap[product._id.toString()] = {
|
||||
...product,
|
||||
displayImage: getProductImage(product),
|
||||
};
|
||||
});
|
||||
|
||||
const enhancedRecentOrders = recentOrders.map(order => ({
|
||||
...order,
|
||||
items: order.items.map(item => ({
|
||||
...item,
|
||||
productImage: productMap[item.productId]?.displayImage || 'https://via.placeholder.com/300',
|
||||
productDetails: productMap[item.productId] || null,
|
||||
})),
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Dashboard stats fetched successfully',
|
||||
data: {
|
||||
// Summary Stats
|
||||
totalUsers,
|
||||
totalOrders,
|
||||
totalProducts,
|
||||
totalRevenue: parseFloat(totalRevenue._sum.totalAmount || 0),
|
||||
|
||||
// ✅ Inventory Stats
|
||||
inventory: inventoryStats,
|
||||
|
||||
// Lists
|
||||
recentOrders: enhancedRecentOrders,
|
||||
topProducts: topSellingProducts, // ✅ Real selling data
|
||||
lowStockProducts, // ✅ Products needing restock
|
||||
|
||||
// Charts
|
||||
charts: {
|
||||
orderOverview,
|
||||
revenueOverview,
|
||||
ordersByStatus,
|
||||
monthlyComparison,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Dashboard stats error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// ✅ GET REAL TOP SELLING PRODUCTS
|
||||
async function getTopSellingProducts() {
|
||||
try {
|
||||
// Get sales data from order items
|
||||
const salesData = await prisma.orderItem.groupBy({
|
||||
by: ['productId'],
|
||||
_sum: { quantity: true },
|
||||
_count: { productId: true },
|
||||
orderBy: { _sum: { quantity: 'desc' } },
|
||||
take: 10,
|
||||
});
|
||||
|
||||
if (salesData.length === 0) {
|
||||
// Fallback: Return recent products
|
||||
const fallback = await Product.find({ status: 'active' })
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(5)
|
||||
.lean();
|
||||
|
||||
return fallback.map(p => ({
|
||||
...p,
|
||||
_id: p._id.toString(),
|
||||
displayImage: getProductImage(p),
|
||||
totalSold: 0,
|
||||
totalOrders: 0,
|
||||
stock: p.stock || 0,
|
||||
stockStatus: getStockStatus(p.stock || 0),
|
||||
}));
|
||||
}
|
||||
|
||||
const productIds = salesData.map(item => item.productId);
|
||||
const products = await Product.find({
|
||||
_id: { $in: productIds },
|
||||
status: 'active',
|
||||
}).lean();
|
||||
|
||||
const statsMap = {};
|
||||
salesData.forEach(item => {
|
||||
statsMap[item.productId] = {
|
||||
totalSold: item._sum.quantity || 0,
|
||||
totalOrders: item._count.productId || 0,
|
||||
};
|
||||
});
|
||||
|
||||
const topProducts = products
|
||||
.map(product => {
|
||||
const stats = statsMap[product._id.toString()] || { totalSold: 0, totalOrders: 0 };
|
||||
const stock = product.stock || 0;
|
||||
|
||||
return {
|
||||
_id: product._id.toString(),
|
||||
name: product.name,
|
||||
slug: product.slug,
|
||||
basePrice: product.basePrice,
|
||||
displayImage: getProductImage(product),
|
||||
totalSold: stats.totalSold,
|
||||
totalOrders: stats.totalOrders,
|
||||
revenue: stats.totalSold * (product.basePrice || 0),
|
||||
stock: stock, // ✅ Current stock
|
||||
stockStatus: getStockStatus(stock), // ✅ Stock status
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.totalSold - a.totalSold)
|
||||
.slice(0, 5);
|
||||
|
||||
return topProducts;
|
||||
} catch (error) {
|
||||
console.error('Error fetching top selling products:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getStockStatus(stock) {
|
||||
if (stock === 0) return 'OUT_OF_STOCK';
|
||||
if (stock <= 5) return 'CRITICAL';
|
||||
if (stock <= 10) return 'LOW';
|
||||
return 'IN_STOCK';
|
||||
}
|
||||
|
||||
function getProductImage(product) {
|
||||
return (
|
||||
(product.images?.gallery?.[0]) ||
|
||||
product.images?.primary ||
|
||||
(product.variants?.[0]?.images?.[0]) ||
|
||||
'https://via.placeholder.com/300'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get daily order count for last 30 days
|
||||
*/
|
||||
// async function getOrderOverview() {
|
||||
// const thirtyDaysAgo = new Date();
|
||||
// thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
// // Group orders by date
|
||||
// const orders = await prisma.order.findMany({
|
||||
// where: {
|
||||
// createdAt: {
|
||||
// gte: thirtyDaysAgo,
|
||||
// },
|
||||
// },
|
||||
// select: {
|
||||
// createdAt: true,
|
||||
// status: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// // Create date map for last 30 days
|
||||
// const dateMap = {};
|
||||
// for (let i = 29; i >= 0; i--) {
|
||||
// const date = new Date();
|
||||
// date.setDate(date.getDate() - i);
|
||||
// const dateKey = date.toISOString().split('T')[0];
|
||||
// dateMap[dateKey] = { total: 0, completed: 0, pending: 0, cancelled: 0 };
|
||||
// }
|
||||
|
||||
// // Count orders by date
|
||||
// orders.forEach(order => {
|
||||
// const dateKey = order.createdAt.toISOString().split('T')[0];
|
||||
// if (dateMap[dateKey]) {
|
||||
// dateMap[dateKey].total++;
|
||||
|
||||
// if (order.status === 'DELIVERED') {
|
||||
// dateMap[dateKey].completed++;
|
||||
// } else if (['PENDING', 'CONFIRMED', 'PROCESSING', 'SHIPPED'].includes(order.status)) {
|
||||
// dateMap[dateKey].pending++;
|
||||
// } else if (order.status === 'CANCELLED') {
|
||||
// dateMap[dateKey].cancelled++;
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
// // Convert to array format for charts
|
||||
// return Object.entries(dateMap).map(([date, counts]) => ({
|
||||
// date,
|
||||
// label: new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
|
||||
// total: counts.total,
|
||||
// completed: counts.completed,
|
||||
// pending: counts.pending,
|
||||
// cancelled: counts.cancelled,
|
||||
// }));
|
||||
// }
|
||||
|
||||
|
||||
|
||||
async function getOrderOverview() {
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const orders = await prisma.order.findMany({
|
||||
where: { createdAt: { gte: thirtyDaysAgo } },
|
||||
select: { createdAt: true, status: true },
|
||||
});
|
||||
|
||||
const dateMap = {};
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateKey = date.toISOString().split('T')[0];
|
||||
dateMap[dateKey] = { total: 0, completed: 0, pending: 0, cancelled: 0 };
|
||||
}
|
||||
|
||||
orders.forEach(order => {
|
||||
const dateKey = order.createdAt.toISOString().split('T')[0];
|
||||
if (dateMap[dateKey]) {
|
||||
dateMap[dateKey].total++;
|
||||
if (order.status === 'DELIVERED') dateMap[dateKey].completed++;
|
||||
else if (['PENDING', 'CONFIRMED', 'PROCESSING', 'SHIPPED'].includes(order.status)) dateMap[dateKey].pending++;
|
||||
else if (order.status === 'CANCELLED') dateMap[dateKey].cancelled++;
|
||||
}
|
||||
});
|
||||
|
||||
return Object.entries(dateMap).map(([date, counts]) => ({
|
||||
date,
|
||||
label: new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
|
||||
...counts,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get daily revenue for last 30 days
|
||||
*/
|
||||
// async function getRevenueOverview() {
|
||||
// const thirtyDaysAgo = new Date();
|
||||
// thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
// const orders = await prisma.order.findMany({
|
||||
// where: {
|
||||
// createdAt: {
|
||||
// gte: thirtyDaysAgo,
|
||||
// },
|
||||
// paymentStatus: 'PAID',
|
||||
// },
|
||||
// select: {
|
||||
// createdAt: true,
|
||||
// totalAmount: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// // Create date map
|
||||
// const dateMap = {};
|
||||
// for (let i = 29; i >= 0; i--) {
|
||||
// const date = new Date();
|
||||
// date.setDate(date.getDate() - i);
|
||||
// const dateKey = date.toISOString().split('T')[0];
|
||||
// dateMap[dateKey] = 0;
|
||||
// }
|
||||
|
||||
// // Sum revenue by date
|
||||
// orders.forEach(order => {
|
||||
// const dateKey = order.createdAt.toISOString().split('T')[0];
|
||||
// if (dateMap[dateKey] !== undefined) {
|
||||
// dateMap[dateKey] += parseFloat(order.totalAmount);
|
||||
// }
|
||||
// });
|
||||
|
||||
// return Object.entries(dateMap).map(([date, revenue]) => ({
|
||||
// date,
|
||||
// label: new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
|
||||
// revenue: Math.round(revenue * 100) / 100, // Round to 2 decimals
|
||||
// }));
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
async function getRevenueOverview() {
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const orders = await prisma.order.findMany({
|
||||
where: { createdAt: { gte: thirtyDaysAgo }, paymentStatus: 'PAID' },
|
||||
select: { createdAt: true, totalAmount: true },
|
||||
});
|
||||
|
||||
const dateMap = {};
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
dateMap[date.toISOString().split('T')[0]] = 0;
|
||||
}
|
||||
|
||||
orders.forEach(order => {
|
||||
const dateKey = order.createdAt.toISOString().split('T')[0];
|
||||
if (dateMap[dateKey] !== undefined) {
|
||||
dateMap[dateKey] += parseFloat(order.totalAmount);
|
||||
}
|
||||
});
|
||||
|
||||
return Object.entries(dateMap).map(([date, revenue]) => ({
|
||||
date,
|
||||
label: new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
|
||||
revenue: Math.round(revenue * 100) / 100,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get order counts by status
|
||||
*/
|
||||
// async function getOrdersByStatus() {
|
||||
// const statusCounts = await prisma.order.groupBy({
|
||||
// by: ['status'],
|
||||
// _count: true,
|
||||
// });
|
||||
|
||||
// const statusLabels = {
|
||||
// PENDING: 'Pending',
|
||||
// CONFIRMED: 'Confirmed',
|
||||
// PROCESSING: 'Processing',
|
||||
// SHIPPED: 'Shipped',
|
||||
// DELIVERED: 'Delivered',
|
||||
// CANCELLED: 'Cancelled',
|
||||
// RETURN_REQUESTED: 'Return Requested',
|
||||
// };
|
||||
|
||||
// const statusColors = {
|
||||
// PENDING: '#FCD34D',
|
||||
// CONFIRMED: '#60A5FA',
|
||||
// PROCESSING: '#A78BFA',
|
||||
// SHIPPED: '#C084FC',
|
||||
// DELIVERED: '#34D399',
|
||||
// CANCELLED: '#F87171',
|
||||
// RETURN_REQUESTED: '#FB923C',
|
||||
// };
|
||||
|
||||
// return statusCounts.map(item => ({
|
||||
// status: item.status,
|
||||
// label: statusLabels[item.status] || item.status,
|
||||
// count: item._count,
|
||||
// color: statusColors[item.status] || '#9CA3AF',
|
||||
// }));
|
||||
// }
|
||||
|
||||
|
||||
|
||||
async function getOrdersByStatus() {
|
||||
const statusCounts = await prisma.order.groupBy({
|
||||
by: ['status'],
|
||||
_count: true,
|
||||
});
|
||||
|
||||
const labels = {
|
||||
PENDING: 'Pending',
|
||||
CONFIRMED: 'Confirmed',
|
||||
PROCESSING: 'Processing',
|
||||
SHIPPED: 'Shipped',
|
||||
DELIVERED: 'Delivered',
|
||||
CANCELLED: 'Cancelled',
|
||||
RETURN_REQUESTED: 'Return Requested',
|
||||
};
|
||||
|
||||
const colors = {
|
||||
PENDING: '#FCD34D',
|
||||
CONFIRMED: '#60A5FA',
|
||||
PROCESSING: '#A78BFA',
|
||||
SHIPPED: '#C084FC',
|
||||
DELIVERED: '#34D399',
|
||||
CANCELLED: '#F87171',
|
||||
RETURN_REQUESTED: '#FB923C',
|
||||
};
|
||||
|
||||
return statusCounts.map(item => ({
|
||||
status: item.status,
|
||||
label: labels[item.status] || item.status,
|
||||
count: item._count,
|
||||
color: colors[item.status] || '#9CA3AF',
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compare current month vs previous month
|
||||
*/
|
||||
// async function getMonthlyComparison() {
|
||||
// const now = new Date();
|
||||
|
||||
// // Current month dates
|
||||
// const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
// const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
|
||||
// // Previous month dates
|
||||
// const previousMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
// const previousMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0);
|
||||
|
||||
// const [currentMonth, previousMonth] = await Promise.all([
|
||||
// prisma.order.aggregate({
|
||||
// where: {
|
||||
// createdAt: {
|
||||
// gte: currentMonthStart,
|
||||
// lte: currentMonthEnd,
|
||||
// },
|
||||
// },
|
||||
// _count: true,
|
||||
// _sum: {
|
||||
// totalAmount: true,
|
||||
// },
|
||||
// }),
|
||||
// prisma.order.aggregate({
|
||||
// where: {
|
||||
// createdAt: {
|
||||
// gte: previousMonthStart,
|
||||
// lte: previousMonthEnd,
|
||||
// },
|
||||
// },
|
||||
// _count: true,
|
||||
// _sum: {
|
||||
// totalAmount: true,
|
||||
// },
|
||||
// }),
|
||||
// ]);
|
||||
|
||||
// const currentRevenue = parseFloat(currentMonth._sum.totalAmount || 0);
|
||||
// const previousRevenue = parseFloat(previousMonth._sum.totalAmount || 0);
|
||||
|
||||
// const orderGrowth = previousMonth._count > 0
|
||||
// ? ((currentMonth._count - previousMonth._count) / previousMonth._count) * 100
|
||||
// : 100;
|
||||
|
||||
// const revenueGrowth = previousRevenue > 0
|
||||
// ? ((currentRevenue - previousRevenue) / previousRevenue) * 100
|
||||
// : 100;
|
||||
|
||||
// return {
|
||||
// currentMonth: {
|
||||
// orders: currentMonth._count,
|
||||
// revenue: Math.round(currentRevenue * 100) / 100,
|
||||
// label: currentMonthStart.toLocaleDateString('en-IN', { month: 'long' }),
|
||||
// },
|
||||
// previousMonth: {
|
||||
// orders: previousMonth._count,
|
||||
// revenue: Math.round(previousRevenue * 100) / 100,
|
||||
// label: previousMonthStart.toLocaleDateString('en-IN', { month: 'long' }),
|
||||
// },
|
||||
// growth: {
|
||||
// orders: Math.round(orderGrowth * 10) / 10,
|
||||
// revenue: Math.round(revenueGrowth * 10) / 10,
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
async function getMonthlyComparison() {
|
||||
const now = new Date();
|
||||
const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
const previousMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const previousMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0);
|
||||
|
||||
const [currentMonth, previousMonth] = await Promise.all([
|
||||
prisma.order.aggregate({
|
||||
where: { createdAt: { gte: currentMonthStart, lte: currentMonthEnd } },
|
||||
_count: true,
|
||||
_sum: { totalAmount: true },
|
||||
}),
|
||||
prisma.order.aggregate({
|
||||
where: { createdAt: { gte: previousMonthStart, lte: previousMonthEnd } },
|
||||
_count: true,
|
||||
_sum: { totalAmount: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const currentRevenue = parseFloat(currentMonth._sum.totalAmount || 0);
|
||||
const previousRevenue = parseFloat(previousMonth._sum.totalAmount || 0);
|
||||
|
||||
return {
|
||||
currentMonth: {
|
||||
orders: currentMonth._count,
|
||||
revenue: Math.round(currentRevenue * 100) / 100,
|
||||
label: currentMonthStart.toLocaleDateString('en-IN', { month: 'long' }),
|
||||
},
|
||||
previousMonth: {
|
||||
orders: previousMonth._count,
|
||||
revenue: Math.round(previousRevenue * 100) / 100,
|
||||
label: previousMonthStart.toLocaleDateString('en-IN', { month: 'long' }),
|
||||
},
|
||||
growth: {
|
||||
orders: previousMonth._count > 0 ? Math.round(((currentMonth._count - previousMonth._count) / previousMonth._count) * 1000) / 10 : 100,
|
||||
revenue: previousRevenue > 0 ? Math.round(((currentRevenue - previousRevenue) / previousRevenue) * 1000) / 10 : 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
395
src/controllers/admin/orderController.js
Normal file
395
src/controllers/admin/orderController.js
Normal file
@@ -0,0 +1,395 @@
|
||||
const { prisma } = require('../../config/database');
|
||||
|
||||
// exports.getAllOrders = async (req, res, next) => {
|
||||
// try {
|
||||
// const { page = 1, limit = 20, status, paymentStatus, userId } = req.query;
|
||||
// const skip = (page - 1) * limit;
|
||||
|
||||
// const where = {};
|
||||
// if (status) where.status = status;
|
||||
// if (paymentStatus) where.paymentStatus = paymentStatus;
|
||||
// if (userId) where.userId = userId;
|
||||
|
||||
// const [orders, total] = await Promise.all([
|
||||
// prisma.order.findMany({
|
||||
// where,
|
||||
// include: {
|
||||
// items: true,
|
||||
// address: true,
|
||||
// user: {
|
||||
// select: { id: true, email: true, firstName: true, lastName: true },
|
||||
// },
|
||||
// },
|
||||
// orderBy: { createdAt: 'desc' },
|
||||
// skip: +skip,
|
||||
// take: +limit,
|
||||
// }),
|
||||
// prisma.order.count({ where }),
|
||||
// ]);
|
||||
|
||||
// // res.json({
|
||||
// // success: true,
|
||||
// // data: { orders, pagination: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) } },
|
||||
// // });
|
||||
// res.status(200).json({
|
||||
// statusCode: 200,
|
||||
// status: true,
|
||||
// message: 'Orders fetched successfully',
|
||||
// data: {
|
||||
// orders,
|
||||
// pagination: {
|
||||
// page: +page,
|
||||
// limit: +limit,
|
||||
// total,
|
||||
// pages: Math.ceil(total / limit),
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// exports.getOrderDetails = async (req, res, next) => {
|
||||
// try {
|
||||
// const { id } = req.params;
|
||||
|
||||
// const order = await prisma.order.findUnique({
|
||||
// where: { id },
|
||||
// include: {
|
||||
// items: {
|
||||
// include: {
|
||||
// product: true, // if you want product details
|
||||
// }
|
||||
// },
|
||||
// address: true,
|
||||
// user: {
|
||||
// select: {
|
||||
// id: true,
|
||||
// firstName: true,
|
||||
// lastName: true,
|
||||
// email: true,
|
||||
// phone: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (!order) {
|
||||
// return res.status(404).json({
|
||||
// success: false,
|
||||
// message: "Order not found",
|
||||
// });
|
||||
// }
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// data: order,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
|
||||
|
||||
exports.getAllOrders = async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
status,
|
||||
search,
|
||||
sortBy = 'createdAt',
|
||||
sortOrder = 'desc',
|
||||
} = req.query;
|
||||
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const where = {};
|
||||
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ orderNumber: { contains: search, mode: 'insensitive' } },
|
||||
{ trackingNumber: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
const [orders, total] = await Promise.all([
|
||||
prisma.order.findMany({
|
||||
where,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
address: true,
|
||||
items: true,
|
||||
// ✅ Include latest status change
|
||||
statusHistory: {
|
||||
take: 1,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
admin: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
[sortBy]: sortOrder,
|
||||
},
|
||||
skip,
|
||||
take: parseInt(limit),
|
||||
}),
|
||||
prisma.order.count({ where }),
|
||||
]);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
orders,
|
||||
pagination: {
|
||||
total,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
pages: Math.ceil(total / parseInt(limit)),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Get all orders error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// **
|
||||
// * @desc Get single order with full history
|
||||
// * @route GET /api/admin/orders/:orderId
|
||||
// * @access Private/Admin
|
||||
// */
|
||||
exports.getOrderById = async (req, res, next) => {
|
||||
try {
|
||||
const { orderId } = req.params;
|
||||
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
address: true,
|
||||
items: true,
|
||||
// ✅ Include full status history
|
||||
statusHistory: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
admin: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Order not found',
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: order,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Get order error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @desc Get order status history
|
||||
* @route GET /api/admin/orders/:orderId/history
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.getOrderStatusHistory = async (req, res, next) => {
|
||||
try {
|
||||
const { orderId } = req.params;
|
||||
|
||||
const history = await prisma.orderStatusHistory.findMany({
|
||||
where: { orderId },
|
||||
include: {
|
||||
admin: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: history,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Get status history error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @desc Get status change statistics
|
||||
* @route GET /api/admin/orders/stats/status-changes
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.getStatusChangeStats = async (req, res, next) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
const where = {};
|
||||
|
||||
if (startDate || endDate) {
|
||||
where.createdAt = {};
|
||||
if (startDate) where.createdAt.gte = new Date(startDate);
|
||||
if (endDate) where.createdAt.lte = new Date(endDate);
|
||||
}
|
||||
|
||||
// Count status changes by status
|
||||
const statusChanges = await prisma.orderStatusHistory.groupBy({
|
||||
by: ['toStatus'],
|
||||
where,
|
||||
_count: true,
|
||||
});
|
||||
|
||||
// Count status changes by admin
|
||||
const changesByAdmin = await prisma.orderStatusHistory.groupBy({
|
||||
by: ['changedBy'],
|
||||
where,
|
||||
_count: true,
|
||||
orderBy: {
|
||||
_count: {
|
||||
changedBy: 'desc',
|
||||
},
|
||||
},
|
||||
take: 10,
|
||||
});
|
||||
|
||||
// Get admin details
|
||||
const adminIds = changesByAdmin
|
||||
.map(item => item.changedBy)
|
||||
.filter(Boolean);
|
||||
|
||||
const admins = await prisma.user.findMany({
|
||||
where: { id: { in: adminIds } },
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
const adminMap = Object.fromEntries(
|
||||
admins.map(admin => [admin.id, admin])
|
||||
);
|
||||
|
||||
const changesByAdminWithDetails = changesByAdmin.map(item => ({
|
||||
admin: item.changedBy ? adminMap[item.changedBy] : null,
|
||||
count: item._count,
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
statusChanges,
|
||||
changesByAdmin: changesByAdminWithDetails,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Get status stats error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
exports.getOrderDetails = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
items: true, // OrderItem snapshot data
|
||||
address: true, // shipping address
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
// success: false,
|
||||
// message: "Order not found",
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Order not found',
|
||||
});
|
||||
}
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// data: order,
|
||||
// });
|
||||
res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Order details fetched successfully',
|
||||
data: order,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
692
src/controllers/admin/productController.js
Normal file
692
src/controllers/admin/productController.js
Normal file
@@ -0,0 +1,692 @@
|
||||
const Product = require('../../models/mongodb/Product');
|
||||
const uploadToS3 = require("../../utils/uploadToS3");
|
||||
|
||||
|
||||
exports.getAllProducts = async (req, res, next) => {
|
||||
try {
|
||||
const { page = 1, limit = 20, status, category, search } = req.query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const query = {};
|
||||
if (status) query.status = status;
|
||||
if (category) query.category = category;
|
||||
if (search) query.$text = { $search: search };
|
||||
|
||||
const [products, total] = await Promise.all([
|
||||
Product.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(+limit)
|
||||
.lean(),
|
||||
Product.countDocuments(query),
|
||||
]);
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// data: { products, pagination: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) } },
|
||||
// });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Products fetched successfully',
|
||||
data: {
|
||||
products,
|
||||
pagination: {
|
||||
page: +page,
|
||||
limit: +limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// exports.createProduct = async (req, res, next) => {
|
||||
// try {
|
||||
// const product = new Product(req.body);
|
||||
// await product.save();
|
||||
// res.status(201).json({ success: true, message: 'Product created', data: product });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// exports.createProduct = async (req, res, next) => {
|
||||
// try {
|
||||
// let { name, slug, ...rest } = req.body;
|
||||
|
||||
// // Generate slug from name if not provided
|
||||
// if (!slug) {
|
||||
// slug = name
|
||||
// .toLowerCase()
|
||||
// .replace(/[^a-z0-9]+/g, '-') // replace spaces & special chars
|
||||
// .replace(/(^-|-$)/g, ''); // remove leading/trailing hyphens
|
||||
// }
|
||||
|
||||
// // Ensure slug is unique
|
||||
// let slugExists = await Product.findOne({ slug });
|
||||
// let counter = 1;
|
||||
// const baseSlug = slug;
|
||||
// while (slugExists) {
|
||||
// slug = `${baseSlug}-${counter}`;
|
||||
// slugExists = await Product.findOne({ slug });
|
||||
// counter++;
|
||||
// }
|
||||
|
||||
// // ✅ Upload images manually
|
||||
// const primaryImage = req.files?.primaryImage
|
||||
// ? await uploadToS3(req.files.primaryImage[0])
|
||||
// : null;
|
||||
|
||||
// const galleryImages = req.files?.galleryImages
|
||||
// ? await Promise.all(
|
||||
// req.files.galleryImages.map((file) => uploadToS3(file))
|
||||
// )
|
||||
// : [];
|
||||
|
||||
// // const product = new Product({ name, slug, ...rest });
|
||||
// const product = new Product({
|
||||
// name,
|
||||
// slug,
|
||||
// ...rest,
|
||||
// images: {
|
||||
// primary: primaryImage,
|
||||
// gallery: galleryImages,
|
||||
// },
|
||||
// });
|
||||
// await product.save();
|
||||
|
||||
// // res.status(201).json({ success: true, message: 'Product created', data: product });
|
||||
// return res.status(201).json({
|
||||
// // statusCode: 201,
|
||||
// status: true,
|
||||
// message: 'Product created successfully',
|
||||
// data: product,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
|
||||
// exports.createProduct = async (req, res, next) => {
|
||||
// try {
|
||||
// let { name, hasVariants, variants, ...rest } = req.body;
|
||||
// let slug;
|
||||
// // slug generation
|
||||
// if (!slug) {
|
||||
// slug = name
|
||||
// .toLowerCase()
|
||||
// .replace(/[^a-z0-9]+/g, "-")
|
||||
// .replace(/(^-|-$)/g, "");
|
||||
// }
|
||||
|
||||
// let exists = await Product.findOne({ slug });
|
||||
// let i = 1;
|
||||
// while (exists) {
|
||||
// slug = `${slug}-${i++}`;
|
||||
// exists = await Product.findOne({ slug });
|
||||
// }
|
||||
|
||||
// // Upload main images
|
||||
// const primaryImage = req.files?.primaryImage
|
||||
// ? await uploadToS3(req.files.primaryImage[0])
|
||||
// : null;
|
||||
|
||||
// const galleryImages = req.files?.galleryImages
|
||||
// ? await Promise.all(
|
||||
// req.files.galleryImages.map((f) => uploadToS3(f))
|
||||
// )
|
||||
// : [];
|
||||
|
||||
// const productData = {
|
||||
// name,
|
||||
// slug,
|
||||
// ...rest,
|
||||
// hasVariants: hasVariants === "true",
|
||||
// };
|
||||
|
||||
// // 🔥 VARIANT LOGIC
|
||||
// if (hasVariants === "true") {
|
||||
// const parsedVariants = JSON.parse(variants); // IMPORTANT
|
||||
|
||||
// productData.variants = await Promise.all(
|
||||
// parsedVariants.map(async (variant) => {
|
||||
// const variantImages =
|
||||
// req.files?.[`variantImages_${variant.color}`]
|
||||
// ? await Promise.all(
|
||||
// req.files[`variantImages_${variant.color}`].map(uploadToS3)
|
||||
// )
|
||||
// : [];
|
||||
|
||||
// return {
|
||||
// size: variant.size,
|
||||
// color: variant.color,
|
||||
// sku: variant.sku,
|
||||
// price: Number(variant.price),
|
||||
// compareAtPrice: Number(variant.compareAtPrice),
|
||||
// inventory: {
|
||||
// quantity: Number(variant.quantity),
|
||||
// trackInventory: true,
|
||||
// },
|
||||
// images: variantImages,
|
||||
// };
|
||||
// })
|
||||
// );
|
||||
// } else {
|
||||
// // simple product images
|
||||
// productData.images = {
|
||||
// primary: primaryImage,
|
||||
// gallery: galleryImages,
|
||||
// };
|
||||
// }
|
||||
|
||||
// const product = await Product.create(productData);
|
||||
|
||||
// res.status(201).json({
|
||||
// status: true,
|
||||
// message: "Product created successfully",
|
||||
// data: product,
|
||||
// });
|
||||
// } catch (err) {
|
||||
// next(err);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
// exports.createProduct = async (req, res, next) => {
|
||||
// try {
|
||||
// const { name, hasVariants, variants, ...rest } = req.body;
|
||||
|
||||
// // ✅ Validate name
|
||||
// if (!name || name.trim() === "") {
|
||||
// return res.status(400).json({
|
||||
// status: false,
|
||||
// message: "Product name is required",
|
||||
// });
|
||||
// }
|
||||
|
||||
// // slug generation
|
||||
// let slug = name
|
||||
// .toLowerCase()
|
||||
// .replace(/[^a-z0-9]+/g, "-")
|
||||
// .replace(/(^-|-$)/g, "");
|
||||
|
||||
// // ensure unique slug
|
||||
// let exists = await Product.findOne({ slug });
|
||||
// let i = 1;
|
||||
// while (exists) {
|
||||
// slug = `${slug}-${i++}`;
|
||||
// exists = await Product.findOne({ slug });
|
||||
// }
|
||||
|
||||
// // Upload main images
|
||||
// const primaryImage = req.files?.primaryImage
|
||||
// ? await uploadToS3(req.files.primaryImage[0])
|
||||
// : null;
|
||||
|
||||
// const galleryImages = req.files?.galleryImages
|
||||
// ? await Promise.all(req.files.galleryImages.map(uploadToS3))
|
||||
// : [];
|
||||
|
||||
// const productData = {
|
||||
// name,
|
||||
// slug,
|
||||
// ...rest,
|
||||
// hasVariants: hasVariants === "true",
|
||||
// };
|
||||
|
||||
// // 🔥 VARIANT LOGIC
|
||||
// if (hasVariants === "true") {
|
||||
// const parsedVariants = JSON.parse(variants); // IMPORTANT
|
||||
|
||||
// productData.variants = await Promise.all(
|
||||
// parsedVariants.map(async (variant) => {
|
||||
// const variantImages =
|
||||
// req.files?.[`variantImages_${variant.color}`]
|
||||
// ? await Promise.all(
|
||||
// req.files[`variantImages_${variant.color}`].map(uploadToS3)
|
||||
// )
|
||||
// : [];
|
||||
|
||||
// return {
|
||||
// size: variant.size,
|
||||
// color: variant.color,
|
||||
// sku: variant.sku,
|
||||
// price: Number(variant.price),
|
||||
// compareAtPrice: Number(variant.compareAtPrice),
|
||||
// inventory: {
|
||||
// quantity: Number(variant.quantity),
|
||||
// trackInventory: true,
|
||||
// },
|
||||
// images: variantImages,
|
||||
// };
|
||||
// })
|
||||
// );
|
||||
// } else {
|
||||
// // simple product images
|
||||
// productData.images = {
|
||||
// primary: primaryImage,
|
||||
// gallery: galleryImages,
|
||||
// };
|
||||
// }
|
||||
|
||||
// const product = await Product.create(productData);
|
||||
|
||||
// res.status(201).json({
|
||||
// status: true,
|
||||
// message: "Product created successfully",
|
||||
// data: product,
|
||||
// });
|
||||
// } catch (err) {
|
||||
// next(err);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
|
||||
// exports.createProduct = async (req, res, next) => {
|
||||
// try {
|
||||
// const { name, hasVariants, variants, ...rest } = req.body;
|
||||
|
||||
// console.log('📥 Request body:', { name, hasVariants, variantsCount: variants ? 'YES' : 'NO' });
|
||||
// console.log('📥 Files received:', req.files ? req.files.length : 0);
|
||||
|
||||
// // ✅ Validate name
|
||||
// if (!name || name.trim() === "") {
|
||||
// return res.status(400).json({
|
||||
// status: false,
|
||||
// message: "Product name is required",
|
||||
// });
|
||||
// }
|
||||
|
||||
// // Generate unique slug
|
||||
// let slug = name
|
||||
// .toLowerCase()
|
||||
// .replace(/[^a-z0-9]+/g, "-")
|
||||
// .replace(/(^-|-$)/g, "");
|
||||
|
||||
// let exists = await Product.findOne({ slug });
|
||||
// let i = 1;
|
||||
// while (exists) {
|
||||
// slug = `${slug}-${i++}`;
|
||||
// exists = await Product.findOne({ slug });
|
||||
// }
|
||||
|
||||
// const productData = {
|
||||
// name,
|
||||
// slug,
|
||||
// ...rest,
|
||||
// hasVariants: hasVariants === "true" || hasVariants === true,
|
||||
// };
|
||||
|
||||
// // ======================
|
||||
// // VARIANT MODE
|
||||
// // ======================
|
||||
// if (productData.hasVariants) {
|
||||
// console.log('🎨 Processing variant product...');
|
||||
|
||||
// if (!variants) {
|
||||
// return res.status(400).json({
|
||||
// status: false,
|
||||
// message: "Variants data is required when hasVariants is true",
|
||||
// });
|
||||
// }
|
||||
|
||||
// const parsedVariants = JSON.parse(variants);
|
||||
// console.log('📦 Parsed variants:', parsedVariants.length);
|
||||
|
||||
// // ✅ Convert req.files array to object grouped by fieldname
|
||||
// const filesGrouped = {};
|
||||
// if (req.files && req.files.length > 0) {
|
||||
// req.files.forEach(file => {
|
||||
// if (!filesGrouped[file.fieldname]) {
|
||||
// filesGrouped[file.fieldname] = [];
|
||||
// }
|
||||
// filesGrouped[file.fieldname].push(file);
|
||||
// });
|
||||
// }
|
||||
|
||||
// console.log('📸 Files grouped:', Object.keys(filesGrouped));
|
||||
|
||||
// // Process each variant
|
||||
// productData.variants = await Promise.all(
|
||||
// parsedVariants.map(async (variant) => {
|
||||
// const color = variant.color;
|
||||
// const fieldName = `variantImages_${color}`;
|
||||
|
||||
// console.log(`🔍 Looking for images with fieldname: ${fieldName}`);
|
||||
|
||||
// // Get images for this variant
|
||||
// const variantFiles = filesGrouped[fieldName] || [];
|
||||
// console.log(`📸 Found ${variantFiles.length} images for ${color}`);
|
||||
|
||||
// // Upload images to S3
|
||||
// const variantImages = variantFiles.length > 0
|
||||
// ? await Promise.all(variantFiles.map(uploadToS3))
|
||||
// : [];
|
||||
|
||||
// console.log(`✅ Uploaded ${variantImages.length} images for ${color}`);
|
||||
|
||||
// return {
|
||||
// size: variant.size || 'default',
|
||||
// color: variant.color,
|
||||
// sku: variant.sku,
|
||||
// price: Number(variant.price),
|
||||
// compareAtPrice: variant.compareAtPrice ? Number(variant.compareAtPrice) : null,
|
||||
// inventory: {
|
||||
// quantity: Number(variant.quantity || variant.stock || 0),
|
||||
// trackInventory: true,
|
||||
// },
|
||||
// images: variantImages,
|
||||
// isActive: true,
|
||||
// };
|
||||
// })
|
||||
// );
|
||||
|
||||
// console.log('✅ All variants processed:', productData.variants.length);
|
||||
// }
|
||||
// // ======================
|
||||
// // SIMPLE PRODUCT MODE
|
||||
// // ======================
|
||||
// else {
|
||||
// console.log('📦 Processing simple product...');
|
||||
|
||||
// // ✅ Handle files from req.files array
|
||||
// let primaryImage = null;
|
||||
// let galleryImages = [];
|
||||
|
||||
// if (req.files && req.files.length > 0) {
|
||||
// // Group files by fieldname
|
||||
// const filesGrouped = {};
|
||||
// req.files.forEach(file => {
|
||||
// if (!filesGrouped[file.fieldname]) {
|
||||
// filesGrouped[file.fieldname] = [];
|
||||
// }
|
||||
// filesGrouped[file.fieldname].push(file);
|
||||
// });
|
||||
|
||||
// // Upload primary image
|
||||
// if (filesGrouped['primaryImage'] && filesGrouped['primaryImage'][0]) {
|
||||
// primaryImage = await uploadToS3(filesGrouped['primaryImage'][0]);
|
||||
// }
|
||||
|
||||
// // Upload gallery images
|
||||
// if (filesGrouped['galleryImages']) {
|
||||
// galleryImages = await Promise.all(
|
||||
// filesGrouped['galleryImages'].map(uploadToS3)
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// productData.images = {
|
||||
// primary: primaryImage,
|
||||
// gallery: galleryImages,
|
||||
// videos: [],
|
||||
// };
|
||||
|
||||
// console.log('✅ Images uploaded:', {
|
||||
// primary: !!primaryImage,
|
||||
// gallery: galleryImages.length,
|
||||
// });
|
||||
// }
|
||||
|
||||
// // Create product in MongoDB
|
||||
// const product = await Product.create(productData);
|
||||
|
||||
// console.log('✅ Product created:', product._id);
|
||||
|
||||
// res.status(201).json({
|
||||
// status: true,
|
||||
// message: "Product created successfully",
|
||||
// data: product,
|
||||
// });
|
||||
// } catch (err) {
|
||||
// console.error('❌ Error creating product:', err);
|
||||
|
||||
// // Send detailed error for debugging
|
||||
// if (process.env.NODE_ENV === 'development') {
|
||||
// return res.status(400).json({
|
||||
// status: false,
|
||||
// message: "Failed to create product",
|
||||
// error: err.message,
|
||||
// stack: err.stack,
|
||||
// });
|
||||
// }
|
||||
|
||||
// next(err);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
exports.createProduct = async (req, res, next) => {
|
||||
try {
|
||||
const { name, hasVariants, variants, ...rest } = req.body;
|
||||
|
||||
console.log('📥 Request body:', { name, hasVariants, variantsCount: variants ? 'YES' : 'NO' });
|
||||
console.log('📥 Files received:', req.files ? req.files.length : 0);
|
||||
|
||||
// ✅ Validate name
|
||||
if (!name || name.trim() === "") {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: "Product name is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Generate unique slug
|
||||
let slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)/g, "");
|
||||
|
||||
let exists = await Product.findOne({ slug });
|
||||
let i = 1;
|
||||
while (exists) {
|
||||
slug = `${slug}-${i++}`;
|
||||
exists = await Product.findOne({ slug });
|
||||
}
|
||||
|
||||
// ✅ Build productData carefully to avoid empty slug
|
||||
const productData = {
|
||||
name,
|
||||
slug, // Generated slug
|
||||
hasVariants: hasVariants === "true" || hasVariants === true,
|
||||
};
|
||||
|
||||
// ✅ Add other fields from rest, but skip 'slug' if it exists
|
||||
Object.keys(rest).forEach(key => {
|
||||
if (key !== 'slug') {
|
||||
productData[key] = rest[key];
|
||||
}
|
||||
});
|
||||
|
||||
// ======================
|
||||
// VARIANT MODE
|
||||
// ======================
|
||||
if (productData.hasVariants) {
|
||||
console.log('🎨 Processing variant product...');
|
||||
|
||||
if (!variants) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: "Variants data is required when hasVariants is true",
|
||||
});
|
||||
}
|
||||
|
||||
const parsedVariants = JSON.parse(variants);
|
||||
console.log('📦 Parsed variants:', parsedVariants.length);
|
||||
|
||||
// ✅ Convert req.files array to object grouped by fieldname
|
||||
const filesGrouped = {};
|
||||
if (req.files && req.files.length > 0) {
|
||||
req.files.forEach(file => {
|
||||
if (!filesGrouped[file.fieldname]) {
|
||||
filesGrouped[file.fieldname] = [];
|
||||
}
|
||||
filesGrouped[file.fieldname].push(file);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('📸 Files grouped:', Object.keys(filesGrouped));
|
||||
|
||||
// Process each variant
|
||||
productData.variants = await Promise.all(
|
||||
parsedVariants.map(async (variant) => {
|
||||
const color = variant.color;
|
||||
const fieldName = `variantImages_${color}`;
|
||||
|
||||
console.log(`🔍 Looking for images with fieldname: ${fieldName}`);
|
||||
|
||||
// Get images for this variant
|
||||
const variantFiles = filesGrouped[fieldName] || [];
|
||||
console.log(`📸 Found ${variantFiles.length} images for ${color}`);
|
||||
|
||||
// Upload images to S3
|
||||
const variantImages = variantFiles.length > 0
|
||||
? await Promise.all(variantFiles.map(uploadToS3))
|
||||
: [];
|
||||
|
||||
console.log(`✅ Uploaded ${variantImages.length} images for ${color}`);
|
||||
|
||||
return {
|
||||
size: variant.size || 'default',
|
||||
color: variant.color,
|
||||
sku: variant.sku,
|
||||
price: Number(variant.price),
|
||||
compareAtPrice: variant.compareAtPrice ? Number(variant.compareAtPrice) : null,
|
||||
inventory: {
|
||||
quantity: Number(variant.quantity || variant.stock || 0),
|
||||
trackInventory: true,
|
||||
},
|
||||
images: variantImages,
|
||||
isActive: true,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
console.log('✅ All variants processed:', productData.variants.length);
|
||||
}
|
||||
// ======================
|
||||
// SIMPLE PRODUCT MODE
|
||||
// ======================
|
||||
else {
|
||||
console.log('📦 Processing simple product...');
|
||||
|
||||
// ✅ Handle files from req.files array
|
||||
let primaryImage = null;
|
||||
let galleryImages = [];
|
||||
|
||||
if (req.files && req.files.length > 0) {
|
||||
// Group files by fieldname
|
||||
const filesGrouped = {};
|
||||
req.files.forEach(file => {
|
||||
if (!filesGrouped[file.fieldname]) {
|
||||
filesGrouped[file.fieldname] = [];
|
||||
}
|
||||
filesGrouped[file.fieldname].push(file);
|
||||
});
|
||||
|
||||
// Upload primary image
|
||||
if (filesGrouped['primaryImage'] && filesGrouped['primaryImage'][0]) {
|
||||
primaryImage = await uploadToS3(filesGrouped['primaryImage'][0]);
|
||||
}
|
||||
|
||||
// Upload gallery images
|
||||
if (filesGrouped['galleryImages']) {
|
||||
galleryImages = await Promise.all(
|
||||
filesGrouped['galleryImages'].map(uploadToS3)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
productData.images = {
|
||||
primary: primaryImage,
|
||||
gallery: galleryImages,
|
||||
videos: [],
|
||||
};
|
||||
|
||||
console.log('✅ Images uploaded:', {
|
||||
primary: !!primaryImage,
|
||||
gallery: galleryImages.length,
|
||||
});
|
||||
}
|
||||
|
||||
// Create product in MongoDB
|
||||
const product = await Product.create(productData);
|
||||
|
||||
console.log('✅ Product created:', product._id);
|
||||
|
||||
res.status(201).json({
|
||||
status: true,
|
||||
message: "Product created successfully",
|
||||
data: product,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('❌ Error creating product:', err);
|
||||
|
||||
// Send detailed error for debugging
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: "Failed to create product",
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
exports.updateProduct = async (req, res, next) => {
|
||||
try {
|
||||
const product = await Product.findByIdAndUpdate(req.params.id, req.body, {
|
||||
new: true,
|
||||
});
|
||||
// if (!product) return res.status(404).json({ success: false, message: 'Product not found' });
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Product not found',
|
||||
});
|
||||
}
|
||||
// res.json({ success: true, message: 'Product updated', data: product });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Product updated successfully',
|
||||
data: product,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.deleteProduct = async (req, res, next) => {
|
||||
try {
|
||||
const product = await Product.findByIdAndDelete(req.params.id);
|
||||
// if (!product) return res.status(404).json({ success: false, message: 'Product not found' });
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Product not found',
|
||||
});
|
||||
}
|
||||
// res.json({ success: true, message: 'Product deleted' });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Product deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
411
src/controllers/admin/reportController.js
Normal file
411
src/controllers/admin/reportController.js
Normal file
@@ -0,0 +1,411 @@
|
||||
const { prisma } = require('../../config/database');
|
||||
const Product = require('../../models/mongodb/Product');
|
||||
// const ReturnModel = require("../../models/mongodb/Return");
|
||||
const Order = prisma.order;
|
||||
|
||||
module.exports = {
|
||||
// ===============================
|
||||
// 📌 1. OVERVIEW KPI
|
||||
// ===============================
|
||||
getOverviewReport: async (req, res, next) => {
|
||||
try {
|
||||
const [
|
||||
totalUsers,
|
||||
totalOrders,
|
||||
totalProducts,
|
||||
totalRevenue,
|
||||
totalCustomers,
|
||||
totalSellers,
|
||||
] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.order.count(),
|
||||
Product.countDocuments(),
|
||||
prisma.order.aggregate({
|
||||
_sum: { totalAmount: true },
|
||||
where: { paymentStatus: 'PAID' },
|
||||
}),
|
||||
prisma.user.count({ where: { role: 'CUSTOMER' } }),
|
||||
prisma.user.count({ where: { role: 'SELLER' } }),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalUsers,
|
||||
totalCustomers,
|
||||
totalSellers,
|
||||
totalOrders,
|
||||
totalProducts,
|
||||
totalRevenue: totalRevenue._sum.totalAmount || 0,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ===============================
|
||||
// 📌 2. SALES ANALYTICS (Daily + Monthly)
|
||||
// ===============================
|
||||
getSalesAnalytics: async (req, res, next) => {
|
||||
try {
|
||||
const today = new Date();
|
||||
const last30Days = new Date();
|
||||
last30Days.setDate(today.getDate() - 30);
|
||||
|
||||
// 1️⃣ Daily sales (last 30 days)
|
||||
const dailyOrders = await prisma.order.findMany({
|
||||
where: {
|
||||
paymentStatus: 'PAID',
|
||||
createdAt: { gte: last30Days },
|
||||
},
|
||||
select: {
|
||||
totalAmount: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
const dailySales = dailyOrders.reduce((acc, order) => {
|
||||
const date = order.createdAt.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
acc[date] = (acc[date] || 0) + Number(order.totalAmount);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 2️⃣ Monthly sales (all time)
|
||||
const allPaidOrders = await prisma.order.findMany({
|
||||
where: { paymentStatus: 'PAID' },
|
||||
select: { totalAmount: true, createdAt: true },
|
||||
});
|
||||
|
||||
const monthlySales = allPaidOrders.reduce((acc, order) => {
|
||||
const month = order.createdAt.getMonth() + 1; // 1-12
|
||||
const year = order.createdAt.getFullYear();
|
||||
const key = `${year}-${month.toString().padStart(2, '0')}`; // YYYY-MM
|
||||
acc[key] = (acc[key] || 0) + Number(order.totalAmount);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
dailySales,
|
||||
monthlySales,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ===============================
|
||||
// 📌 3. CUSTOMER STATISTICS
|
||||
// ===============================
|
||||
// ===============================
|
||||
// 📌 3. CUSTOMER STATISTICS (UPDATED)
|
||||
// ===============================
|
||||
getCustomerStats: async (req, res, next) => {
|
||||
try {
|
||||
const now = new Date();
|
||||
|
||||
// ---------- WEEKLY DATA (Mon–Sun) ----------
|
||||
const startOfWeek = new Date(now);
|
||||
startOfWeek.setDate(now.getDate() - now.getDay() + 1); // Monday
|
||||
|
||||
const weeklyData = await Promise.all(
|
||||
Array.from({ length: 7 }).map(async (_, i) => {
|
||||
const dayStart = new Date(startOfWeek);
|
||||
dayStart.setDate(startOfWeek.getDate() + i);
|
||||
|
||||
const dayEnd = new Date(dayStart);
|
||||
dayEnd.setDate(dayStart.getDate() + 1);
|
||||
|
||||
const count = await prisma.user.count({
|
||||
where: {
|
||||
role: 'CUSTOMER',
|
||||
createdAt: {
|
||||
gte: dayStart,
|
||||
lt: dayEnd,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return { label: dayLabels[i], value: count };
|
||||
})
|
||||
);
|
||||
|
||||
// ---------- MONTHLY DATA (Jan–Dec) ----------
|
||||
const monthlyData = await Promise.all(
|
||||
Array.from({ length: 12 }).map(async (_, i) => {
|
||||
const monthStart = new Date(now.getFullYear(), i, 1);
|
||||
const monthEnd = new Date(now.getFullYear(), i + 1, 1);
|
||||
|
||||
const count = await prisma.user.count({
|
||||
where: {
|
||||
role: 'CUSTOMER',
|
||||
createdAt: {
|
||||
gte: monthStart,
|
||||
lt: monthEnd,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const monthLabels = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
return { label: monthLabels[i], value: count };
|
||||
})
|
||||
);
|
||||
|
||||
// ---------- YEARLY DATA (Last 5 Years) ----------
|
||||
const currentYear = now.getFullYear();
|
||||
const yearlyData = await Promise.all(
|
||||
Array.from({ length: 5 }).map(async (_, i) => {
|
||||
const year = currentYear - (4 - i);
|
||||
|
||||
const yearStart = new Date(year, 0, 1);
|
||||
const yearEnd = new Date(year + 1, 0, 1);
|
||||
|
||||
const count = await prisma.user.count({
|
||||
where: {
|
||||
role: 'CUSTOMER',
|
||||
createdAt: {
|
||||
gte: yearStart,
|
||||
lt: yearEnd,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { label: year.toString(), value: count };
|
||||
})
|
||||
);
|
||||
|
||||
// ---------- CURRENT STATS ----------
|
||||
const [newCustomers, repeatCustomers] = await Promise.all([
|
||||
prisma.user.count({
|
||||
where: {
|
||||
role: 'CUSTOMER',
|
||||
createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
|
||||
},
|
||||
}),
|
||||
|
||||
prisma.order.groupBy({
|
||||
by: ['userId'],
|
||||
_count: { id: true },
|
||||
having: { id: { _count: { gt: 1 } } },
|
||||
}),
|
||||
]);
|
||||
|
||||
// ---------- RESPONSE ----------
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
newCustomers,
|
||||
repeatCustomers: repeatCustomers.length,
|
||||
|
||||
graph: {
|
||||
weekly: weeklyData,
|
||||
monthly: monthlyData,
|
||||
yearly: yearlyData,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ===============================
|
||||
// 📌 4. SELLER STATISTICS
|
||||
// ===============================
|
||||
getSellerStats: async (req, res, next) => {
|
||||
try {
|
||||
// Fetch all sellers from PostgreSQL
|
||||
const sellers = await prisma.user.findMany({
|
||||
where: { role: 'SELLER' },
|
||||
});
|
||||
|
||||
// For each seller, count products from MongoDB
|
||||
const formatted = await Promise.all(
|
||||
sellers.map(async s => {
|
||||
const totalProducts = await Product.countDocuments({
|
||||
sellerId: s.id,
|
||||
});
|
||||
return {
|
||||
sellerId: s.id,
|
||||
name: `${s.firstName} ${s.lastName}`,
|
||||
totalProducts,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
res.json({ success: true, data: formatted });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ===============================
|
||||
// 📌 5. ORDER ANALYTICS
|
||||
// ===============================
|
||||
getOrderAnalytics: async (req, res, next) => {
|
||||
try {
|
||||
const orders = await prisma.order.groupBy({
|
||||
by: ['status'],
|
||||
_count: { id: true },
|
||||
});
|
||||
|
||||
res.json({ success: true, data: orders });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ===============================
|
||||
// 📌 6. RETURN / REFUND REPORT
|
||||
// ===============================
|
||||
// getReturnAnalytics: async (req, res, next) => {
|
||||
// try {
|
||||
// const totalReturns = await ReturnModel.countDocuments();
|
||||
|
||||
// const returnReasons = await ReturnModel.aggregate([
|
||||
// {
|
||||
// $group: {
|
||||
// _id: "$reason",
|
||||
// count: { $sum: 1 },
|
||||
// },
|
||||
// },
|
||||
// ]);
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// data: {
|
||||
// totalReturns,
|
||||
// returnReasons,
|
||||
// },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// },
|
||||
|
||||
// ===============================
|
||||
// 📌 7. INVENTORY & STOCK REPORT
|
||||
// ===============================
|
||||
// Controller: getInventoryStats
|
||||
getInventoryStats: async (req, res, next) => {
|
||||
try {
|
||||
const [lowStock, outOfStock, fastMoving] = await Promise.all([
|
||||
Product.find({ stock: { $lte: 5, $gt: 0 } })
|
||||
.select('_id name stock category')
|
||||
.populate('category', 'name'), // get only category name
|
||||
Product.find({ stock: 0 })
|
||||
.select('_id name stock category')
|
||||
.populate('category', 'name'),
|
||||
Product.find()
|
||||
.sort({ purchaseCount: -1 })
|
||||
.limit(10)
|
||||
.select('_id name stock category purchaseCount')
|
||||
.populate('category', 'name'),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
lowStock,
|
||||
outOfStock,
|
||||
fastMoving,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ===============================
|
||||
// 📌 Financial Stats (Safe Version)
|
||||
// ===============================
|
||||
getFinancialStats: async (req, res, next) => {
|
||||
try {
|
||||
// Total revenue from PAID orders
|
||||
const totalRevenue = await prisma.order.aggregate({
|
||||
_sum: { totalAmount: true },
|
||||
where: { paymentStatus: 'PAID' },
|
||||
});
|
||||
|
||||
// Total number of orders
|
||||
const totalOrders = await prisma.order.count();
|
||||
|
||||
// Total number of paid orders
|
||||
const paidOrders = await prisma.order.count({
|
||||
where: { paymentStatus: 'PAID' },
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalRevenue: totalRevenue._sum.totalAmount || 0,
|
||||
totalOrders,
|
||||
paidOrders,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ===============================
|
||||
// 📌 9. PAYOUT / TRANSFER HISTORY
|
||||
// ===============================
|
||||
getPayoutHistory: async (req, res, next) => {
|
||||
try {
|
||||
// Fetch all delivered orders with user info
|
||||
const orders = await prisma.order.findMany({
|
||||
where: { status: 'DELIVERED' },
|
||||
include: { user: true },
|
||||
orderBy: { deliveredAt: 'desc' },
|
||||
});
|
||||
|
||||
const formatted = orders.map(order => ({
|
||||
orderId: order.id,
|
||||
orderNumber: order.orderNumber,
|
||||
sellerId: order.userId,
|
||||
sellerName: `${order.user.firstName} ${order.user.lastName}`,
|
||||
totalAmount: Number(order.totalAmount),
|
||||
deliveredAt: order.deliveredAt,
|
||||
}));
|
||||
|
||||
res.json({ success: true, data: formatted });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ===============================
|
||||
// 📌 10. REAL-TIME ACTIVITY FEED
|
||||
// ===============================
|
||||
getActivityFeed: async (req, res, next) => {
|
||||
try {
|
||||
const logs = await prisma.activityLog.findMany({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 20,
|
||||
});
|
||||
|
||||
res.json({ success: true, data: logs });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
160
src/controllers/admin/userController.js
Normal file
160
src/controllers/admin/userController.js
Normal file
@@ -0,0 +1,160 @@
|
||||
const { prisma } = require('../../config/database');
|
||||
|
||||
exports.getAllUsers = async (req, res, next) => {
|
||||
try {
|
||||
const { page = 1, limit = 20, role, search, isActive } = req.query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where = {};
|
||||
if (role) where.role = role;
|
||||
if (isActive !== undefined) where.isActive = isActive === 'true';
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ email: { contains: search, mode: 'insensitive' } },
|
||||
{ firstName: { contains: search, mode: 'insensitive' } },
|
||||
{ lastName: { contains: search, mode: 'insensitive' } },
|
||||
{ username: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
const [users, total] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
role: true,
|
||||
isVerified: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
lastLoginAt: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: parseInt(skip),
|
||||
take: parseInt(limit),
|
||||
}),
|
||||
prisma.user.count({ where }),
|
||||
]);
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// data: { users, pagination: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) } },
|
||||
// });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Users fetched successfully',
|
||||
data: {
|
||||
users,
|
||||
pagination: {
|
||||
page: +page,
|
||||
limit: +limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateUserStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { isActive, role } = req.body;
|
||||
const { id } = req.params;
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id } });
|
||||
// if (!user) return res.status(404).json({ success: false, message: 'User not found' });
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(isActive !== undefined && { isActive }),
|
||||
...(role && { role }),
|
||||
},
|
||||
});
|
||||
|
||||
// res.json({ success: true, message: 'User updated successfully', data: updatedUser });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'User updated successfully',
|
||||
data: updatedUser,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getUserById = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// if (!id) {
|
||||
// return res.status(400).json({
|
||||
// success: false,
|
||||
// message: "User ID is required",
|
||||
// });
|
||||
// }
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'User ID is required',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
role: true,
|
||||
isVerified: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
lastLoginAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
// if (!user) {
|
||||
// return res.status(404).json({
|
||||
// success: false,
|
||||
// message: "User not found",
|
||||
// });
|
||||
// }
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
// res.json({ success: true, data: { user } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'User details fetched successfully',
|
||||
data: { user },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
297
src/controllers/authController.js
Normal file
297
src/controllers/authController.js
Normal file
@@ -0,0 +1,297 @@
|
||||
const authService = require('../services/authService');
|
||||
|
||||
// @desc Register user
|
||||
exports.register = async (req, res, next) => {
|
||||
try {
|
||||
const { email, password, firstName, lastName, username, phone, role } =
|
||||
req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Email and password are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Password must be at least 6 characters',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await authService.register({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
username,
|
||||
phone,
|
||||
role,
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
statusCode: 201,
|
||||
status: true,
|
||||
message: 'User registered successfully',
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Login user
|
||||
exports.login = async (req, res, next) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Email and password are required',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await authService.login(email, password);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Login successful',
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Refresh token
|
||||
// @desc Refresh token
|
||||
exports.refreshToken = async (req, res, next) => {
|
||||
try {
|
||||
const { refreshToken } = req.body;
|
||||
|
||||
if (!refreshToken) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Refresh token is required',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await authService.refreshToken(refreshToken);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Token refreshed successfully',
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Logout user
|
||||
exports.logout = async (req, res, next) => {
|
||||
try {
|
||||
await authService.logout(req.user.id);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Logout successful',
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Change password
|
||||
exports.changePassword = async (req, res, next) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Current password and new password are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'New password must be at least 6 characters',
|
||||
});
|
||||
}
|
||||
|
||||
await authService.changePassword(req.user.id, currentPassword, newPassword);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Password changed successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Forgot password
|
||||
exports.forgotPassword = async (req, res, next) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Email is required',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await authService.requestPasswordReset(email);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: result.message || 'Password reset email sent successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Reset password
|
||||
exports.resetPassword = async (req, res, next) => {
|
||||
try {
|
||||
const { token, newPassword } = req.body;
|
||||
|
||||
if (!token || !newPassword) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Token and new password are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Password must be at least 6 characters',
|
||||
});
|
||||
}
|
||||
|
||||
await authService.resetPassword(token, newPassword);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Password reset successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Send verification email
|
||||
exports.sendVerification = async (req, res, next) => {
|
||||
try {
|
||||
await authService.sendVerificationEmail(req.user.id);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Verification email sent',
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Verify email
|
||||
exports.verifyEmail = async (req, res, next) => {
|
||||
try {
|
||||
const { token } = req.body;
|
||||
|
||||
if (!token) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Verification token is required',
|
||||
});
|
||||
}
|
||||
|
||||
await authService.verifyEmail(token);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Email verified successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get current user profile
|
||||
exports.getMe = async (req, res, next) => {
|
||||
try {
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'User retrieved successfully',
|
||||
data: { user: req.user },
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
351
src/controllers/couponController.js
Normal file
351
src/controllers/couponController.js
Normal file
@@ -0,0 +1,351 @@
|
||||
const { prisma } = require('../config/database');
|
||||
const { Decimal } = require('@prisma/client/runtime/library');
|
||||
|
||||
/**
|
||||
* @desc Validate and apply coupon code
|
||||
* @route POST /api/coupons/validate
|
||||
* @access Public/Private
|
||||
*/
|
||||
exports.validateCoupon = async (req, res, next) => {
|
||||
try {
|
||||
const { code, orderAmount } = req.body;
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (!code || !orderAmount) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: 'Coupon code and order amount are required',
|
||||
});
|
||||
}
|
||||
|
||||
// Find coupon
|
||||
const coupon = await prisma.coupon.findUnique({
|
||||
where: { code: code.toUpperCase() },
|
||||
});
|
||||
|
||||
if (!coupon) {
|
||||
return res.status(404).json({
|
||||
status: false,
|
||||
message: 'Invalid coupon code',
|
||||
});
|
||||
}
|
||||
|
||||
// Validate coupon
|
||||
const validation = validateCouponRules(coupon, orderAmount);
|
||||
|
||||
if (!validation.isValid) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: validation.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate discount
|
||||
const discountAmount = calculateDiscount(coupon, orderAmount);
|
||||
const finalAmount = orderAmount - discountAmount;
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Coupon applied successfully',
|
||||
data: {
|
||||
couponCode: coupon.code,
|
||||
couponType: coupon.type,
|
||||
discountAmount: parseFloat(discountAmount.toFixed(2)),
|
||||
originalAmount: parseFloat(orderAmount.toFixed(2)),
|
||||
finalAmount: parseFloat(finalAmount.toFixed(2)),
|
||||
savings: parseFloat(discountAmount.toFixed(2)),
|
||||
savingsPercentage: parseFloat(((discountAmount / orderAmount) * 100).toFixed(2)),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Validate coupon error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Get available coupons for user
|
||||
* @route GET /api/coupons/available
|
||||
* @access Public
|
||||
*/
|
||||
exports.getAvailableCoupons = async (req, res, next) => {
|
||||
try {
|
||||
const { orderAmount } = req.query;
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Get active coupons
|
||||
const coupons = await prisma.coupon.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
validFrom: { lte: now },
|
||||
validUntil: { gte: now },
|
||||
OR: [
|
||||
{ maxUses: null }, // No limit
|
||||
{ usedCount: { lt: prisma.coupon.fields.maxUses } }, // Has remaining uses
|
||||
],
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Filter by minimum order amount if provided
|
||||
let filteredCoupons = coupons;
|
||||
if (orderAmount) {
|
||||
filteredCoupons = coupons.filter(coupon => {
|
||||
if (!coupon.minOrderAmount) return true;
|
||||
return parseFloat(orderAmount) >= parseFloat(coupon.minOrderAmount);
|
||||
});
|
||||
}
|
||||
|
||||
// Add discount preview
|
||||
const couponsWithPreview = filteredCoupons.map(coupon => {
|
||||
let discountPreview = '';
|
||||
|
||||
if (coupon.type === 'PERCENTAGE') {
|
||||
discountPreview = `${coupon.value}% OFF`;
|
||||
} else if (coupon.type === 'FIXED_AMOUNT') {
|
||||
discountPreview = `₹${coupon.value} OFF`;
|
||||
} else if (coupon.type === 'FREE_SHIPPING') {
|
||||
discountPreview = 'FREE SHIPPING';
|
||||
}
|
||||
|
||||
return {
|
||||
code: coupon.code,
|
||||
description: coupon.description,
|
||||
type: coupon.type,
|
||||
value: parseFloat(coupon.value),
|
||||
minOrderAmount: coupon.minOrderAmount ? parseFloat(coupon.minOrderAmount) : null,
|
||||
validUntil: coupon.validUntil,
|
||||
discountPreview,
|
||||
remainingUses: coupon.maxUses ? coupon.maxUses - coupon.usedCount : null,
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Available coupons fetched successfully',
|
||||
data: couponsWithPreview,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Apply coupon to order
|
||||
* @route POST /api/coupons/apply
|
||||
* @access Private
|
||||
*/
|
||||
exports.applyCouponToOrder = async (req, res, next) => {
|
||||
try {
|
||||
const { couponCode, orderId } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
// Get order
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
status: false,
|
||||
message: 'Order not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Verify order belongs to user
|
||||
if (order.userId !== userId) {
|
||||
return res.status(403).json({
|
||||
status: false,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if order already has a coupon
|
||||
if (order.discountAmount > 0) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: 'Order already has a discount applied',
|
||||
});
|
||||
}
|
||||
|
||||
// Get coupon
|
||||
const coupon = await prisma.coupon.findUnique({
|
||||
where: { code: couponCode.toUpperCase() },
|
||||
});
|
||||
|
||||
if (!coupon) {
|
||||
return res.status(404).json({
|
||||
status: false,
|
||||
message: 'Invalid coupon code',
|
||||
});
|
||||
}
|
||||
|
||||
// Validate coupon
|
||||
const orderAmount = parseFloat(order.subtotal);
|
||||
const validation = validateCouponRules(coupon, orderAmount);
|
||||
|
||||
if (!validation.isValid) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: validation.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate discount
|
||||
let discountAmount = calculateDiscount(coupon, orderAmount);
|
||||
let shippingAmount = parseFloat(order.shippingAmount);
|
||||
|
||||
// If free shipping coupon, set shipping to 0
|
||||
if (coupon.type === 'FREE_SHIPPING') {
|
||||
discountAmount = shippingAmount;
|
||||
shippingAmount = 0;
|
||||
}
|
||||
|
||||
// Recalculate total
|
||||
const newTotal = orderAmount + parseFloat(order.taxAmount) + shippingAmount - discountAmount;
|
||||
|
||||
// Update order
|
||||
const updatedOrder = await prisma.$transaction([
|
||||
// Update order
|
||||
prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
discountAmount,
|
||||
shippingAmount,
|
||||
totalAmount: newTotal,
|
||||
},
|
||||
}),
|
||||
// Increment coupon usage
|
||||
prisma.coupon.update({
|
||||
where: { id: coupon.id },
|
||||
data: {
|
||||
usedCount: { increment: 1 },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Coupon applied successfully',
|
||||
data: updatedOrder[0],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Apply coupon error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Remove coupon from order
|
||||
* @route POST /api/coupons/remove
|
||||
* @access Private
|
||||
*/
|
||||
exports.removeCouponFromOrder = async (req, res, next) => {
|
||||
try {
|
||||
const { orderId } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
status: false,
|
||||
message: 'Order not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (order.userId !== userId) {
|
||||
return res.status(403).json({
|
||||
status: false,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
|
||||
if (order.discountAmount === 0) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: 'No coupon applied to this order',
|
||||
});
|
||||
}
|
||||
|
||||
// Recalculate total without discount
|
||||
const newTotal =
|
||||
parseFloat(order.subtotal) +
|
||||
parseFloat(order.taxAmount) +
|
||||
parseFloat(order.shippingAmount);
|
||||
|
||||
const updatedOrder = await prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
discountAmount: 0,
|
||||
totalAmount: newTotal,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Coupon removed successfully',
|
||||
data: updatedOrder,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Validate coupon rules
|
||||
*/
|
||||
function validateCouponRules(coupon, orderAmount) {
|
||||
const now = new Date();
|
||||
|
||||
// Check if active
|
||||
if (!coupon.isActive) {
|
||||
return { isValid: false, message: 'This coupon is no longer active' };
|
||||
}
|
||||
|
||||
// Check date range
|
||||
if (now < new Date(coupon.validFrom)) {
|
||||
return { isValid: false, message: 'This coupon is not yet valid' };
|
||||
}
|
||||
|
||||
if (now > new Date(coupon.validUntil)) {
|
||||
return { isValid: false, message: 'This coupon has expired' };
|
||||
}
|
||||
|
||||
// Check usage limit
|
||||
if (coupon.maxUses && coupon.usedCount >= coupon.maxUses) {
|
||||
return { isValid: false, message: 'This coupon has reached its usage limit' };
|
||||
}
|
||||
|
||||
// Check minimum order amount
|
||||
if (coupon.minOrderAmount && orderAmount < parseFloat(coupon.minOrderAmount)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Minimum order amount of ₹${coupon.minOrderAmount} required`,
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate discount amount
|
||||
*/
|
||||
function calculateDiscount(coupon, orderAmount) {
|
||||
if (coupon.type === 'PERCENTAGE') {
|
||||
return (orderAmount * parseFloat(coupon.value)) / 100;
|
||||
} else if (coupon.type === 'FIXED_AMOUNT') {
|
||||
return Math.min(parseFloat(coupon.value), orderAmount);
|
||||
} else if (coupon.type === 'FREE_SHIPPING') {
|
||||
return 0; // Handled separately in order
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
822
src/controllers/orderController.js
Normal file
822
src/controllers/orderController.js
Normal file
@@ -0,0 +1,822 @@
|
||||
const { prisma } = require('../config/database');
|
||||
const Product = require('../models/mongodb/Product');
|
||||
const {
|
||||
RETURN_WINDOW_DAYS,
|
||||
ALLOWED_STATUSES,
|
||||
} = require('../config/returnPolicy');
|
||||
const {
|
||||
calculateDeliveryDate,
|
||||
} = require('../services/deliveryEstimationService');
|
||||
const { reduceStockOnDelivery } = require('../services/inventoryService');
|
||||
|
||||
// @desc Create new order
|
||||
// @route POST /api/orders
|
||||
// @access Private
|
||||
exports.createOrder = async (req, res, next) => {
|
||||
try {
|
||||
const { items, shippingAddressId, paymentMethod, couponCode } = req.body;
|
||||
const userId = req.user.id;
|
||||
console.log('=================================');
|
||||
console.log('🎟️ COUPON DEBUG');
|
||||
console.log('Received couponCode:', req.body.couponCode);
|
||||
console.log('Full body:', req.body);
|
||||
console.log('=================================');
|
||||
console.log('📦 Creating order for user:', userId);
|
||||
console.log('🎟️ Coupon code:', couponCode);
|
||||
|
||||
// Validate items
|
||||
if (!items || items.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'No items in the order',
|
||||
});
|
||||
}
|
||||
|
||||
// Validate shipping address
|
||||
const address = await prisma.address.findUnique({
|
||||
where: { id: shippingAddressId },
|
||||
});
|
||||
const deliveryEstimation = calculateDeliveryDate(
|
||||
address.postalCode,
|
||||
new Date(),
|
||||
'STANDARD'
|
||||
);
|
||||
|
||||
console.log(
|
||||
'📅 Estimated delivery:',
|
||||
deliveryEstimation.estimatedDelivery.formatted
|
||||
);
|
||||
|
||||
if (!address || address.userId !== userId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid shipping address',
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch product details from MongoDB
|
||||
const productIds = items.map(item => item.productId);
|
||||
const products = await Product.find({ _id: { $in: productIds } });
|
||||
|
||||
if (products.length !== productIds.length) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Some products not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate totals
|
||||
let subtotal = 0;
|
||||
const orderItems = [];
|
||||
|
||||
for (const item of items) {
|
||||
const product = products.find(p => p._id.toString() === item.productId);
|
||||
|
||||
if (!product) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Product ${item.productId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
let price = product.basePrice;
|
||||
let sku = product.slug;
|
||||
|
||||
if (product.hasVariants && product.variants?.length > 0) {
|
||||
const variant = product.variants.find(v => v.sku === item.sku);
|
||||
if (variant) {
|
||||
price = variant.price;
|
||||
sku = variant.sku;
|
||||
}
|
||||
}
|
||||
|
||||
const itemTotal = price * item.quantity;
|
||||
subtotal += itemTotal;
|
||||
|
||||
orderItems.push({
|
||||
productId: item.productId,
|
||||
productName: product.name,
|
||||
productSku: sku,
|
||||
price: price,
|
||||
quantity: item.quantity,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate tax and shipping
|
||||
const taxRate = 0.18; // 18% GST
|
||||
const taxAmount = subtotal * taxRate;
|
||||
let shippingAmount = subtotal > 500 ? 0 : 50; // Free shipping above ₹500
|
||||
let discountAmount = 0;
|
||||
let appliedCoupon = null;
|
||||
|
||||
// ==========================================
|
||||
// VALIDATE AND APPLY COUPON
|
||||
// ==========================================
|
||||
if (couponCode) {
|
||||
console.log('🎟️ Validating coupon:', couponCode);
|
||||
|
||||
const coupon = await prisma.coupon.findUnique({
|
||||
where: { code: couponCode.toUpperCase() },
|
||||
});
|
||||
|
||||
if (coupon) {
|
||||
// Validate coupon
|
||||
const now = new Date();
|
||||
let couponError = null;
|
||||
|
||||
if (!coupon.isActive) {
|
||||
couponError = 'Coupon is not active';
|
||||
} else if (now < new Date(coupon.validFrom)) {
|
||||
couponError = 'Coupon is not yet valid';
|
||||
} else if (now > new Date(coupon.validUntil)) {
|
||||
couponError = 'Coupon has expired';
|
||||
} else if (coupon.maxUses && coupon.usedCount >= coupon.maxUses) {
|
||||
couponError = 'Coupon usage limit reached';
|
||||
} else if (
|
||||
coupon.minOrderAmount &&
|
||||
subtotal < parseFloat(coupon.minOrderAmount)
|
||||
) {
|
||||
couponError = `Minimum order amount of ₹${coupon.minOrderAmount} required`;
|
||||
}
|
||||
|
||||
if (couponError) {
|
||||
console.log('❌ Coupon validation failed:', couponError);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: couponError,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate discount
|
||||
if (coupon.type === 'PERCENTAGE') {
|
||||
discountAmount = (subtotal * parseFloat(coupon.value)) / 100;
|
||||
} else if (coupon.type === 'FIXED_AMOUNT') {
|
||||
discountAmount = Math.min(parseFloat(coupon.value), subtotal);
|
||||
} else if (coupon.type === 'FREE_SHIPPING') {
|
||||
discountAmount = shippingAmount;
|
||||
shippingAmount = 0;
|
||||
}
|
||||
|
||||
appliedCoupon = coupon;
|
||||
console.log('✅ Coupon applied:', {
|
||||
code: coupon.code,
|
||||
type: coupon.type,
|
||||
discount: discountAmount,
|
||||
});
|
||||
} else {
|
||||
console.log('❌ Coupon not found');
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid coupon code',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate final total
|
||||
const totalAmount = subtotal + taxAmount + shippingAmount - discountAmount;
|
||||
|
||||
// Generate unique order number
|
||||
const orderNumber = `ORD${Date.now()}${Math.floor(Math.random() * 1000)}`;
|
||||
|
||||
// ==========================================
|
||||
// CREATE ORDER IN TRANSACTION
|
||||
// ==========================================
|
||||
const result = await prisma.$transaction(async tx => {
|
||||
// Create order
|
||||
const order = await tx.order.create({
|
||||
data: {
|
||||
orderNumber,
|
||||
userId,
|
||||
status: 'PENDING',
|
||||
subtotal,
|
||||
taxAmount,
|
||||
shippingAmount,
|
||||
discountAmount,
|
||||
totalAmount,
|
||||
paymentStatus: 'PENDING',
|
||||
paymentMethod: paymentMethod || 'PAYTM',
|
||||
shippingAddressId,
|
||||
items: {
|
||||
create: orderItems,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
items: true,
|
||||
address: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ INCREMENT COUPON USAGE COUNT
|
||||
if (appliedCoupon) {
|
||||
await tx.coupon.update({
|
||||
where: { id: appliedCoupon.id },
|
||||
data: {
|
||||
usedCount: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log('✅ Coupon usage incremented:', {
|
||||
code: appliedCoupon.code,
|
||||
previousCount: appliedCoupon.usedCount,
|
||||
newCount: appliedCoupon.usedCount + 1,
|
||||
});
|
||||
}
|
||||
|
||||
return order;
|
||||
});
|
||||
|
||||
console.log('✅ Order created:', result.id);
|
||||
|
||||
// Clear user's cart after order creation
|
||||
await prisma.cartItem.deleteMany({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Order created successfully',
|
||||
order: result,
|
||||
appliedCoupon: appliedCoupon
|
||||
? {
|
||||
code: appliedCoupon.code,
|
||||
discount: discountAmount,
|
||||
}
|
||||
: null,
|
||||
deliveryEstimation,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create order error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get user orders
|
||||
exports.getUserOrders = async (req, res, next) => {
|
||||
try {
|
||||
const { page = 1, limit = 10, status } = req.query;
|
||||
const skip = (page - 1) * limit;
|
||||
const where = { userId: req.user.id };
|
||||
if (status) where.status = status;
|
||||
|
||||
const [orders, total] = await Promise.all([
|
||||
prisma.order.findMany({
|
||||
where,
|
||||
include: { items: true, address: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: parseInt(skip),
|
||||
take: parseInt(limit),
|
||||
}),
|
||||
prisma.order.count({ where }),
|
||||
]);
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// data: {
|
||||
// orders,
|
||||
// pagination: {
|
||||
// page: parseInt(page),
|
||||
// limit: parseInt(limit),
|
||||
// total,
|
||||
// pages: Math.ceil(total / limit),
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Orders fetched successfully',
|
||||
data: {
|
||||
orders,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get single order
|
||||
exports.getOrderById = async (req, res, next) => {
|
||||
try {
|
||||
const order = await prisma.order.findFirst({
|
||||
where: { id: req.params.id, userId: req.user.id },
|
||||
include: { items: true, address: true },
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
// statusCode: 404,
|
||||
status: false,
|
||||
message: 'Order not found',
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
// 2️⃣ Collect productIds
|
||||
const productIds = order.items.map(item => item.productId);
|
||||
|
||||
// 3️⃣ Fetch products from MongoDB
|
||||
const products = await Product.find({
|
||||
_id: { $in: productIds },
|
||||
}).select('name images');
|
||||
|
||||
// 4️⃣ Convert products array to map for quick lookup
|
||||
const productMap = {};
|
||||
products.forEach(product => {
|
||||
productMap[product._id.toString()] = product;
|
||||
});
|
||||
|
||||
// 5️⃣ Attach product image to each order item
|
||||
const updatedItems = order.items.map(item => {
|
||||
const product = productMap[item.productId];
|
||||
|
||||
return {
|
||||
...item,
|
||||
productImage: product?.images?.primary || null,
|
||||
productGallery: product?.images?.gallery || [],
|
||||
};
|
||||
});
|
||||
|
||||
order.items = updatedItems;
|
||||
|
||||
// res.json({ success: true, data: { order } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Order fetched successfully',
|
||||
data: { order },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Update order status (Admin)
|
||||
exports.updateOrderStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { status, trackingNumber } = req.body;
|
||||
// if (!status)
|
||||
// return res
|
||||
// .status(400)
|
||||
// .json({ success: false, message: 'Status is required' });
|
||||
|
||||
if (!status) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Status is required',
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: req.params.id },
|
||||
});
|
||||
// if (!order)
|
||||
// return res
|
||||
// .status(404)
|
||||
// .json({ success: false, message: 'Order not found' });
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Order not found',
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
const updateData = { status };
|
||||
if (trackingNumber) updateData.trackingNumber = trackingNumber;
|
||||
if (status === 'SHIPPED') updateData.shippedAt = new Date();
|
||||
if (status === 'DELIVERED') updateData.deliveredAt = new Date();
|
||||
|
||||
const updatedOrder = await prisma.order.update({
|
||||
where: { id: req.params.id },
|
||||
data: updateData,
|
||||
include: { items: true, address: true },
|
||||
});
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// message: 'Order status updated',
|
||||
// data: { order: updatedOrder },
|
||||
// });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Order status updated successfully',
|
||||
data: { order: updatedOrder },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Cancel order
|
||||
exports.cancelOrder = async (req, res, next) => {
|
||||
try {
|
||||
const order = await prisma.order.findFirst({
|
||||
where: { id: req.params.id, userId: req.user.id },
|
||||
});
|
||||
|
||||
// if (!order)
|
||||
// return res
|
||||
// .status(404)
|
||||
// .json({ success: false, message: 'Order not found' });
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Order not found',
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
// if (!['PENDING', 'CONFIRMED'].includes(order.status))
|
||||
// return res
|
||||
// .status(400)
|
||||
// .json({ success: false, message: 'Order cannot be cancelled' });
|
||||
|
||||
if (!['PENDING', 'CONFIRMED'].includes(order.status)) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Order cannot be cancelled',
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedOrder = await prisma.order.update({
|
||||
where: { id: req.params.id },
|
||||
data: { status: 'CANCELLED' },
|
||||
include: { items: true, address: true },
|
||||
});
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// message: 'Order cancelled',
|
||||
// data: { order: updatedOrder },
|
||||
// });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Order cancelled successfully',
|
||||
data: { order: updatedOrder },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// exports.returnOrder = async (req, res, next) => {
|
||||
// try {
|
||||
// const order = await prisma.order.findFirst({
|
||||
// where: {
|
||||
// id: req.params.id,
|
||||
// userId: req.user.id,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (!order) {
|
||||
// return res.status(404).json({
|
||||
// status: false,
|
||||
// message: 'Order not found',
|
||||
// });
|
||||
// }
|
||||
|
||||
// // ✅ RETURN STATUS CHECK (YOUR CODE GOES HERE)
|
||||
// if (!ALLOWED_STATUSES.includes(order.status)) {
|
||||
// return res.status(400).json({
|
||||
// status: false,
|
||||
// message: 'Order not eligible for return',
|
||||
// });
|
||||
// }
|
||||
|
||||
// // ✅ RETURN WINDOW CHECK
|
||||
// const deliveredAt = order.deliveredAt || order.updatedAt;
|
||||
// const diffDays =
|
||||
// (Date.now() - new Date(deliveredAt)) / (1000 * 60 * 60 * 24);
|
||||
|
||||
// if (diffDays > RETURN_WINDOW_DAYS) {
|
||||
// return res.status(400).json({
|
||||
// status: false,
|
||||
// message: `Return allowed within ${RETURN_WINDOW_DAYS} days only`,
|
||||
// });
|
||||
// }
|
||||
|
||||
// // ✅ UPDATE ORDER
|
||||
// const updatedOrder = await prisma.order.update({
|
||||
// where: { id: order.id },
|
||||
// data: {
|
||||
// status: 'RETURN_REQUESTED',
|
||||
// returnRequestedAt: new Date(),
|
||||
// },
|
||||
// });
|
||||
|
||||
// return res.status(200).json({
|
||||
// status: true,
|
||||
// message: 'Return request submitted successfully',
|
||||
// data: { order: updatedOrder },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// @desc Return order
|
||||
// @route PUT /api/orders/:id/return
|
||||
// @access Private
|
||||
exports.returnOrder = async (req, res, next) => {
|
||||
try {
|
||||
// Find the order belonging to the logged-in user
|
||||
const order = await prisma.order.findFirst({
|
||||
where: {
|
||||
id: req.params.id,
|
||||
userId: req.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
status: false,
|
||||
message: 'Order not found',
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Check if order status allows return
|
||||
const ALLOWED_STATUSES = ['DELIVERED']; // only delivered orders can be returned
|
||||
if (!ALLOWED_STATUSES.includes(order.status)) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: 'Order not eligible for return',
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Check return window (e.g., 7 days)
|
||||
const RETURN_WINDOW_DAYS = 7;
|
||||
const deliveredAt = order.deliveredAt || order.updatedAt;
|
||||
const diffDays =
|
||||
(Date.now() - new Date(deliveredAt)) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (diffDays > RETURN_WINDOW_DAYS) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: `Return allowed within ${RETURN_WINDOW_DAYS} days only`,
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Update order: set both status and returnStatus
|
||||
const updatedOrder = await prisma.order.update({
|
||||
where: { id: order.id },
|
||||
data: {
|
||||
status: 'RETURN_REQUESTED', // OrderStatus
|
||||
returnStatus: 'REQUESTED', // ReturnStatus (admin will use this)
|
||||
returnRequestedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
status: true,
|
||||
message: 'Return request submitted successfully',
|
||||
data: { order: updatedOrder },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get all orders (Admin)
|
||||
exports.getAllOrdersAdmin = async (req, res, next) => {
|
||||
try {
|
||||
const { page = 1, limit = 20, status, paymentStatus } = req.query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where = {};
|
||||
if (status) where.status = status;
|
||||
if (paymentStatus) where.paymentStatus = paymentStatus;
|
||||
|
||||
const [orders, total] = await Promise.all([
|
||||
prisma.order.findMany({
|
||||
where,
|
||||
include: {
|
||||
items: true,
|
||||
address: true,
|
||||
user: {
|
||||
select: { id: true, email: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: parseInt(skip),
|
||||
take: parseInt(limit),
|
||||
}),
|
||||
prisma.order.count({ where }),
|
||||
]);
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// data: {
|
||||
// orders,
|
||||
// pagination: {
|
||||
// page: parseInt(page),
|
||||
// limit: parseInt(limit),
|
||||
// total,
|
||||
// pages: Math.ceil(total / limit),
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Orders fetched successfully',
|
||||
data: {
|
||||
orders,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Approve or reject a return request
|
||||
// @route PUT /api/orders/:id/return/status
|
||||
// @access Private/Admin
|
||||
exports.updateReturnStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { action } = req.body; // "APPROVE" or "REJECT"
|
||||
|
||||
const order = await prisma.order.findUnique({ where: { id } });
|
||||
|
||||
if (!order) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ status: false, message: 'Order not found' });
|
||||
}
|
||||
|
||||
if (order.returnStatus !== 'REQUESTED') {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ status: false, message: 'No return request pending' });
|
||||
}
|
||||
|
||||
let newStatus;
|
||||
if (action === 'APPROVE') newStatus = 'APPROVED';
|
||||
else if (action === 'REJECT') newStatus = 'REJECTED';
|
||||
else
|
||||
return res.status(400).json({ status: false, message: 'Invalid action' });
|
||||
|
||||
const updatedOrder = await prisma.order.update({
|
||||
where: { id },
|
||||
data: { returnStatus: newStatus },
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: `Return request ${action.toLowerCase()}ed successfully`,
|
||||
data: { order: updatedOrder },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get all return requests (Admin only)
|
||||
// @route GET /api/orders/admin/returns
|
||||
// @access Private/Admin
|
||||
exports.getAdminReturnRequests = async (req, res) => {
|
||||
try {
|
||||
const page = Number(req.query.page) || 1;
|
||||
const limit = Number(req.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [returns, count] = await Promise.all([
|
||||
prisma.order.findMany({
|
||||
where: {
|
||||
returnStatus: {
|
||||
in: ['REQUESTED', 'APPROVED', 'REJECTED', 'COMPLETED'],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
address: true,
|
||||
items: true,
|
||||
},
|
||||
orderBy: { returnRequestedAt: 'desc' },
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.order.count({
|
||||
where: {
|
||||
returnStatus: {
|
||||
in: ['REQUESTED', 'APPROVED', 'REJECTED', 'COMPLETED'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
status: true,
|
||||
count,
|
||||
data: returns,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Admin return list error:', error);
|
||||
res.status(500).json({
|
||||
status: false,
|
||||
message: 'Failed to fetch return requests',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get all returned products (Admin only)
|
||||
// @route GET /api/orders/admin/returns/list
|
||||
// @access Private/Admin
|
||||
exports.getReturnedProducts = async (req, res, next) => {
|
||||
try {
|
||||
const returnedOrders = await prisma.order.findMany({
|
||||
where: {
|
||||
returnStatus: { in: ['APPROVED', 'COMPLETED'] },
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
address: true,
|
||||
items: true,
|
||||
},
|
||||
orderBy: {
|
||||
returnRequestedAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
count: returnedOrders.length,
|
||||
data: returnedOrders,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getReturnRequestById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Fetch the order with related user, address, and items
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: true,
|
||||
address: true,
|
||||
items: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ status: false, message: 'Return request not found' });
|
||||
}
|
||||
|
||||
// Ensure this order is a return request
|
||||
if (
|
||||
!['RETURN_REQUESTED', 'APPROVED', 'REJECTED', 'COMPLETED'].includes(
|
||||
order.returnStatus
|
||||
)
|
||||
) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ status: false, message: 'This order is not a return request' });
|
||||
}
|
||||
|
||||
res.json({ status: true, data: order });
|
||||
} catch (error) {
|
||||
console.error('Error fetching return request:', error);
|
||||
res.status(500).json({ status: false, message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// module.exports = { getReturnRequestById };
|
||||
719
src/controllers/orderTrackingController.js
Normal file
719
src/controllers/orderTrackingController.js
Normal file
@@ -0,0 +1,719 @@
|
||||
// controllers/orderTrackingController.js
|
||||
|
||||
const { prisma } = require('../config/database');
|
||||
const {
|
||||
calculateDeliveryDate,
|
||||
getDeliveryEstimation,
|
||||
} = require('../services/deliveryEstimationService');
|
||||
|
||||
/**
|
||||
* @desc Get order tracking details
|
||||
* @route GET /api/orders/:orderId/tracking
|
||||
* @access Private
|
||||
*/
|
||||
exports.getOrderTracking = async (req, res, next) => {
|
||||
try {
|
||||
const { orderId } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
// Get order with all details
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
include: {
|
||||
items: true,
|
||||
address: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Order not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user owns this order
|
||||
if (order.userId !== userId) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Unauthorized to view this order',
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate delivery estimation
|
||||
const deliveryEstimation = await getDeliveryEstimation(
|
||||
order.address.postalCode,
|
||||
'STANDARD'
|
||||
);
|
||||
|
||||
// Get order timeline/history
|
||||
const timeline = generateOrderTimeline(order, deliveryEstimation.data);
|
||||
|
||||
// Get current status details
|
||||
const statusDetails = getStatusDetails(order);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
order: {
|
||||
id: order.id,
|
||||
orderNumber: order.orderNumber,
|
||||
status: order.status,
|
||||
paymentStatus: order.paymentStatus,
|
||||
totalAmount: parseFloat(order.totalAmount),
|
||||
createdAt: order.createdAt,
|
||||
},
|
||||
tracking: {
|
||||
currentStatus: statusDetails,
|
||||
timeline,
|
||||
deliveryEstimation: deliveryEstimation.data,
|
||||
trackingNumber: order.trackingNumber,
|
||||
},
|
||||
shippingAddress: {
|
||||
name: `${order.address.firstName} ${order.address.lastName}`,
|
||||
addressLine1: order.address.addressLine1,
|
||||
addressLine2: order.address.addressLine2,
|
||||
city: order.address.city,
|
||||
state: order.address.state,
|
||||
postalCode: order.address.postalCode,
|
||||
country: order.address.country,
|
||||
phone: order.address.phone,
|
||||
},
|
||||
items: order.items.map(item => ({
|
||||
productName: item.productName,
|
||||
productSku: item.productSku,
|
||||
quantity: item.quantity,
|
||||
price: parseFloat(item.price),
|
||||
})),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get order tracking error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Update order status (Admin) - FLEXIBLE VERSION
|
||||
* @route PUT /api/admin/orders/:orderId/status
|
||||
* @access Private/Admin
|
||||
*/
|
||||
// exports.updateOrderStatus = async (req, res, next) => {
|
||||
// try {
|
||||
// const { orderId } = req.params;
|
||||
// const { status, trackingNumber, notes, skipValidation = false } = req.body;
|
||||
|
||||
// console.log('📦 Updating order status:', {
|
||||
// orderId,
|
||||
// currentStatus: 'Fetching...',
|
||||
// newStatus: status,
|
||||
// trackingNumber,
|
||||
// });
|
||||
|
||||
// const order = await prisma.order.findUnique({
|
||||
// where: { id: orderId },
|
||||
// });
|
||||
|
||||
// if (!order) {
|
||||
// return res.status(404).json({
|
||||
// success: false,
|
||||
// message: 'Order not found',
|
||||
// });
|
||||
// }
|
||||
|
||||
// console.log('📦 Current order status:', order.status);
|
||||
|
||||
// // ✅ FLEXIBLE VALIDATION - Allow skipping intermediate steps
|
||||
// const validTransitions = {
|
||||
// PENDING: ['CONFIRMED', 'PROCESSING', 'SHIPPED', 'CANCELLED'],
|
||||
// CONFIRMED: ['PROCESSING', 'SHIPPED', 'CANCELLED'],
|
||||
// PROCESSING: ['SHIPPED', 'DELIVERED', 'CANCELLED'],
|
||||
// SHIPPED: ['DELIVERED'],
|
||||
// DELIVERED: ['RETURN_REQUESTED'],
|
||||
// RETURN_REQUESTED: ['REFUNDED'],
|
||||
// CANCELLED: [], // Cannot transition from cancelled
|
||||
// REFUNDED: [], // Cannot transition from refunded
|
||||
// };
|
||||
|
||||
// // Validate transition (unless skipValidation is true)
|
||||
// if (!skipValidation && !validTransitions[order.status]?.includes(status)) {
|
||||
// return res.status(400).json({
|
||||
// success: false,
|
||||
// message: `Cannot transition from ${order.status} to ${status}`,
|
||||
// allowedTransitions: validTransitions[order.status],
|
||||
// hint: 'You can set skipValidation: true to force this transition',
|
||||
// });
|
||||
// }
|
||||
|
||||
// // ✅ Auto-update intermediate statuses if needed
|
||||
// let intermediateUpdates = [];
|
||||
|
||||
// if (order.status === 'PENDING' && status === 'SHIPPED') {
|
||||
// intermediateUpdates = ['CONFIRMED', 'PROCESSING'];
|
||||
// console.log(
|
||||
// '⚡ Auto-updating intermediate statuses:',
|
||||
// intermediateUpdates
|
||||
// );
|
||||
// } else if (order.status === 'CONFIRMED' && status === 'DELIVERED') {
|
||||
// intermediateUpdates = ['PROCESSING', 'SHIPPED'];
|
||||
// console.log(
|
||||
// '⚡ Auto-updating intermediate statuses:',
|
||||
// intermediateUpdates
|
||||
// );
|
||||
// } else if (order.status === 'PENDING' && status === 'DELIVERED') {
|
||||
// intermediateUpdates = ['CONFIRMED', 'PROCESSING', 'SHIPPED'];
|
||||
// console.log(
|
||||
// '⚡ Auto-updating intermediate statuses:',
|
||||
// intermediateUpdates
|
||||
// );
|
||||
// }
|
||||
|
||||
// // Build update data
|
||||
// const updateData = { status };
|
||||
|
||||
// if (trackingNumber) {
|
||||
// updateData.trackingNumber = trackingNumber;
|
||||
// }
|
||||
|
||||
// if (status === 'SHIPPED' && !order.shippedAt) {
|
||||
// updateData.shippedAt = new Date();
|
||||
// }
|
||||
|
||||
// if (status === 'DELIVERED' && !order.deliveredAt) {
|
||||
// updateData.deliveredAt = new Date();
|
||||
// updateData.shippedAt = updateData.shippedAt || new Date(); // Ensure shipped date is set
|
||||
// }
|
||||
|
||||
// // Update order
|
||||
// const updatedOrder = await prisma.order.update({
|
||||
// where: { id: orderId },
|
||||
// data: updateData,
|
||||
// });
|
||||
|
||||
// console.log('✅ Order status updated:', {
|
||||
// from: order.status,
|
||||
// to: updatedOrder.status,
|
||||
// trackingNumber: updatedOrder.trackingNumber,
|
||||
// });
|
||||
|
||||
// // TODO: Send notification to user
|
||||
// // await sendOrderStatusNotification(updatedOrder);
|
||||
|
||||
// res.status(200).json({
|
||||
// success: true,
|
||||
// message: `Order status updated to ${status}`,
|
||||
// data: updatedOrder,
|
||||
// intermediateUpdates:
|
||||
// intermediateUpdates.length > 0 ? intermediateUpdates : undefined,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.error('❌ Update order status error:', error);
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @desc Update order status (Admin) - ALL MANUAL
|
||||
* @route PUT /api/admin/orders/:orderId/status
|
||||
* @access Private/Admin
|
||||
*/
|
||||
// exports.updateOrderStatus = async (req, res, next) => {
|
||||
// try {
|
||||
// const { orderId } = req.params;
|
||||
// const { status, trackingNumber, notes } = req.body;
|
||||
|
||||
// console.log('📦 Updating order status:', {
|
||||
// orderId,
|
||||
// newStatus: status,
|
||||
// });
|
||||
|
||||
// const order = await prisma.order.findUnique({
|
||||
// where: { id: orderId },
|
||||
// });
|
||||
|
||||
// if (!order) {
|
||||
// return res.status(404).json({
|
||||
// success: false,
|
||||
// message: 'Order not found',
|
||||
// });
|
||||
// }
|
||||
|
||||
// console.log('📦 Current order status:', order.status);
|
||||
|
||||
// // ✅ SIMPLE VALIDATION - Admin can update to any status
|
||||
// const validStatuses = [
|
||||
// 'PENDING',
|
||||
// 'CONFIRMED',
|
||||
// 'PROCESSING',
|
||||
// 'SHIPPED',
|
||||
// 'DELIVERED',
|
||||
// 'CANCELLED',
|
||||
// 'RETURN_REQUESTED',
|
||||
// ];
|
||||
|
||||
// if (!validStatuses.includes(status)) {
|
||||
// return res.status(400).json({
|
||||
// success: false,
|
||||
// message: `Invalid status: ${status}`,
|
||||
// validStatuses,
|
||||
// });
|
||||
// }
|
||||
|
||||
// // Build update data
|
||||
// const updateData = { status };
|
||||
|
||||
// if (trackingNumber) {
|
||||
// updateData.trackingNumber = trackingNumber;
|
||||
// }
|
||||
|
||||
// // Auto-set timestamps
|
||||
// if (status === 'SHIPPED' && !order.shippedAt) {
|
||||
// updateData.shippedAt = new Date();
|
||||
// }
|
||||
|
||||
// if (status === 'DELIVERED' && !order.deliveredAt) {
|
||||
// updateData.deliveredAt = new Date();
|
||||
// }
|
||||
|
||||
// // Update order
|
||||
// const updatedOrder = await prisma.order.update({
|
||||
// where: { id: orderId },
|
||||
// data: updateData,
|
||||
// });
|
||||
|
||||
// console.log('✅ Order status updated:', {
|
||||
// from: order.status,
|
||||
// to: updatedOrder.status,
|
||||
// });
|
||||
|
||||
// res.status(200).json({
|
||||
// success: true,
|
||||
// message: `Order status updated to ${status}`,
|
||||
// data: updatedOrder,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.error('❌ Update order status error:', error);
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
exports.updateOrderStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { orderId } = req.params;
|
||||
const { status, trackingNumber, notes } = req.body;
|
||||
|
||||
const adminId = req.user.id;
|
||||
const ipAddress = req.ip || req.connection.remoteAddress;
|
||||
const userAgent = req.get("user-agent");
|
||||
|
||||
console.log("📦 Updating order status:", {
|
||||
orderId,
|
||||
newStatus: status,
|
||||
admin: adminId,
|
||||
});
|
||||
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Order not found",
|
||||
});
|
||||
}
|
||||
|
||||
const oldStatus = order.status;
|
||||
|
||||
const validStatuses = [
|
||||
"PENDING",
|
||||
"CONFIRMED",
|
||||
"PROCESSING",
|
||||
"SHIPPED",
|
||||
"DELIVERED",
|
||||
"CANCELLED",
|
||||
"RETURN_REQUESTED",
|
||||
];
|
||||
|
||||
if (!validStatuses.includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Invalid status: ${status}`,
|
||||
validStatuses,
|
||||
});
|
||||
}
|
||||
|
||||
const updateData = { status };
|
||||
|
||||
if (trackingNumber) updateData.trackingNumber = trackingNumber;
|
||||
|
||||
if (status === "SHIPPED" && !order.shippedAt) {
|
||||
updateData.shippedAt = new Date();
|
||||
}
|
||||
|
||||
if (status === "DELIVERED" && !order.deliveredAt) {
|
||||
updateData.deliveredAt = new Date();
|
||||
}
|
||||
|
||||
// ✅ SINGLE CLEAN TRANSACTION
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const updatedOrder = await tx.order.update({
|
||||
where: { id: orderId },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
const historyRecord = await tx.orderStatusHistory.create({
|
||||
data: {
|
||||
orderId,
|
||||
fromStatus: oldStatus,
|
||||
toStatus: status,
|
||||
changedBy: adminId,
|
||||
trackingNumber: trackingNumber || null,
|
||||
notes: notes || null,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
},
|
||||
});
|
||||
|
||||
return { order: updatedOrder, history: historyRecord };
|
||||
});
|
||||
|
||||
// ✅ Auto stock reduction AFTER successful transaction
|
||||
let stockReduction = null;
|
||||
|
||||
if (status === "DELIVERED" && oldStatus !== "DELIVERED") {
|
||||
try {
|
||||
stockReduction = await reduceStockOnDelivery(orderId);
|
||||
console.log("✅ Stock reduced automatically");
|
||||
} catch (err) {
|
||||
console.error("❌ Stock reduction failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ Order status updated:", {
|
||||
from: oldStatus,
|
||||
to: result.order.status,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: `Order status updated from ${oldStatus} to ${status}`,
|
||||
data: result.order,
|
||||
stockReduction,
|
||||
historyId: result.history.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ Update order status error:", error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @desc Get order status history
|
||||
* @route GET /api/admin/orders/:orderId/history
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.getOrderStatusHistory = async (req, res, next) => {
|
||||
try {
|
||||
const { orderId } = req.params;
|
||||
|
||||
const history = await prisma.orderStatusHistory.findMany({
|
||||
where: { orderId },
|
||||
include: {
|
||||
admin: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: history,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Get status history error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @desc Bulk update order status (Admin)
|
||||
* @route PUT /api/admin/orders/bulk-status
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.bulkUpdateOrderStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { orderIds, status, trackingNumbers } = req.body;
|
||||
|
||||
if (!orderIds || !Array.isArray(orderIds) || orderIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Order IDs array is required',
|
||||
});
|
||||
}
|
||||
|
||||
const updateData = { status };
|
||||
|
||||
if (status === 'SHIPPED') {
|
||||
updateData.shippedAt = new Date();
|
||||
}
|
||||
|
||||
if (status === 'DELIVERED') {
|
||||
updateData.deliveredAt = new Date();
|
||||
}
|
||||
|
||||
// Update all orders
|
||||
const updates = await Promise.all(
|
||||
orderIds.map(async (orderId, index) => {
|
||||
const data = { ...updateData };
|
||||
|
||||
if (trackingNumbers && trackingNumbers[index]) {
|
||||
data.trackingNumber = trackingNumbers[index];
|
||||
}
|
||||
|
||||
return prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: `${updates.length} orders updated to ${status}`,
|
||||
data: updates,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Bulk update error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Get delivery estimation for pincode
|
||||
* @route POST /api/delivery/estimate
|
||||
* @access Public
|
||||
*/
|
||||
exports.getDeliveryEstimate = async (req, res, next) => {
|
||||
try {
|
||||
const { pincode, shippingMethod = 'STANDARD' } = req.body;
|
||||
|
||||
if (!pincode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Pincode is required',
|
||||
});
|
||||
}
|
||||
|
||||
const estimation = await getDeliveryEstimation(pincode, shippingMethod);
|
||||
|
||||
res.status(200).json(estimation);
|
||||
} catch (error) {
|
||||
console.error('Get delivery estimate error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Generate order timeline
|
||||
*/
|
||||
function generateOrderTimeline(order, deliveryEstimation) {
|
||||
const timeline = [];
|
||||
|
||||
timeline.push({
|
||||
status: 'PLACED',
|
||||
title: 'Order Placed',
|
||||
description: 'Your order has been placed successfully',
|
||||
timestamp: order.createdAt,
|
||||
completed: true,
|
||||
icon: '🛒',
|
||||
});
|
||||
|
||||
if (order.status !== 'PENDING') {
|
||||
timeline.push({
|
||||
status: 'CONFIRMED',
|
||||
title: 'Order Confirmed',
|
||||
description: 'Your order has been confirmed',
|
||||
timestamp: order.createdAt,
|
||||
completed: true,
|
||||
icon: '✅',
|
||||
});
|
||||
} else {
|
||||
timeline.push({
|
||||
status: 'CONFIRMED',
|
||||
title: 'Order Confirmation',
|
||||
description: 'Awaiting order confirmation',
|
||||
completed: false,
|
||||
icon: '⏳',
|
||||
});
|
||||
}
|
||||
|
||||
if (['PROCESSING', 'SHIPPED', 'DELIVERED'].includes(order.status)) {
|
||||
timeline.push({
|
||||
status: 'PROCESSING',
|
||||
title: 'Processing',
|
||||
description: 'Your order is being processed',
|
||||
timestamp: order.createdAt,
|
||||
completed: true,
|
||||
icon: '📦',
|
||||
});
|
||||
} else if (order.status === 'CONFIRMED') {
|
||||
timeline.push({
|
||||
status: 'PROCESSING',
|
||||
title: 'Processing',
|
||||
description: 'Order will be processed soon',
|
||||
completed: false,
|
||||
icon: '⏳',
|
||||
});
|
||||
}
|
||||
|
||||
if (['SHIPPED', 'DELIVERED'].includes(order.status)) {
|
||||
timeline.push({
|
||||
status: 'SHIPPED',
|
||||
title: 'Shipped',
|
||||
description: order.trackingNumber
|
||||
? `Tracking: ${order.trackingNumber}`
|
||||
: 'Your order has been shipped',
|
||||
timestamp: order.shippedAt || new Date(),
|
||||
completed: true,
|
||||
icon: '🚚',
|
||||
});
|
||||
} else if (['CONFIRMED', 'PROCESSING'].includes(order.status)) {
|
||||
timeline.push({
|
||||
status: 'SHIPPED',
|
||||
title: 'Shipping',
|
||||
description: 'Your order will be shipped soon',
|
||||
completed: false,
|
||||
icon: '⏳',
|
||||
});
|
||||
}
|
||||
|
||||
if (order.status === 'DELIVERED') {
|
||||
timeline.push({
|
||||
status: 'OUT_FOR_DELIVERY',
|
||||
title: 'Out for Delivery',
|
||||
description: 'Your order is out for delivery',
|
||||
timestamp: order.deliveredAt || new Date(),
|
||||
completed: true,
|
||||
icon: '🛵',
|
||||
});
|
||||
} else if (order.status === 'SHIPPED') {
|
||||
timeline.push({
|
||||
status: 'OUT_FOR_DELIVERY',
|
||||
title: 'Out for Delivery',
|
||||
description: 'Will be out for delivery soon',
|
||||
completed: false,
|
||||
icon: '⏳',
|
||||
});
|
||||
}
|
||||
|
||||
if (order.status === 'DELIVERED') {
|
||||
timeline.push({
|
||||
status: 'DELIVERED',
|
||||
title: 'Delivered',
|
||||
description: 'Your order has been delivered',
|
||||
timestamp: order.deliveredAt,
|
||||
completed: true,
|
||||
icon: '🎉',
|
||||
});
|
||||
} else {
|
||||
const estimatedDate = deliveryEstimation?.estimatedDelivery?.formatted;
|
||||
timeline.push({
|
||||
status: 'DELIVERED',
|
||||
title: 'Delivery',
|
||||
description: estimatedDate
|
||||
? `Expected by ${estimatedDate}`
|
||||
: 'Estimated delivery date will be updated',
|
||||
completed: false,
|
||||
icon: '📍',
|
||||
});
|
||||
}
|
||||
|
||||
return timeline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current status details
|
||||
*/
|
||||
function getStatusDetails(order) {
|
||||
const statusMap = {
|
||||
PENDING: {
|
||||
label: 'Order Pending',
|
||||
description: 'Your order is awaiting confirmation',
|
||||
color: 'yellow',
|
||||
progress: 10,
|
||||
},
|
||||
CONFIRMED: {
|
||||
label: 'Order Confirmed',
|
||||
description: 'Your order has been confirmed and will be processed soon',
|
||||
color: 'blue',
|
||||
progress: 25,
|
||||
},
|
||||
PROCESSING: {
|
||||
label: 'Processing',
|
||||
description: 'Your order is being prepared for shipment',
|
||||
color: 'blue',
|
||||
progress: 50,
|
||||
},
|
||||
SHIPPED: {
|
||||
label: 'Shipped',
|
||||
description: 'Your order is on the way',
|
||||
color: 'purple',
|
||||
progress: 75,
|
||||
},
|
||||
DELIVERED: {
|
||||
label: 'Delivered',
|
||||
description: 'Your order has been delivered',
|
||||
color: 'green',
|
||||
progress: 100,
|
||||
},
|
||||
CANCELLED: {
|
||||
label: 'Cancelled',
|
||||
description: 'Your order has been cancelled',
|
||||
color: 'red',
|
||||
progress: 0,
|
||||
},
|
||||
RETURN_REQUESTED: {
|
||||
label: 'Return Requested',
|
||||
description: 'Return request is being processed',
|
||||
color: 'orange',
|
||||
progress: 100,
|
||||
},
|
||||
REFUNDED: {
|
||||
label: 'Refunded',
|
||||
description: 'Your order has been refunded',
|
||||
color: 'green',
|
||||
progress: 100,
|
||||
},
|
||||
};
|
||||
|
||||
return statusMap[order.status] || statusMap.PENDING;
|
||||
}
|
||||
430
src/controllers/payment/paytmController.js
Normal file
430
src/controllers/payment/paytmController.js
Normal file
@@ -0,0 +1,430 @@
|
||||
// controllers/paytmController.js
|
||||
const { prisma } = require('../../config/database');
|
||||
const {
|
||||
initiateTransaction,
|
||||
checkTransactionStatus,
|
||||
verifyChecksum,
|
||||
processRefund,
|
||||
PaytmConfig,
|
||||
} = require('../../utils/paytm');
|
||||
|
||||
/**
|
||||
* @desc Initiate Paytm Payment
|
||||
* @route POST /api/payments/paytm/initiate
|
||||
* @access Private
|
||||
*/
|
||||
exports.initiatePayment = async (req, res, next) => {
|
||||
try {
|
||||
const { orderId, amount } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
// Validate order
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Order not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Verify order belongs to user
|
||||
if (order.userId !== userId) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Unauthorized access to order',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if order is already paid
|
||||
if (order.paymentStatus === 'PAID') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Order is already paid',
|
||||
});
|
||||
}
|
||||
|
||||
// Validate amount
|
||||
const orderAmount = parseFloat(order.totalAmount);
|
||||
if (parseFloat(amount) !== orderAmount) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Payment amount mismatch',
|
||||
});
|
||||
}
|
||||
|
||||
// Initiate Paytm transaction
|
||||
const paytmResponse = await initiateTransaction(
|
||||
order.orderNumber,
|
||||
orderAmount,
|
||||
userId,
|
||||
order.user.email,
|
||||
order.user.phone || '9999999999'
|
||||
);
|
||||
|
||||
if (!paytmResponse.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Failed to initiate payment',
|
||||
error: paytmResponse,
|
||||
});
|
||||
}
|
||||
|
||||
// Update order with payment details
|
||||
await prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
paymentMethod: 'PAYTM',
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Payment initiated successfully',
|
||||
data: {
|
||||
txnToken: paytmResponse.txnToken,
|
||||
orderId: order.orderNumber,
|
||||
amount: orderAmount,
|
||||
mid: PaytmConfig.mid,
|
||||
website: PaytmConfig.website,
|
||||
callbackUrl: PaytmConfig.callbackUrl,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Paytm initiate payment error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Paytm Payment Callback
|
||||
* @route POST /api/payments/paytm/callback
|
||||
* @access Public (Called by Paytm)
|
||||
*/
|
||||
exports.paymentCallback = async (req, res, next) => {
|
||||
try {
|
||||
const paytmChecksum = req.body.CHECKSUMHASH;
|
||||
delete req.body.CHECKSUMHASH;
|
||||
|
||||
// Verify checksum
|
||||
const isValidChecksum = await verifyChecksum(
|
||||
req.body,
|
||||
PaytmConfig.key,
|
||||
paytmChecksum
|
||||
);
|
||||
|
||||
if (!isValidChecksum) {
|
||||
console.error('Invalid checksum received from Paytm');
|
||||
return res.redirect(`${process.env.FRONTEND_URL}/payment/failed?reason=invalid_checksum`);
|
||||
}
|
||||
|
||||
const {
|
||||
ORDERID,
|
||||
TXNID,
|
||||
TXNAMOUNT,
|
||||
STATUS,
|
||||
RESPCODE,
|
||||
RESPMSG,
|
||||
TXNDATE,
|
||||
BANKTXNID,
|
||||
GATEWAYNAME,
|
||||
} = req.body;
|
||||
|
||||
console.log('Paytm callback received:', {
|
||||
orderId: ORDERID,
|
||||
txnId: TXNID,
|
||||
status: STATUS,
|
||||
amount: TXNAMOUNT,
|
||||
});
|
||||
|
||||
// Find order by order number
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { orderNumber: ORDERID },
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
console.error('Order not found:', ORDERID);
|
||||
return res.redirect(`${process.env.FRONTEND_URL}/payment/failed?reason=order_not_found`);
|
||||
}
|
||||
|
||||
// Update order based on payment status
|
||||
if (STATUS === 'TXN_SUCCESS') {
|
||||
await prisma.order.update({
|
||||
where: { id: order.id },
|
||||
data: {
|
||||
paymentStatus: 'PAID',
|
||||
paymentId: TXNID,
|
||||
paymentMethod: `PAYTM - ${GATEWAYNAME || 'Gateway'}`,
|
||||
status: 'CONFIRMED',
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Redirect to success page
|
||||
return res.redirect(
|
||||
`${process.env.FRONTEND_URL}/payment/success?orderId=${order.id}&txnId=${TXNID}`
|
||||
);
|
||||
} else if (STATUS === 'TXN_FAILURE') {
|
||||
await prisma.order.update({
|
||||
where: { id: order.id },
|
||||
data: {
|
||||
paymentStatus: 'FAILED',
|
||||
paymentId: TXNID,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return res.redirect(
|
||||
`${process.env.FRONTEND_URL}/payment/failed?orderId=${order.id}&reason=${RESPMSG}`
|
||||
);
|
||||
} else {
|
||||
// Pending or other status
|
||||
return res.redirect(
|
||||
`${process.env.FRONTEND_URL}/payment/pending?orderId=${order.id}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Paytm callback error:', error);
|
||||
return res.redirect(
|
||||
`${process.env.FRONTEND_URL}/payment/failed?reason=processing_error`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Check Payment Status
|
||||
* @route GET /api/payments/paytm/status/:orderId
|
||||
* @access Private
|
||||
*/
|
||||
exports.checkPaymentStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { orderId } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
// Get order
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Order not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Verify order belongs to user
|
||||
if (order.userId !== userId) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Unauthorized access',
|
||||
});
|
||||
}
|
||||
|
||||
// Check status with Paytm
|
||||
const statusResponse = await checkTransactionStatus(order.orderNumber);
|
||||
|
||||
if (statusResponse.body && statusResponse.body.resultInfo) {
|
||||
const { resultStatus } = statusResponse.body.resultInfo;
|
||||
const txnInfo = statusResponse.body;
|
||||
|
||||
// Update order if status changed
|
||||
if (resultStatus === 'TXN_SUCCESS' && order.paymentStatus !== 'PAID') {
|
||||
await prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
paymentStatus: 'PAID',
|
||||
paymentId: txnInfo.txnId,
|
||||
status: 'CONFIRMED',
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
orderId: order.id,
|
||||
orderNumber: order.orderNumber,
|
||||
paymentStatus: order.paymentStatus,
|
||||
paytmStatus: resultStatus,
|
||||
txnInfo: txnInfo,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
orderId: order.id,
|
||||
orderNumber: order.orderNumber,
|
||||
paymentStatus: order.paymentStatus,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Check payment status error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Process Refund
|
||||
* @route POST /api/payments/paytm/refund
|
||||
* @access Private/Admin
|
||||
*/
|
||||
exports.processRefund = async (req, res, next) => {
|
||||
try {
|
||||
const { orderId, amount, reason } = req.body;
|
||||
|
||||
// Get order
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Order not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Validate refund amount
|
||||
const orderAmount = parseFloat(order.totalAmount);
|
||||
const refundAmount = parseFloat(amount);
|
||||
|
||||
if (refundAmount > orderAmount) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Refund amount cannot exceed order amount',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if order is paid
|
||||
if (order.paymentStatus !== 'PAID') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Cannot refund unpaid order',
|
||||
});
|
||||
}
|
||||
|
||||
// Generate unique refund ID
|
||||
const refId = `REFUND_${order.orderNumber}_${Date.now()}`;
|
||||
|
||||
// Process refund with Paytm
|
||||
const refundResponse = await processRefund(
|
||||
order.orderNumber,
|
||||
refId,
|
||||
order.paymentId,
|
||||
refundAmount
|
||||
);
|
||||
|
||||
if (refundResponse.body && refundResponse.body.resultInfo) {
|
||||
const { resultStatus, resultMsg } = refundResponse.body.resultInfo;
|
||||
|
||||
if (resultStatus === 'TXN_SUCCESS' || resultStatus === 'PENDING') {
|
||||
// Update order
|
||||
const isFullRefund = refundAmount >= orderAmount;
|
||||
|
||||
await prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
paymentStatus: isFullRefund ? 'REFUNDED' : 'PARTIALLY_REFUNDED',
|
||||
status: isFullRefund ? 'REFUNDED' : order.status,
|
||||
returnStatus: 'COMPLETED',
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Refund processed successfully',
|
||||
data: {
|
||||
refId,
|
||||
status: resultStatus,
|
||||
message: resultMsg,
|
||||
amount: refundAmount,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Refund failed: ${resultMsg}`,
|
||||
data: refundResponse,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to process refund',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Process refund error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Get Payment Details
|
||||
* @route GET /api/payments/paytm/:orderId
|
||||
* @access Private
|
||||
*/
|
||||
exports.getPaymentDetails = async (req, res, next) => {
|
||||
try {
|
||||
const { orderId } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Order not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Verify access
|
||||
if (order.userId !== userId && req.user.role !== 'ADMIN') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Unauthorized access',
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
orderId: order.id,
|
||||
orderNumber: order.orderNumber,
|
||||
totalAmount: order.totalAmount,
|
||||
paymentStatus: order.paymentStatus,
|
||||
paymentMethod: order.paymentMethod,
|
||||
paymentId: order.paymentId,
|
||||
createdAt: order.createdAt,
|
||||
user: order.user,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get payment details error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
811
src/controllers/products/productController.js
Normal file
811
src/controllers/products/productController.js
Normal file
@@ -0,0 +1,811 @@
|
||||
const Product = require('../../models/mongodb/Product');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Get all products
|
||||
exports.getAllProducts = async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
category,
|
||||
brand,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
search,
|
||||
sort = 'createdAt',
|
||||
order = 'desc',
|
||||
} = req.query;
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
let query = { status: 'active' };
|
||||
|
||||
if (category) query.category = category;
|
||||
if (brand) query.brand = brand;
|
||||
if (minPrice || maxPrice) {
|
||||
query.basePrice = {};
|
||||
if (minPrice) query.basePrice.$gte = parseFloat(minPrice);
|
||||
if (maxPrice) query.basePrice.$lte = parseFloat(maxPrice);
|
||||
}
|
||||
if (search) query.$text = { $search: search };
|
||||
|
||||
const sortOptions = {};
|
||||
sortOptions[sort] = order === 'desc' ? -1 : 1;
|
||||
|
||||
const [products, total] = await Promise.all([
|
||||
Product.find(query)
|
||||
.sort(sortOptions)
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit))
|
||||
.lean(),
|
||||
Product.countDocuments(query),
|
||||
]);
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// data: {
|
||||
// products,
|
||||
// pagination: {
|
||||
// page: parseInt(page),
|
||||
// limit: parseInt(limit),
|
||||
// total,
|
||||
// pages: Math.ceil(total / limit),
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Products fetched successfully',
|
||||
data: {
|
||||
products,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Get single product by slug
|
||||
exports.getProductBySlug = async (req, res, next) => {
|
||||
try {
|
||||
const product = await Product.findBySlug(req.params.slug);
|
||||
// if (!product)
|
||||
// return res
|
||||
// .status(404)
|
||||
// .json({ success: false, message: 'Product not found' });
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Product not found',
|
||||
});
|
||||
}
|
||||
|
||||
await product.incrementViewCount();
|
||||
|
||||
// res.json({ success: true, data: { product } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Product fetched successfully',
|
||||
data: { product },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Create new product
|
||||
exports.createProduct = async (req, res, next) => {
|
||||
try {
|
||||
const product = new Product(req.body);
|
||||
await product.save();
|
||||
|
||||
// res.status(201).json({
|
||||
// success: true,
|
||||
// message: 'Product created successfully',
|
||||
// data: { product },
|
||||
// });
|
||||
return res.status(201).json({
|
||||
statusCode: 201,
|
||||
status: true,
|
||||
message: 'Product created successfully',
|
||||
data: { product },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// Update product
|
||||
exports.updateProduct = async (req, res, next) => {
|
||||
try {
|
||||
const product = await Product.findByIdAndUpdate(req.params.id, req.body, {
|
||||
new: true,
|
||||
runValidators: true,
|
||||
});
|
||||
|
||||
// if (!product)
|
||||
// return res
|
||||
// .status(404)
|
||||
// .json({ success: false, message: 'Product not found' });
|
||||
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Product not found',
|
||||
});
|
||||
}
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// message: 'Product updated successfully',
|
||||
// data: { product },
|
||||
// });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Product updated successfully',
|
||||
data: { product },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete product
|
||||
exports.deleteProduct = async (req, res, next) => {
|
||||
try {
|
||||
const product = await Product.findByIdAndDelete(req.params.id);
|
||||
// if (!product)
|
||||
// return res
|
||||
// .status(404)
|
||||
// .json({ success: false, message: 'Product not found' });
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Product not found',
|
||||
});
|
||||
}
|
||||
|
||||
// res.json({ success: true, message: 'Product deleted successfully' });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Product deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Search products
|
||||
exports.searchProducts = async (req, res, next) => {
|
||||
try {
|
||||
const { query } = req.params;
|
||||
const {
|
||||
category,
|
||||
brand,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
limit = 20,
|
||||
skip = 0,
|
||||
} = req.query;
|
||||
|
||||
const products = await Product.searchProducts(query, {
|
||||
category,
|
||||
brand,
|
||||
minPrice: minPrice ? parseFloat(minPrice) : undefined,
|
||||
maxPrice: maxPrice ? parseFloat(maxPrice) : undefined,
|
||||
limit: parseInt(limit),
|
||||
skip: parseInt(skip),
|
||||
});
|
||||
|
||||
// res.json({ success: true, data: { products } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Products searched successfully',
|
||||
data: { products },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Get products by category
|
||||
// exports.getProductsByCategory = async (req, res, next) => {
|
||||
// try {
|
||||
// const { category } = req.params;
|
||||
// const { limit = 20, skip = 0 } = req.query;
|
||||
|
||||
// const products = await Product.findByCategory(
|
||||
// category,
|
||||
// parseInt(limit),
|
||||
// parseInt(skip)
|
||||
// );
|
||||
|
||||
// // res.json({ success: true, data: { products } });
|
||||
// return res.status(200).json({
|
||||
// statusCode: 200,
|
||||
// status: true,
|
||||
// message: 'Products fetched by category',
|
||||
// data: { products },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// exports.getProductsByCategory = async (req, res, next) => {
|
||||
// try {
|
||||
// const { category: slug } = req.params;
|
||||
// const { limit = 20, skip = 0 } = req.query;
|
||||
|
||||
// // 1️⃣ Get category from PostgreSQL
|
||||
// const categoryDoc = await prisma.category.findFirst({
|
||||
// where: { slug, isActive: true },
|
||||
// });
|
||||
|
||||
// if (!categoryDoc) {
|
||||
// return res.status(404).json({
|
||||
// statusCode: 404,
|
||||
// status: false,
|
||||
// message: "Category not found",
|
||||
// data: null
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 2️⃣ Get products from MongoDB using the PostgreSQL category ID
|
||||
// const products = await Product.findByCategory(
|
||||
// categoryDoc.id, // <-- MongoDB stores this as the `category` field
|
||||
// parseInt(limit),
|
||||
// parseInt(skip)
|
||||
// );
|
||||
|
||||
// return res.status(200).json({
|
||||
// statusCode: 200,
|
||||
// status: true,
|
||||
// message: "Products fetched by category",
|
||||
// data: { products },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// exports.getProductsByCategory = async (req, res, next) => {
|
||||
// try {
|
||||
// const { categorySlug } = req.params;
|
||||
// const limit = parseInt(req.query.limit) || 20;
|
||||
// const skip = parseInt(req.query.skip) || 0;
|
||||
|
||||
// // 1️⃣ Find category from PostgreSQL by slug
|
||||
// const categoryDoc = await prisma.category.findFirst({
|
||||
// where: { slug: categorySlug, isActive: true },
|
||||
// });
|
||||
|
||||
// if (!categoryDoc) {
|
||||
// return res.status(404).json({
|
||||
// statusCode: 404,
|
||||
// status: false,
|
||||
// message: 'Category not found',
|
||||
// data: null,
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 2️⃣ Find products from MongoDB using the PostgreSQL category ID
|
||||
// const query = { category: categoryDoc.id }; // MongoDB field must store PostgreSQL category id
|
||||
// const [products, totalCount] = await Promise.all([
|
||||
// Product.find(query)
|
||||
// .skip(skip)
|
||||
// .limit(limit)
|
||||
// .sort({ createdAt: -1 }) // optional: newest first
|
||||
// .lean(), // optional: return plain JS objects
|
||||
// Product.countDocuments(query),
|
||||
// ]);
|
||||
|
||||
// return res.status(200).json({
|
||||
// statusCode: 200,
|
||||
// status: true,
|
||||
// message: 'Products fetched by category',
|
||||
// data: { products, totalCount },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
exports.getProductsByCategory = async (req, res, next) => {
|
||||
try {
|
||||
const { categorySlug } = req.params;
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
const skip = parseInt(req.query.skip) || 0;
|
||||
|
||||
// 1️⃣ Find category from PostgreSQL
|
||||
const categoryDoc = await prisma.category.findFirst({
|
||||
where: { slug: categorySlug, isActive: true },
|
||||
});
|
||||
|
||||
if (!categoryDoc) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Category not found',
|
||||
data: { products: [], totalCount: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
// 2️⃣ Find all child categories recursively (if any)
|
||||
const allCategoryIds = [categoryDoc.id];
|
||||
|
||||
const getChildCategoryIds = async (parentId) => {
|
||||
const children = await prisma.category.findMany({
|
||||
where: { parentId, isActive: true },
|
||||
select: { id: true },
|
||||
});
|
||||
for (const child of children) {
|
||||
allCategoryIds.push(child.id);
|
||||
await getChildCategoryIds(child.id);
|
||||
}
|
||||
};
|
||||
|
||||
await getChildCategoryIds(categoryDoc.id);
|
||||
|
||||
// 3️⃣ Find products in MongoDB with category in allCategoryIds
|
||||
const query = { category: { $in: allCategoryIds } };
|
||||
const [products, totalCount] = await Promise.all([
|
||||
Product.find(query)
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.sort({ createdAt: -1 })
|
||||
.lean(),
|
||||
Product.countDocuments(query),
|
||||
]);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Products fetched by category',
|
||||
data: { products, totalCount },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Get featured products
|
||||
exports.getFeaturedProducts = async (req, res, next) => {
|
||||
try {
|
||||
const { limit = 10 } = req.query;
|
||||
|
||||
const products = await Product.find({ status: 'active', isFeatured: true })
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(parseInt(limit))
|
||||
.lean();
|
||||
|
||||
// res.json({ success: true, data: { products } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Featured products fetched successfully',
|
||||
data: { products },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Get all unique categories
|
||||
// Get all categories that have products
|
||||
exports.getAllCategories = async (req, res) => {
|
||||
try {
|
||||
// Get unique category IDs from MongoDB products
|
||||
const categoryIds = await Product.distinct('category', {
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
console.log('Category IDs from products:', categoryIds);
|
||||
|
||||
if (!categoryIds || categoryIds.length === 0) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'No categories found',
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch category details from PostgreSQL
|
||||
const categories = await prisma.category.findMany({
|
||||
where: {
|
||||
id: { in: categoryIds },
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
image: true,
|
||||
description: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (!categories || categories.length === 0) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'No matching categories found in database',
|
||||
});
|
||||
}
|
||||
|
||||
// Get product count for each category
|
||||
const productCounts = await Product.aggregate([
|
||||
{
|
||||
$match: {
|
||||
category: { $in: categoryIds },
|
||||
status: 'active',
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$category',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Add product count to each category
|
||||
const categoriesWithCount = categories.map(cat => ({
|
||||
...cat,
|
||||
productCount: productCounts.find(pc => pc._id === cat.id)?.count || 0,
|
||||
}));
|
||||
|
||||
console.log('Categories with counts:', categoriesWithCount);
|
||||
|
||||
return res.json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
data: categoriesWithCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: 'Server error',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.debugMissingCategories = async (req, res) => {
|
||||
try {
|
||||
const categoryIds = await Product.distinct('category');
|
||||
|
||||
const existingCategories = await prisma.category.findMany({
|
||||
where: { id: { in: categoryIds } },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
const existingIds = existingCategories.map(c => c.id);
|
||||
const missingIds = categoryIds.filter(id => !existingIds.includes(id));
|
||||
|
||||
return res.json({
|
||||
totalProductCategories: categoryIds.length,
|
||||
existingInDB: existingCategories,
|
||||
missingFromDB: missingIds,
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// exports.getUserCategoryHierarchy = async (req, res, next) => {
|
||||
// try {
|
||||
// const categories = await prisma.category.findMany({
|
||||
// where: {
|
||||
// isActive: true,
|
||||
// // isVisible: true,
|
||||
// },
|
||||
// select: {
|
||||
// id: true,
|
||||
// name: true,
|
||||
// slug: true,
|
||||
// parentId: true,
|
||||
// },
|
||||
// orderBy: { name: 'asc' },
|
||||
// });
|
||||
|
||||
// const lookup = {};
|
||||
// categories.forEach(cat => {
|
||||
// lookup[cat.id] = { ...cat, children: [] };
|
||||
// });
|
||||
|
||||
// const hierarchy = [];
|
||||
|
||||
// categories.forEach(cat => {
|
||||
// if (cat.parentId && lookup[cat.parentId]) {
|
||||
// lookup[cat.parentId].children.push(lookup[cat.id]);
|
||||
// } else {
|
||||
// hierarchy.push(lookup[cat.id]);
|
||||
// }
|
||||
// });
|
||||
|
||||
// res.status(200).json({
|
||||
// statusCode: 200,
|
||||
// status: true,
|
||||
// message: 'Category tree fetched successfully',
|
||||
// data: hierarchy,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
exports.getUserCategoryHierarchy = async (req, res, next) => {
|
||||
try {
|
||||
const categories = await prisma.category.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
parentId: true,
|
||||
sequence: true,
|
||||
},
|
||||
orderBy: {
|
||||
sequence: 'asc', // ✅ IMPORTANT
|
||||
},
|
||||
});
|
||||
|
||||
const lookup = {};
|
||||
categories.forEach(cat => {
|
||||
lookup[cat.id] = { ...cat, children: [] };
|
||||
});
|
||||
|
||||
const hierarchy = [];
|
||||
|
||||
categories.forEach(cat => {
|
||||
if (cat.parentId && lookup[cat.parentId]) {
|
||||
lookup[cat.parentId].children.push(lookup[cat.id]);
|
||||
} else {
|
||||
hierarchy.push(lookup[cat.id]);
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Recursive sort by sequence
|
||||
const sortTree = nodes => {
|
||||
nodes.sort((a, b) => a.sequence - b.sequence);
|
||||
nodes.forEach(node => {
|
||||
if (node.children.length) {
|
||||
sortTree(node.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
sortTree(hierarchy);
|
||||
|
||||
res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Category tree fetched successfully',
|
||||
data: hierarchy,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Get all available colors for a category
|
||||
// const Product = require('../../models/mongodb/Product');
|
||||
// controllers/products/productController.js
|
||||
exports.getCategoryColors = async (req, res) => {
|
||||
try {
|
||||
const { categorySlug } = req.params;
|
||||
|
||||
// 1️⃣ Find category ID from Prisma (PostgreSQL)
|
||||
const category = await prisma.category.findFirst({
|
||||
where: { slug: categorySlug },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
return res.json({
|
||||
status: false,
|
||||
message: 'Category not found',
|
||||
data: [],
|
||||
});
|
||||
}
|
||||
|
||||
// 2️⃣ Fetch products from MongoDB using category ID
|
||||
const products = await Product.find(
|
||||
{ category: category.id, status: 'active' },
|
||||
{ variants: 1, images: 1 }
|
||||
);
|
||||
|
||||
if (!products.length) {
|
||||
return res.json({
|
||||
status: true,
|
||||
message: 'No products found in this category',
|
||||
data: [],
|
||||
});
|
||||
}
|
||||
|
||||
// 3️⃣ Extract unique colors
|
||||
const colorMap = new Map();
|
||||
|
||||
products.forEach((product) => {
|
||||
if (product.variants?.length) {
|
||||
product.variants.forEach((variant) => {
|
||||
if (!variant.color) return; // skip if no color
|
||||
|
||||
const key = variant.color.toLowerCase();
|
||||
|
||||
if (!colorMap.has(key)) {
|
||||
colorMap.set(key, {
|
||||
name: variant.color,
|
||||
slug: key,
|
||||
image: variant.images?.[0] || product.images?.primary || null,
|
||||
bg: getColorBg(key), // Optional: your helper to generate color background
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 4️⃣ Prepare response
|
||||
const colors = Array.from(colorMap.values());
|
||||
|
||||
const message =
|
||||
colors.length === 0
|
||||
? 'No color variants available for this category'
|
||||
: colors.length === 1
|
||||
? `Only 1 color available: ${colors[0].name}`
|
||||
: 'Category colors fetched successfully';
|
||||
|
||||
res.json({
|
||||
status: true,
|
||||
message,
|
||||
data: colors,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Category Colors Error:', error);
|
||||
res.status(500).json({
|
||||
status: false,
|
||||
message: 'Failed to fetch category colors',
|
||||
data: [],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Helper function to assign background color (optional)
|
||||
function getColorBg(colorKey) {
|
||||
const defaultBg = '#F5F5F5';
|
||||
const colorMap = {
|
||||
white: '#FFFFFF',
|
||||
black: '#000000',
|
||||
red: '#FF0000',
|
||||
blue: '#007BFF',
|
||||
green: '#28A745',
|
||||
yellow: '#FFC107',
|
||||
// Add more colors if needed
|
||||
};
|
||||
|
||||
return colorMap[colorKey] || defaultBg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* helpers */
|
||||
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);
|
||||
|
||||
function getColorBg(color) {
|
||||
const map = {
|
||||
white: '#F5F5F5',
|
||||
black: '#E5E5E5',
|
||||
red: '#FDE6E6',
|
||||
blue: '#E6F0FD',
|
||||
green: '#E6FDEB',
|
||||
pink: '#FDE6F1',
|
||||
maroon: '#FDE6EB',
|
||||
beige: '#FDEDE6',
|
||||
grey: '#EFEFEF',
|
||||
gray: '#EFEFEF',
|
||||
brown: '#EEE6D8',
|
||||
yellow: '#FFF7CC',
|
||||
orange: '#FFE5CC',
|
||||
purple: '#F1E6FD',
|
||||
};
|
||||
return map[color] || '#F3F4F6';
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc Get New Arrivals
|
||||
* @route GET /api/products/new-arrivals
|
||||
* @access Public
|
||||
*/
|
||||
exports.getNewArrivals = async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
|
||||
// Fetch latest active products
|
||||
const products = await Product.find({ status: 'active' })
|
||||
.sort({ createdAt: -1 }) // newest first
|
||||
.limit(limit);
|
||||
|
||||
res.status(200).json({ success: true, data: products });
|
||||
} catch (error) {
|
||||
console.error('Error fetching new arrivals:', error);
|
||||
res.status(500).json({ success: false, message: 'Server Error' });
|
||||
}
|
||||
};
|
||||
|
||||
// controllers/products/productController.js
|
||||
|
||||
// @desc Get Most Loved / Discounted Products
|
||||
// @route GET /api/products/most-loved
|
||||
// @access Public
|
||||
// Get Most Loved Products (based on purchaseCount or discounted items)
|
||||
// @desc Get Most Loved / Discounted Products
|
||||
// @route GET /api/products/most-loved
|
||||
// @access Public
|
||||
exports.getMostLovedProducts = async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 8;
|
||||
|
||||
// Fetch products with discount or highest purchase count
|
||||
let products = await Product.find({
|
||||
status: 'active',
|
||||
$or: [
|
||||
{ compareAtPrice: { $gt: 0 } }, // discounted
|
||||
{ purchaseCount: { $gt: 0 } }, // most loved
|
||||
],
|
||||
})
|
||||
.sort({ purchaseCount: -1, createdAt: -1 })
|
||||
.limit(limit);
|
||||
|
||||
// If no products match, return latest active products as fallback
|
||||
if (!products.length) {
|
||||
products = await Product.find({ status: 'active' })
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(limit);
|
||||
|
||||
return res.status(200).json({
|
||||
status: true,
|
||||
message: 'Fallback products fetched successfully',
|
||||
data: { products },
|
||||
});
|
||||
}
|
||||
|
||||
// Return the main products
|
||||
return res.status(200).json({
|
||||
status: true,
|
||||
message: 'Products fetched successfully',
|
||||
data: { products },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching most loved products:', error);
|
||||
return res.status(500).json({ status: false, message: 'Server Error' });
|
||||
}
|
||||
};
|
||||
345
src/controllers/products/recommendation.js
Normal file
345
src/controllers/products/recommendation.js
Normal file
@@ -0,0 +1,345 @@
|
||||
// controllers/products/recommendationController.js
|
||||
|
||||
const Product = require('../../models/mongodb/Product');
|
||||
const { prisma } = require('../../config/database');
|
||||
|
||||
/**
|
||||
* @desc Get recommendations for a product
|
||||
* @route GET /api/products/:slug/recommendations
|
||||
* @access Public
|
||||
*/
|
||||
const getProductRecommendations = async (req, res, next) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const { limit = 12 } = req.query;
|
||||
|
||||
// Get the current product
|
||||
const currentProduct = await Product.findOne({ slug, status: 'active' });
|
||||
|
||||
if (!currentProduct) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Product not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Build recommendation query
|
||||
const recommendations = await getRecommendedProducts(
|
||||
currentProduct,
|
||||
parseInt(limit)
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: recommendations.length,
|
||||
data: recommendations,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get recommendations error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Get "Customers also bought" products
|
||||
* @route GET /api/products/:slug/also-bought
|
||||
* @access Public
|
||||
*/
|
||||
const getAlsoBoughtProducts = async (req, res, next) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const { limit = 8 } = req.query;
|
||||
|
||||
const currentProduct = await Product.findOne({ slug, status: 'active' });
|
||||
|
||||
if (!currentProduct) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Product not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Get products frequently bought together
|
||||
const alsoBought = await getFrequentlyBoughtTogether(
|
||||
currentProduct._id.toString(),
|
||||
parseInt(limit)
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: alsoBought.length,
|
||||
data: alsoBought,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get also bought error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Get similar products
|
||||
* @route GET /api/products/:slug/similar
|
||||
* @access Public
|
||||
*/
|
||||
const getSimilarProducts = async (req, res, next) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const { limit = 10 } = req.query;
|
||||
|
||||
const currentProduct = await Product.findOne({ slug, status: 'active' });
|
||||
|
||||
if (!currentProduct) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Product not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Get similar products based on attributes
|
||||
const similar = await getSimilarProductsByAttributes(
|
||||
currentProduct,
|
||||
parseInt(limit)
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: similar.length,
|
||||
data: similar,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get similar products error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Get personalized recommendations for user
|
||||
* @route GET /api/products/recommendations/personalized
|
||||
* @access Private
|
||||
*/
|
||||
const getPersonalizedRecommendations = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
const { limit = 20 } = req.query;
|
||||
|
||||
if (!userId) {
|
||||
// Return popular products for non-authenticated users
|
||||
const popularProducts = await Product.find({ status: 'active' })
|
||||
.sort({ purchaseCount: -1, viewCount: -1 })
|
||||
.limit(parseInt(limit));
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
count: popularProducts.length,
|
||||
data: popularProducts,
|
||||
});
|
||||
}
|
||||
|
||||
// Get user's purchase history
|
||||
const userOrders = await prisma.order.findMany({
|
||||
where: {
|
||||
userId,
|
||||
status: 'DELIVERED',
|
||||
},
|
||||
include: {
|
||||
items: true,
|
||||
},
|
||||
take: 10,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Get user's wishlist
|
||||
const wishlist = await prisma.wishlistItem.findMany({
|
||||
where: { userId },
|
||||
take: 20,
|
||||
});
|
||||
|
||||
// Extract product IDs
|
||||
const purchasedProductIds = userOrders.flatMap((order) =>
|
||||
order.items.map((item) => item.productId)
|
||||
);
|
||||
const wishlistProductIds = wishlist.map((item) => item.productId);
|
||||
|
||||
// Get categories and tags from purchased products
|
||||
const purchasedProducts = await Product.find({
|
||||
_id: { $in: purchasedProductIds },
|
||||
});
|
||||
|
||||
const categories = [...new Set(purchasedProducts.map((p) => p.category))];
|
||||
const tags = [
|
||||
...new Set(purchasedProducts.flatMap((p) => p.tags || [])),
|
||||
];
|
||||
|
||||
// Build personalized recommendations
|
||||
const recommendations = await Product.find({
|
||||
status: 'active',
|
||||
_id: {
|
||||
$nin: [...purchasedProductIds, ...wishlistProductIds],
|
||||
},
|
||||
$or: [
|
||||
{ category: { $in: categories } },
|
||||
{ tags: { $in: tags } },
|
||||
{ isFeatured: true },
|
||||
],
|
||||
})
|
||||
.sort({ purchaseCount: -1, viewCount: -1 })
|
||||
.limit(parseInt(limit));
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: recommendations.length,
|
||||
data: recommendations,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get personalized recommendations error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper Functions
|
||||
*/
|
||||
|
||||
// Get recommended products based on multiple factors
|
||||
async function getRecommendedProducts(currentProduct, limit) {
|
||||
const priceRange = {
|
||||
min: currentProduct.basePrice * 0.7,
|
||||
max: currentProduct.basePrice * 1.3,
|
||||
};
|
||||
|
||||
// Score-based recommendation
|
||||
const recommendations = await Product.aggregate([
|
||||
{
|
||||
$match: {
|
||||
_id: { $ne: currentProduct._id },
|
||||
status: 'active',
|
||||
},
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
score: {
|
||||
$add: [
|
||||
{ $cond: [{ $eq: ['$category', currentProduct.category] }, 50, 0] },
|
||||
{
|
||||
$cond: [
|
||||
{
|
||||
$and: [
|
||||
{ $gte: ['$basePrice', priceRange.min] },
|
||||
{ $lte: ['$basePrice', priceRange.max] },
|
||||
],
|
||||
},
|
||||
30,
|
||||
0,
|
||||
],
|
||||
},
|
||||
{
|
||||
$multiply: [
|
||||
{
|
||||
$size: {
|
||||
$ifNull: [
|
||||
{
|
||||
$setIntersection: [
|
||||
{ $ifNull: ['$tags', []] },
|
||||
currentProduct.tags || [],
|
||||
],
|
||||
},
|
||||
[],
|
||||
],
|
||||
},
|
||||
},
|
||||
5,
|
||||
],
|
||||
},
|
||||
{ $cond: ['$isFeatured', 20, 0] },
|
||||
{ $divide: [{ $ifNull: ['$viewCount', 0] }, 100] },
|
||||
{ $divide: [{ $multiply: [{ $ifNull: ['$purchaseCount', 0] }, 10] }, 10] },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ $sort: { score: -1 } },
|
||||
{ $limit: limit },
|
||||
]);
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
// Get frequently bought together products
|
||||
async function getFrequentlyBoughtTogether(productId, limit) {
|
||||
try {
|
||||
const ordersWithProduct = await prisma.orderItem.findMany({
|
||||
where: { productId },
|
||||
select: { orderId: true },
|
||||
distinct: ['orderId'],
|
||||
});
|
||||
|
||||
if (ordersWithProduct.length === 0) {
|
||||
const product = await Product.findById(productId);
|
||||
return await Product.find({
|
||||
_id: { $ne: productId },
|
||||
category: product.category,
|
||||
status: 'active',
|
||||
})
|
||||
.sort({ purchaseCount: -1 })
|
||||
.limit(limit);
|
||||
}
|
||||
|
||||
const orderIds = ordersWithProduct.map((item) => item.orderId);
|
||||
|
||||
const otherProducts = await prisma.orderItem.findMany({
|
||||
where: {
|
||||
orderId: { in: orderIds },
|
||||
productId: { not: productId },
|
||||
},
|
||||
select: { productId: true },
|
||||
});
|
||||
|
||||
const productFrequency = {};
|
||||
otherProducts.forEach((item) => {
|
||||
productFrequency[item.productId] =
|
||||
(productFrequency[item.productId] || 0) + 1;
|
||||
});
|
||||
|
||||
const sortedProductIds = Object.entries(productFrequency)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, limit)
|
||||
.map(([id]) => id);
|
||||
|
||||
const products = await Product.find({
|
||||
_id: { $in: sortedProductIds },
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
return products;
|
||||
} catch (error) {
|
||||
console.error('Get frequently bought together error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Get similar products by attributes
|
||||
async function getSimilarProductsByAttributes(currentProduct, limit) {
|
||||
const similar = await Product.find({
|
||||
_id: { $ne: currentProduct._id },
|
||||
status: 'active',
|
||||
$or: [
|
||||
{ category: currentProduct.category },
|
||||
{ tags: { $in: currentProduct.tags || [] } },
|
||||
{ brand: currentProduct.brand },
|
||||
],
|
||||
})
|
||||
.sort({
|
||||
purchaseCount: -1,
|
||||
viewCount: -1,
|
||||
})
|
||||
.limit(limit);
|
||||
|
||||
return similar;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getProductRecommendations,
|
||||
getAlsoBoughtProducts,
|
||||
getSimilarProducts,
|
||||
getPersonalizedRecommendations,
|
||||
};
|
||||
128
src/controllers/users/addressController.js
Normal file
128
src/controllers/users/addressController.js
Normal file
@@ -0,0 +1,128 @@
|
||||
const { prisma } = require('../../config/database');
|
||||
|
||||
// Get addresses
|
||||
exports.getAddresses = async (req, res, next) => {
|
||||
try {
|
||||
const addresses = await prisma.address.findMany({
|
||||
where: { userId: req.user.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
// res.json({ success: true, data: { addresses } });
|
||||
return res.json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Addresses fetched successfully',
|
||||
data: { addresses },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Add address
|
||||
exports.addAddress = async (req, res, next) => {
|
||||
try {
|
||||
const data = { ...req.body, userId: req.user.id };
|
||||
|
||||
if (data.isDefault) {
|
||||
await prisma.address.updateMany({
|
||||
where: { userId: req.user.id },
|
||||
data: { isDefault: false },
|
||||
});
|
||||
}
|
||||
|
||||
const address = await prisma.address.create({ data });
|
||||
// res.status(201).json({ success: true, message: 'Address added successfully', data: { address } });
|
||||
return res.status(201).json({
|
||||
statusCode: 201,
|
||||
status: true,
|
||||
message: 'Address added successfully',
|
||||
data: { address },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Update address
|
||||
exports.updateAddress = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const existingAddress = await prisma.address.findFirst({
|
||||
where: { id, userId: req.user.id },
|
||||
});
|
||||
|
||||
// if (!existingAddress) {
|
||||
// return res
|
||||
// .status(404)
|
||||
// .json({ success: false, message: 'Address not found' });
|
||||
// }
|
||||
|
||||
if (!existingAddress) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Address not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.isDefault) {
|
||||
await prisma.address.updateMany({
|
||||
where: { userId: req.user.id },
|
||||
data: { isDefault: false },
|
||||
});
|
||||
}
|
||||
|
||||
const updatedAddress = await prisma.address.update({
|
||||
where: { id },
|
||||
data: req.body,
|
||||
});
|
||||
// res.json({
|
||||
// success: true,
|
||||
// message: 'Address updated successfully',
|
||||
// data: { address: updatedAddress },
|
||||
// });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Address updated successfully',
|
||||
data: { address: updatedAddress },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete address
|
||||
exports.deleteAddress = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const existing = await prisma.address.findFirst({
|
||||
where: { id, userId: req.user.id },
|
||||
});
|
||||
|
||||
// if (!existing) {
|
||||
// return res
|
||||
// .status(404)
|
||||
// .json({ success: false, message: 'Address not found' });
|
||||
// }
|
||||
|
||||
if (!existing) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Address not found',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.address.delete({ where: { id } });
|
||||
// res.json({ success: true, message: 'Address deleted successfully' });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Address deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
70
src/controllers/users/adminUserController.js
Normal file
70
src/controllers/users/adminUserController.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const { prisma } = require('../../config/database');
|
||||
|
||||
exports.getAllUsers = async (req, res, next) => {
|
||||
try {
|
||||
const { page = 1, limit = 20, role, search } = req.query;
|
||||
const skip = (page - 1) * limit;
|
||||
const where = {};
|
||||
|
||||
if (role) where.role = role;
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ email: { contains: search, mode: 'insensitive' } },
|
||||
{ firstName: { contains: search, mode: 'insensitive' } },
|
||||
{ lastName: { contains: search, mode: 'insensitive' } },
|
||||
{ username: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
const [users, total] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
role: true,
|
||||
isVerified: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
lastLoginAt: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: parseInt(skip),
|
||||
take: parseInt(limit),
|
||||
}),
|
||||
prisma.user.count({ where }),
|
||||
]);
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// data: {
|
||||
// users,
|
||||
// pagination: {
|
||||
// page: parseInt(page),
|
||||
// limit: parseInt(limit),
|
||||
// total,
|
||||
// pages: Math.ceil(total / limit),
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: "Users fetched successfully",
|
||||
data: {
|
||||
users,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
150
src/controllers/users/cartController.js
Normal file
150
src/controllers/users/cartController.js
Normal file
@@ -0,0 +1,150 @@
|
||||
const mongoose = require("mongoose");
|
||||
// const Product = require("../models/Product");
|
||||
const Product = require('../../models/mongodb/Product');
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
||||
// Add to Cart
|
||||
exports.addToCart = async (req, res, next) => {
|
||||
try {
|
||||
const { productId, quantity = 1 } = req.body;
|
||||
|
||||
if (!productId) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: "Product ID is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate ObjectId
|
||||
if (!mongoose.Types.ObjectId.isValid(productId)) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: "Invalid product ID",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if product exists in MongoDB
|
||||
const productExists = await Product.findById(productId);
|
||||
if (!productExists) {
|
||||
return res.status(404).json({
|
||||
status: false,
|
||||
message: "Product not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if item already exists in cart
|
||||
const existing = await prisma.cartItem.findUnique({
|
||||
where: { userId_productId: { userId: req.user.id, productId } },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Update quantity
|
||||
const updated = await prisma.cartItem.update({
|
||||
where: { userId_productId: { userId: req.user.id, productId } },
|
||||
data: { quantity: existing.quantity + quantity },
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
status: true,
|
||||
message: "Cart quantity updated",
|
||||
data: updated,
|
||||
});
|
||||
}
|
||||
|
||||
// Create new cart item
|
||||
const item = await prisma.cartItem.create({
|
||||
data: {
|
||||
userId: req.user.id,
|
||||
productId,
|
||||
quantity,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
status: true,
|
||||
message: "Item added to cart",
|
||||
data: item,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//Get User Cart
|
||||
exports.getCart = async (req, res, next) => {
|
||||
try {
|
||||
const cart = await prisma.cartItem.findMany({
|
||||
where: { userId: req.user.id },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
// Fetch product details from MongoDB
|
||||
const productIds = cart.map((item) => item.productId);
|
||||
const products = await Product.find({ _id: { $in: productIds } });
|
||||
|
||||
// Merge product details into cart
|
||||
const cartWithProducts = cart.map((item) => ({
|
||||
...item,
|
||||
product: products.find((p) => p._id.toString() === item.productId),
|
||||
}));
|
||||
|
||||
return res.status(200).json({
|
||||
status: true,
|
||||
message: "Cart fetched successfully",
|
||||
data: cartWithProducts,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//Update Quantity
|
||||
exports.updateQuantity = async (req, res, next) => {
|
||||
try {
|
||||
const { productId } = req.params;
|
||||
const { quantity } = req.body;
|
||||
|
||||
if (!quantity || quantity < 1) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: "Quantity must be at least 1",
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await prisma.cartItem.update({
|
||||
where: { userId_productId: { userId: req.user.id, productId } },
|
||||
data: { quantity },
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
status: true,
|
||||
message: "Cart quantity updated",
|
||||
data: updated,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//Remove From Cart
|
||||
exports.removeFromCart = async (req, res, next) => {
|
||||
try {
|
||||
const { productId } = req.params;
|
||||
|
||||
await prisma.cartItem.delete({
|
||||
where: { userId_productId: { userId: req.user.id, productId } },
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
status: true,
|
||||
message: "Item removed from cart",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
51
src/controllers/users/orderController.js
Normal file
51
src/controllers/users/orderController.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const { prisma } = require('../../config/database');
|
||||
|
||||
exports.getOrders = async (req, res, next) => {
|
||||
try {
|
||||
const { page = 1, limit = 10, status } = req.query;
|
||||
const skip = (page - 1) * limit;
|
||||
const where = { userId: req.user.id };
|
||||
if (status) where.status = status;
|
||||
|
||||
const [orders, total] = await Promise.all([
|
||||
prisma.order.findMany({
|
||||
where,
|
||||
include: { items: true, address: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: parseInt(skip),
|
||||
take: parseInt(limit),
|
||||
}),
|
||||
prisma.order.count({ where }),
|
||||
]);
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// data: {
|
||||
// orders,
|
||||
// pagination: {
|
||||
// page: parseInt(page),
|
||||
// limit: parseInt(limit),
|
||||
// total,
|
||||
// pages: Math.ceil(total / limit),
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: "Orders fetched successfully",
|
||||
data: {
|
||||
orders,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
113
src/controllers/users/profileController.js
Normal file
113
src/controllers/users/profileController.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const { prisma } = require('../../config/database');
|
||||
const uploadToS3 = require('../../utils/uploadToS3');
|
||||
|
||||
|
||||
// Get user profile
|
||||
exports.getProfile = async (req, res, next) => {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user.id },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
phone: true,
|
||||
avatar: true,
|
||||
role: true,
|
||||
isVerified: true,
|
||||
createdAt: true,
|
||||
lastLoginAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
// res.json({ success: true, data: { user } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Profile fetched successfully',
|
||||
data: { user },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Update user profile
|
||||
exports.updateProfile = async (req, res, next) => {
|
||||
try {
|
||||
const { firstName, lastName, username, phone } = req.body;
|
||||
|
||||
if (username) {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: { username, NOT: { id: req.user.id } },
|
||||
});
|
||||
// if (existingUser) {
|
||||
// return res.status(400).json({ success: false, message: 'Username already taken' });
|
||||
// }
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Username already taken',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: req.user.id },
|
||||
data: { firstName, lastName, username, phone },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
phone: true,
|
||||
avatar: true,
|
||||
role: true,
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
// res.json({ success: true, message: 'Profile updated successfully', data: { user: updatedUser } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Profile updated successfully',
|
||||
data: { user: updatedUser },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Upload avatar (profile picture)
|
||||
exports.uploadAvatar = async (req, res, next) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "No file uploaded",
|
||||
});
|
||||
}
|
||||
|
||||
const avatarUrl = req.file.location; // multer-s3 gives S3 URL
|
||||
|
||||
// Update avatar in Prisma
|
||||
await prisma.user.update({
|
||||
where: { id: req.user.id },
|
||||
data: { avatar: avatarUrl },
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "Profile picture uploaded successfully",
|
||||
avatarUrl, // only return the profile picture link
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
245
src/controllers/users/wishlistController.js
Normal file
245
src/controllers/users/wishlistController.js
Normal file
@@ -0,0 +1,245 @@
|
||||
const { prisma } = require('../../config/database');
|
||||
// import Product from '../../models/mongodb/Product';
|
||||
const Product = require('../../models/mongodb/Product');
|
||||
|
||||
|
||||
// exports.getWishlist = async (req, res, next) => {
|
||||
// try {
|
||||
// const wishlist = await prisma.wishlistItem.findMany({
|
||||
// where: { userId: req.user.id },
|
||||
// orderBy: { createdAt: 'desc' },
|
||||
// });
|
||||
|
||||
// // Fetch product details from MongoDB
|
||||
// const detailedWishlist = await Promise.all(
|
||||
// wishlist.map(async item => {
|
||||
// const product = await Product.findById(item.productId).select(
|
||||
// 'name basePrice variants images'
|
||||
// );
|
||||
|
||||
// return {
|
||||
// ...item,
|
||||
// product: product || null,
|
||||
// };
|
||||
// })
|
||||
// );
|
||||
|
||||
// // res.json({ success: true, data: { wishlist } });
|
||||
// return res.status(200).json({
|
||||
// // statusCode: 200,
|
||||
// status: true,
|
||||
// message: 'Wishlist fetched successfully',
|
||||
// // data: { wishlist },
|
||||
// data: { wishlist: detailedWishlist },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// exports.addToWishlist = async (req, res, next) => {
|
||||
// try {
|
||||
// const { productId } = req.body;
|
||||
// // if (!productId) return res.status(400).json({ success: false, message: 'Product ID is required' });
|
||||
|
||||
// if (!productId) {
|
||||
// return res.status(400).json({
|
||||
// statusCode: 400,
|
||||
// status: false,
|
||||
// message: 'Product ID is required',
|
||||
// });
|
||||
// }
|
||||
|
||||
// const existing = await prisma.wishlistItem.findUnique({
|
||||
// where: { userId_productId: { userId: req.user.id, productId } },
|
||||
// });
|
||||
// // if (existing) return res.status(400).json({ success: false, message: 'Item already in wishlist' });
|
||||
// if (existing) {
|
||||
// return res.status(400).json({
|
||||
// statusCode: 400,
|
||||
// status: false,
|
||||
// message: 'Item already in wishlist',
|
||||
// });
|
||||
// }
|
||||
|
||||
// const item = await prisma.wishlistItem.create({
|
||||
// data: { userId: req.user.id, productId },
|
||||
// });
|
||||
// // res.status(201).json({ success: true, message: 'Item added to wishlist', data: { item } });
|
||||
// return res.status(201).json({
|
||||
// statusCode: 201,
|
||||
// status: true,
|
||||
// message: 'Item added to wishlist',
|
||||
// data: { item },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
exports.getWishlist = async (req, res, next) => {
|
||||
try {
|
||||
const wishlist = await prisma.wishlistItem.findMany({
|
||||
where: { userId: req.user.id },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
const detailedWishlist = await Promise.all(
|
||||
wishlist.map(async (item) => {
|
||||
let product = null;
|
||||
|
||||
// Only try MongoDB lookup if valid ObjectId
|
||||
if (mongoose.Types.ObjectId.isValid(item.productId)) {
|
||||
product = await Product.findById(item.productId).select(
|
||||
"name basePrice variants images"
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
product: product || null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
status: true,
|
||||
message: "Wishlist fetched successfully",
|
||||
data: { wishlist: detailedWishlist },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// exports.addToWishlist = async (req, res, next) => {
|
||||
// try {
|
||||
// const { productId } = req.body;
|
||||
|
||||
// if (!productId) {
|
||||
// return res.status(400).json({
|
||||
// statusCode: 400,
|
||||
// status: false,
|
||||
// message: 'Product ID is required',
|
||||
// });
|
||||
// }
|
||||
|
||||
// const existing = await prisma.wishlistItem.findUnique({
|
||||
// where: { userId_productId: { userId: req.user.id, productId } },
|
||||
// });
|
||||
|
||||
// if (existing) {
|
||||
// return res.status(400).json({
|
||||
// statusCode: 400,
|
||||
// status: false,
|
||||
// message: 'Item already in wishlist',
|
||||
// });
|
||||
// }
|
||||
|
||||
// const item = await prisma.wishlistItem.create({
|
||||
// data: { userId: req.user.id, productId },
|
||||
// });
|
||||
|
||||
// return res.status(201).json({
|
||||
// statusCode: 201,
|
||||
// status: true,
|
||||
// message: 'Item added to wishlist',
|
||||
// data: { item },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
// const mongoose = require("mongoose");
|
||||
|
||||
exports.addToWishlist = async (req, res, next) => {
|
||||
try {
|
||||
const { productId } = req.body;
|
||||
|
||||
if (!productId) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: "Product ID is required",
|
||||
});
|
||||
}
|
||||
|
||||
// 1️⃣ Validate ObjectId
|
||||
if (!mongoose.Types.ObjectId.isValid(productId)) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: "Invalid product ID (must be MongoDB ObjectId)",
|
||||
});
|
||||
}
|
||||
|
||||
// 2️⃣ Ensure product exists in MongoDB
|
||||
const productExists = await Product.findById(productId);
|
||||
if (!productExists) {
|
||||
return res.status(404).json({
|
||||
status: false,
|
||||
message: "Product not found in database",
|
||||
});
|
||||
}
|
||||
|
||||
// 3️⃣ Check duplicate
|
||||
const existing = await prisma.wishlistItem.findUnique({
|
||||
where: { userId_productId: { userId: req.user.id, productId } },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: "Item already in wishlist",
|
||||
});
|
||||
}
|
||||
|
||||
// 4️⃣ Save VALID productId in Prisma
|
||||
const item = await prisma.wishlistItem.create({
|
||||
data: { userId: req.user.id, productId },
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
status: true,
|
||||
message: "Item added to wishlist",
|
||||
data: { item },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
exports.removeFromWishlist = async (req, res, next) => {
|
||||
try {
|
||||
const { productId } = req.params;
|
||||
const existing = await prisma.wishlistItem.findUnique({
|
||||
where: { userId_productId: { userId: req.user.id, productId } },
|
||||
});
|
||||
// if (!existing) return res.status(404).json({ success: false, message: 'Item not found' });
|
||||
|
||||
if (!existing) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Item not found in wishlist',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.wishlistItem.delete({
|
||||
where: { userId_productId: { userId: req.user.id, productId } },
|
||||
});
|
||||
// res.json({ success: true, message: 'Item removed from wishlist' });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Item removed from wishlist',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
200
src/controllers/wardrobe/wardrobeItemController.js
Normal file
200
src/controllers/wardrobe/wardrobeItemController.js
Normal file
@@ -0,0 +1,200 @@
|
||||
const Wardrobe = require('../../models/mongodb/Wardrobe');
|
||||
|
||||
// @desc Add item to wardrobe
|
||||
// exports.addItem = async (req, res, next) => {
|
||||
// try {
|
||||
// const itemData = req.body;
|
||||
|
||||
// let wardrobe = await Wardrobe.findByUserId(req.user.id);
|
||||
|
||||
// if (!wardrobe) {
|
||||
// wardrobe = new Wardrobe({ userId: req.user.id, name: 'My Wardrobe' });
|
||||
// }
|
||||
|
||||
// await wardrobe.addItem(itemData);
|
||||
|
||||
// // res.status(201).json({
|
||||
// // success: true,
|
||||
// // message: 'Item added successfully',
|
||||
// // data: { wardrobe },
|
||||
// // });
|
||||
// return res.status(201).json({
|
||||
// statusCode: 201,
|
||||
// status: true,
|
||||
// message: 'Item added successfully',
|
||||
// data: { wardrobe },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
|
||||
exports.addItem = async (req, res, next) => {
|
||||
try {
|
||||
const itemData = { ...req.body };
|
||||
|
||||
// ✅ FIX: map image → images array
|
||||
if (itemData.image) {
|
||||
itemData.images = [
|
||||
{
|
||||
url: itemData.image,
|
||||
isPrimary: true,
|
||||
}
|
||||
];
|
||||
delete itemData.image; // prevent schema pollution
|
||||
}
|
||||
|
||||
let wardrobe = await Wardrobe.findByUserId(req.user.id);
|
||||
|
||||
if (!wardrobe) {
|
||||
wardrobe = new Wardrobe({
|
||||
userId: req.user.id,
|
||||
name: 'My Wardrobe',
|
||||
});
|
||||
}
|
||||
|
||||
await wardrobe.addItem(itemData);
|
||||
|
||||
return res.status(201).json({
|
||||
statusCode: 201,
|
||||
status: true,
|
||||
message: 'Item added successfully',
|
||||
data: { wardrobe },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// @desc Update wardrobe item
|
||||
exports.updateItem = async (req, res, next) => {
|
||||
try {
|
||||
const { itemId } = req.params;
|
||||
const updateData = req.body;
|
||||
const wardrobe = await Wardrobe.findByUserId(req.user.id);
|
||||
|
||||
// if (!wardrobe) {
|
||||
// return res.status(404).json({ success: false, message: 'Wardrobe not found' });
|
||||
// }
|
||||
|
||||
if (!wardrobe) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Wardrobe not found',
|
||||
});
|
||||
}
|
||||
|
||||
await wardrobe.updateItem(itemId, updateData);
|
||||
|
||||
// res.json({ success: true, message: 'Item updated successfully', data: { wardrobe } });
|
||||
return res.json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Item updated successfully',
|
||||
data: { wardrobe },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Remove item from wardrobe
|
||||
exports.removeItem = async (req, res, next) => {
|
||||
try {
|
||||
const { itemId } = req.params;
|
||||
const wardrobe = await Wardrobe.findByUserId(req.user.id);
|
||||
|
||||
// if (!wardrobe) {
|
||||
// return res.status(404).json({ success: false, message: 'Wardrobe not found' });
|
||||
// }
|
||||
|
||||
if (!wardrobe) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Wardrobe not found',
|
||||
});
|
||||
}
|
||||
|
||||
await wardrobe.removeItem(itemId);
|
||||
|
||||
// res.json({ success: true, message: 'Item removed successfully', data: { wardrobe } });
|
||||
return res.json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Item removed successfully',
|
||||
data: { wardrobe },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get items by category
|
||||
// exports.getItemsByCategory = async (req, res, next) => {
|
||||
// try {
|
||||
// const { category } = req.params;
|
||||
// const wardrobe = await Wardrobe.findByUserId(req.user.id);
|
||||
|
||||
// // if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' });
|
||||
|
||||
// if (!wardrobe) {
|
||||
// return res.status(404).json({
|
||||
// statusCode: 404,
|
||||
// status: false,
|
||||
// message: 'Wardrobe not found',
|
||||
// });
|
||||
// }
|
||||
|
||||
// const items = wardrobe.getItemsByCategory(category);
|
||||
// // res.json({ success: true, data: { items } });
|
||||
// return res.json({
|
||||
// statusCode: 200,
|
||||
// status: true,
|
||||
// message: 'Items fetched successfully',
|
||||
// data: { items },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
// controllers/wardrobeController.js
|
||||
exports.getItemsByCategory = async (req, res, next) => {
|
||||
try {
|
||||
const { category } = req.params;
|
||||
const wardrobe = await Wardrobe.findByUserId(req.user.id);
|
||||
|
||||
if (!wardrobe) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: "Wardrobe not found",
|
||||
});
|
||||
}
|
||||
|
||||
const items = wardrobe.getItemsByCategory(category) || [];
|
||||
|
||||
return res.json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: "Items fetched successfully",
|
||||
data: { items },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching items by category:", error);
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: "Internal server error",
|
||||
});
|
||||
}
|
||||
};
|
||||
66
src/controllers/wardrobe/wardrobeMainController.js
Normal file
66
src/controllers/wardrobe/wardrobeMainController.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const Wardrobe = require('../../models/mongodb/Wardrobe');
|
||||
|
||||
// @desc Get user's wardrobe
|
||||
exports.getWardrobe = async (req, res, next) => {
|
||||
try {
|
||||
let wardrobe = await Wardrobe.findByUserId(req.user.id);
|
||||
|
||||
if (!wardrobe) {
|
||||
wardrobe = new Wardrobe({
|
||||
userId: req.user.id,
|
||||
name: 'My Wardrobe',
|
||||
});
|
||||
await wardrobe.save();
|
||||
}
|
||||
|
||||
// res.json({ success: true, data: { wardrobe } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: "Wardrobe fetched successfully",
|
||||
data: { wardrobe },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Update wardrobe details
|
||||
exports.updateWardrobe = async (req, res, next) => {
|
||||
try {
|
||||
const { name, description, isPublic, shareSettings } = req.body;
|
||||
|
||||
let wardrobe = await Wardrobe.findByUserId(req.user.id);
|
||||
|
||||
if (!wardrobe) {
|
||||
wardrobe = new Wardrobe({
|
||||
userId: req.user.id,
|
||||
name: name || 'My Wardrobe',
|
||||
description,
|
||||
isPublic: isPublic || false,
|
||||
shareSettings: shareSettings || { allowViewing: false, allowRecommendations: false },
|
||||
});
|
||||
} else {
|
||||
wardrobe.name = name || wardrobe.name;
|
||||
wardrobe.description = description;
|
||||
wardrobe.isPublic = isPublic ?? wardrobe.isPublic;
|
||||
wardrobe.shareSettings = shareSettings || wardrobe.shareSettings;
|
||||
}
|
||||
|
||||
await wardrobe.save();
|
||||
|
||||
// res.json({
|
||||
// success: true,
|
||||
// message: 'Wardrobe updated successfully',
|
||||
// data: { wardrobe },
|
||||
// });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: "Wardrobe updated successfully",
|
||||
data: { wardrobe },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
48
src/controllers/wardrobe/wardrobePublicController.js
Normal file
48
src/controllers/wardrobe/wardrobePublicController.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const Wardrobe = require('../../models/mongodb/Wardrobe');
|
||||
|
||||
// @desc Get public wardrobes
|
||||
exports.getPublicWardrobes = async (req, res, next) => {
|
||||
try {
|
||||
const { page = 1, limit = 20 } = req.query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const wardrobes = await Wardrobe.findPublicWardrobes(parseInt(limit), parseInt(skip));
|
||||
|
||||
// res.json({ success: true, data: { wardrobes } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: "Public wardrobes fetched successfully",
|
||||
data: { wardrobes },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get public wardrobe by ID
|
||||
exports.getPublicWardrobeById = async (req, res, next) => {
|
||||
try {
|
||||
const wardrobe = await Wardrobe.findOne({ _id: req.params.id, isPublic: true });
|
||||
|
||||
// if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' });
|
||||
|
||||
if (!wardrobe) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: "Wardrobe not found",
|
||||
});
|
||||
}
|
||||
|
||||
// res.json({ success: true, data: { wardrobe } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: "Public wardrobe fetched successfully",
|
||||
data: { wardrobe },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
29
src/controllers/wardrobe/wardrobeRecommendationController.js
Normal file
29
src/controllers/wardrobe/wardrobeRecommendationController.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const Wardrobe = require('../../models/mongodb/Wardrobe');
|
||||
|
||||
// @desc Generate outfit recommendations
|
||||
exports.getRecommendations = async (req, res, next) => {
|
||||
try {
|
||||
const wardrobe = await Wardrobe.findByUserId(req.user.id);
|
||||
// if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' });
|
||||
|
||||
if (!wardrobe) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: "Wardrobe not found",
|
||||
data: null
|
||||
});
|
||||
}
|
||||
|
||||
const recommendations = wardrobe.generateOutfitRecommendations();
|
||||
// res.json({ success: true, data: { recommendations } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: "Recommendations fetched successfully",
|
||||
data: { recommendations }
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
52
src/controllers/wardrobe/wardrobeSearchController.js
Normal file
52
src/controllers/wardrobe/wardrobeSearchController.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const Wardrobe = require('../../models/mongodb/Wardrobe');
|
||||
|
||||
// @desc Search wardrobe items
|
||||
exports.searchItems = async (req, res, next) => {
|
||||
try {
|
||||
const { query, tags, category } = req.query;
|
||||
const wardrobe = await Wardrobe.findByUserId(req.user.id);
|
||||
|
||||
// if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' });
|
||||
|
||||
if (!wardrobe) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: "Wardrobe not found",
|
||||
data: null
|
||||
});
|
||||
}
|
||||
|
||||
let items = wardrobe.items.filter(item => item.isActive);
|
||||
|
||||
if (category) items = items.filter(item => item.category === category);
|
||||
|
||||
if (tags) {
|
||||
const tagArray = tags.split(',');
|
||||
items = items.filter(item =>
|
||||
tagArray.some(tag =>
|
||||
item.aiTags.includes(tag) || item.userTags.includes(tag)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const searchTerm = query.toLowerCase();
|
||||
items = items.filter(item =>
|
||||
item.name.toLowerCase().includes(searchTerm) ||
|
||||
item.brand?.toLowerCase().includes(searchTerm) ||
|
||||
item.description?.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
// res.json({ success: true, data: { items } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: "Items fetched successfully",
|
||||
data: { items }
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
39
src/controllers/wardrobe/wardrobeStatsController.js
Normal file
39
src/controllers/wardrobe/wardrobeStatsController.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const Wardrobe = require('../../models/mongodb/Wardrobe');
|
||||
|
||||
// @desc Get wardrobe statistics
|
||||
exports.getStats = async (req, res, next) => {
|
||||
try {
|
||||
const wardrobe = await Wardrobe.findByUserId(req.user.id);
|
||||
// if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' });
|
||||
|
||||
if (!wardrobe) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: "Wardrobe not found",
|
||||
data: null
|
||||
});
|
||||
}
|
||||
|
||||
const stats = {
|
||||
totalItems: wardrobe.totalItems,
|
||||
categoryCounts: wardrobe.categoryCounts,
|
||||
recentItems: wardrobe.items
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||
.slice(0, 5),
|
||||
mostWornItems: wardrobe.items
|
||||
.sort((a, b) => (b.purchaseCount || 0) - (a.purchaseCount || 0))
|
||||
.slice(0, 5),
|
||||
};
|
||||
|
||||
// res.json({ success: true, data: { stats } });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: "Wardrobe statistics fetched successfully",
|
||||
data: { stats }
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user