From bb275002384427fc6b48b0038334319b95cc14a7 Mon Sep 17 00:00:00 2001 From: Suhaskumard Date: Sun, 21 Jun 2026 08:53:11 +0530 Subject: [PATCH 1/2] feat: implement product return and refund flow --- BACKEND/app.js | 2 + BACKEND/controllers/return.controller.js | 149 +++++++++++ BACKEND/middleware/auth.js | 8 + BACKEND/models/order.model.js | 9 + BACKEND/models/returnRequest.model.js | 66 +++++ BACKEND/models/user.model.js | 6 + BACKEND/routes/return.route.js | 20 ++ BACKEND/scripts/makeAdmin.js | 36 +++ BACKEND/tests/return.test.js | 232 ++++++++++++++++++ FRONTEND/applyDemo.cjs | 48 ++++ FRONTEND/cleanup.cjs | 24 ++ FRONTEND/correct_refactor.cjs | 58 +++++ FRONTEND/fix.cjs | 54 ++++ FRONTEND/fix2.cjs | 39 +++ FRONTEND/fix2.py | 37 +++ FRONTEND/fixApp.cjs | 15 ++ FRONTEND/fixHomePage.cjs | 21 ++ FRONTEND/refactor.cjs | 71 ++++++ FRONTEND/src/App.jsx | 2 + .../components/Returns/CreateReturnModal.jsx | 203 +++++++++++++++ FRONTEND/src/components/ui/ErrorBoundary.jsx | 10 + FRONTEND/src/pages/HomePage.jsx | 3 + FRONTEND/src/pages/MyReturnsPage.jsx | 185 ++++++++++++++ FRONTEND/src/pages/admin/ReturnsAdminPage.jsx | 227 +++++++++++++++++ FRONTEND/src/utils/toastService.js | 49 ++++ 25 files changed, 1574 insertions(+) create mode 100644 BACKEND/controllers/return.controller.js create mode 100644 BACKEND/models/returnRequest.model.js create mode 100644 BACKEND/routes/return.route.js create mode 100644 BACKEND/scripts/makeAdmin.js create mode 100644 BACKEND/tests/return.test.js create mode 100644 FRONTEND/applyDemo.cjs create mode 100644 FRONTEND/cleanup.cjs create mode 100644 FRONTEND/correct_refactor.cjs create mode 100644 FRONTEND/fix.cjs create mode 100644 FRONTEND/fix2.cjs create mode 100644 FRONTEND/fix2.py create mode 100644 FRONTEND/fixApp.cjs create mode 100644 FRONTEND/fixHomePage.cjs create mode 100644 FRONTEND/refactor.cjs create mode 100644 FRONTEND/src/components/Returns/CreateReturnModal.jsx create mode 100644 FRONTEND/src/pages/MyReturnsPage.jsx create mode 100644 FRONTEND/src/pages/admin/ReturnsAdminPage.jsx create mode 100644 FRONTEND/src/utils/toastService.js diff --git a/BACKEND/app.js b/BACKEND/app.js index 970d672..3477d40 100644 --- a/BACKEND/app.js +++ b/BACKEND/app.js @@ -14,6 +14,7 @@ import wishlistRoutes from "./routes/wishlist.route.js"; import newsletterRoutes from "./routes/newsletter.route.js"; import ordersRoutes from "./routes/orders.route.js"; import userRoutes from "./routes/user.route.js"; +import returnRoutes from "./routes/return.route.js"; import passport from "./config/passport.js"; import swaggerUi from 'swagger-ui-express'; import swaggerSpec from './swagger.js'; @@ -111,6 +112,7 @@ app.use("/api/wishlist", wishlistRoutes); app.use("/api/orders", ordersRoutes); app.use("/api/user", userRoutes); app.use("/api/newsletter", newsletterRoutes); +app.use("/api/returns", returnRoutes); // ============= PRODUCTION STATIC FILES & REACT APP ============= diff --git a/BACKEND/controllers/return.controller.js b/BACKEND/controllers/return.controller.js new file mode 100644 index 0000000..83cd7e0 --- /dev/null +++ b/BACKEND/controllers/return.controller.js @@ -0,0 +1,149 @@ +import ReturnRequest from '../models/returnRequest.model.js'; +import Order from '../models/order.model.js'; + +// @desc Initiate a return request +// @route POST /api/returns +// @access Private +export const createReturnRequest = async (req, res) => { + try { + const { orderId, items } = req.body; + const userId = req.user._id; + + if (!items || items.length === 0) { + return res.status(400).json({ success: false, message: 'No items selected for return' }); + } + + const order = await Order.findOne({ _id: orderId, user: userId }); + if (!order) { + return res.status(404).json({ success: false, message: 'Order not found' }); + } + + if (order.deliveryStatus !== 'delivered') { + return res.status(400).json({ success: false, message: 'Only delivered orders can be returned' }); + } + + if (!order.deliveryDate) { + return res.status(400).json({ success: false, message: 'Order delivery date is unknown, cannot verify eligibility' }); + } + + const daysSinceDelivery = (new Date() - new Date(order.deliveryDate)) / (1000 * 60 * 60 * 24); + if (daysSinceDelivery > 30) { + return res.status(400).json({ success: false, message: 'Return window of 30 days has expired' }); + } + + // Check if a return request already exists for these items + const existingReturns = await ReturnRequest.find({ orderId }); + for (const returnReq of existingReturns) { + for (const reqItem of returnReq.items) { + if (items.some(i => i.productId === reqItem.productId.toString())) { + // Simplified validation: doesn't check exact quantities to avoid complex partial returns for now, + // but prevents returning the same product twice. + return res.status(400).json({ success: false, message: 'A return request already exists for one or more of these items.' }); + } + } + } + + // Calculate refund amount + let refundAmount = 0; + for (const item of items) { + // Find the item in the original order to ensure the price is accurate + const orderItem = order.items.find(i => i.product.toString() === item.productId); + if (!orderItem) { + return res.status(400).json({ success: false, message: `Product ${item.productId} not found in this order` }); + } + if (item.quantity > orderItem.quantity) { + return res.status(400).json({ success: false, message: `Cannot return more quantity than ordered for product ${item.productId}` }); + } + refundAmount += orderItem.price * item.quantity; + } + + const newReturnRequest = new ReturnRequest({ + orderId, + userId, + items, + refundAmount, + }); + + await newReturnRequest.save(); + + res.status(201).json({ + success: true, + message: 'Return request submitted successfully', + returnRequest: newReturnRequest, + }); + } catch (error) { + console.error('Create Return Request Error:', error); + res.status(500).json({ success: false, message: 'Internal server error' }); + } +}; + +// @desc Get all return requests for logged in user +// @route GET /api/returns/my-returns +// @access Private +export const getMyReturnRequests = async (req, res) => { + try { + const returns = await ReturnRequest.find({ userId: req.user._id }) + .populate('orderId') + .sort({ createdAt: -1 }); + + res.json({ success: true, returns }); + } catch (error) { + console.error('Get My Returns Error:', error); + res.status(500).json({ success: false, message: 'Internal server error' }); + } +}; + +// @desc Get all return requests (Admin) +// @route GET /api/returns +// @access Private/Admin +export const getAllReturnRequests = async (req, res) => { + try { + const returns = await ReturnRequest.find({}) + .populate('userId', 'name email') + .populate('orderId') + .sort({ createdAt: -1 }); + + res.json({ success: true, returns }); + } catch (error) { + console.error('Get All Returns Error:', error); + res.status(500).json({ success: false, message: 'Internal server error' }); + } +}; + +// @desc Update return request status (Admin) +// @route PUT /api/returns/:id/status +// @access Private/Admin +export const updateReturnStatus = async (req, res) => { + try { + const { status, adminComments } = req.body; + const returnReq = await ReturnRequest.findById(req.params.id); + + if (!returnReq) { + return res.status(404).json({ success: false, message: 'Return request not found' }); + } + + const validStatuses = ["Requested", "Under Review", "Approved", "Rejected", "Refund Initiated", "Completed"]; + if (!validStatuses.includes(status)) { + return res.status(400).json({ success: false, message: 'Invalid status' }); + } + + returnReq.status = status; + if (adminComments !== undefined) { + returnReq.adminComments = adminComments; + } + + // Optional: Add logic here to interact with Stripe or Payment Gateway for 'Refund Initiated' + // if (status === 'Refund Initiated') { ... } + + await returnReq.save(); + + res.json({ + success: true, + message: 'Return request status updated', + returnRequest: returnReq, + }); + } catch (error) { + console.error('Update Return Status Error:', error); + res.status(500).json({ success: false, message: 'Internal server error' }); + } +}; diff --git a/BACKEND/middleware/auth.js b/BACKEND/middleware/auth.js index b4b3059..8ed087f 100644 --- a/BACKEND/middleware/auth.js +++ b/BACKEND/middleware/auth.js @@ -41,3 +41,11 @@ export async function optionalProtect(req, res, next) { } next(); } + +export async function adminProtect(req, res, next) { + if (req.user && req.user.role === 'admin') { + next(); + } else { + res.status(403).json({ success: false, message: 'Not authorized as an admin' }); + } +} diff --git a/BACKEND/models/order.model.js b/BACKEND/models/order.model.js index a26a518..552bdc5 100644 --- a/BACKEND/models/order.model.js +++ b/BACKEND/models/order.model.js @@ -53,6 +53,15 @@ const orderSchema = new mongoose.Schema({ enum: ["pending", "completed", "failed", "refunded"], default: "pending", // changed from "completed" to "pending" }, + deliveryStatus: { + type: String, + enum: ["pending", "shipped", "delivered"], + default: "pending", + }, + deliveryDate: { + type: Date, + default: null, + }, }, { timestamps: true }); // Indexes for faster lookups diff --git a/BACKEND/models/returnRequest.model.js b/BACKEND/models/returnRequest.model.js new file mode 100644 index 0000000..03b08e7 --- /dev/null +++ b/BACKEND/models/returnRequest.model.js @@ -0,0 +1,66 @@ +import mongoose from "mongoose"; + +const returnItemSchema = new mongoose.Schema({ + productId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Product", + required: true, + }, + name: { + type: String, + required: true, + }, + price: { + type: Number, + required: true, + }, + quantity: { + type: Number, + required: true, + min: 1, + }, + reason: { + type: String, + required: true, + }, + condition: { + type: String, + enum: ["Opened", "Unopened", "Damaged", "Defective", "Other"], + default: "Other", + } +}, { _id: false }); + +const returnRequestSchema = new mongoose.Schema({ + orderId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Order", + required: true, + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + items: [returnItemSchema], + status: { + type: String, + enum: ["Requested", "Under Review", "Approved", "Rejected", "Refund Initiated", "Completed"], + default: "Requested", + }, + adminComments: { + type: String, + default: "", + }, + refundAmount: { + type: Number, + required: true, + min: 0, + }, +}, { timestamps: true }); + +// Indexes +returnRequestSchema.index({ orderId: 1 }); +returnRequestSchema.index({ userId: 1 }); + +const ReturnRequest = mongoose.model("ReturnRequest", returnRequestSchema); +export default ReturnRequest; diff --git a/BACKEND/models/user.model.js b/BACKEND/models/user.model.js index 176b893..9ddd36f 100644 --- a/BACKEND/models/user.model.js +++ b/BACKEND/models/user.model.js @@ -74,6 +74,12 @@ const userSchema = new mongoose.Schema( type: Date, default: null, }, + + role: { + type: String, + enum: ['user', 'admin'], + default: 'user', + }, }, { timestamps: true, diff --git a/BACKEND/routes/return.route.js b/BACKEND/routes/return.route.js new file mode 100644 index 0000000..3d3f5c0 --- /dev/null +++ b/BACKEND/routes/return.route.js @@ -0,0 +1,20 @@ +import express from 'express'; +import { + createReturnRequest, + getMyReturnRequests, + getAllReturnRequests, + updateReturnStatus, +} from '../controllers/return.controller.js'; +import { protect, adminProtect } from '../middleware/auth.js'; + +const router = express.Router(); + +// User routes +router.post('/', protect, createReturnRequest); +router.get('/my-returns', protect, getMyReturnRequests); + +// Admin routes +router.get('/', protect, adminProtect, getAllReturnRequests); +router.put('/:id/status', protect, adminProtect, updateReturnStatus); + +export default router; diff --git a/BACKEND/scripts/makeAdmin.js b/BACKEND/scripts/makeAdmin.js new file mode 100644 index 0000000..710b3c5 --- /dev/null +++ b/BACKEND/scripts/makeAdmin.js @@ -0,0 +1,36 @@ +import mongoose from 'mongoose'; +import dotenv from 'dotenv'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import User from '../models/user.model.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +dotenv.config({ path: path.join(__dirname, '..', '.env') }); + +const makeAdmin = async (email) => { + try { + await mongoose.connect(process.env.MONGODB_URI); + const user = await User.findOne({ email }); + if (!user) { + console.log(`User with email ${email} not found.`); + process.exit(1); + } + + user.role = 'admin'; + await user.save(); + console.log(`User ${email} is now an admin!`); + process.exit(0); + } catch (error) { + console.error('Error:', error); + process.exit(1); + } +}; + +const email = process.argv[2]; +if (!email) { + console.log('Please provide an email address: node makeAdmin.js '); + process.exit(1); +} + +makeAdmin(email); diff --git a/BACKEND/tests/return.test.js b/BACKEND/tests/return.test.js new file mode 100644 index 0000000..be5f5af --- /dev/null +++ b/BACKEND/tests/return.test.js @@ -0,0 +1,232 @@ +import request from 'supertest'; +import mongoose from 'mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import app from '../app.js'; +import User from '../models/user.model.js'; +import Order from '../models/order.model.js'; +import Product from '../models/product.model.js'; +import ReturnRequest from '../models/returnRequest.model.js'; +import { generateToken } from '../middleware/auth.js'; + +let mongoServer; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const uri = mongoServer.getUri(); + await mongoose.connect(uri); +}, 600000); + +afterAll(async () => { + await mongoose.disconnect(); + if (mongoServer) { + await mongoServer.stop(); + } +}); + +beforeEach(async () => { + await User.deleteMany({}); + await Order.deleteMany({}); + await Product.deleteMany({}); + await ReturnRequest.deleteMany({}); +}); + +describe('Return API Routes', () => { + let user, admin, userToken, adminToken, product; + + beforeEach(async () => { + // Create users + user = await User.create({ name: 'User 1', email: 'user@test.com', password: 'password', role: 'user' }); + admin = await User.create({ name: 'Admin 1', email: 'admin@test.com', password: 'password', role: 'admin' }); + + // Create tokens + userToken = generateToken(user); + adminToken = generateToken(admin); + + // Create product + product = await Product.create({ name: 'Test Product', price: 50, image: 'img.jpg' }); + }); + + describe('POST /api/returns', () => { + it('should create a return request for an eligible order', async () => { + const order = await Order.create({ + user: user._id, + items: [{ product: product._id, name: product.name, price: product.price, quantity: 2 }], + totalAmount: 100, + stripeSessionId: 'sess_1', + paymentStatus: 'completed', + deliveryStatus: 'delivered', + deliveryDate: new Date() // Today + }); + + const response = await request(app) + .post('/api/returns') + .set('Authorization', `Bearer ${userToken}`) + .send({ + orderId: order._id, + items: [{ productId: product._id.toString(), name: product.name, price: product.price, quantity: 1, reason: 'Defective' }] + }); + + expect(response.status).toBe(201); + expect(response.body.success).toBe(true); + expect(response.body.returnRequest.refundAmount).toBe(50); + }); + + it('should fail if order is not delivered', async () => { + const order = await Order.create({ + user: user._id, + items: [{ product: product._id, name: product.name, price: product.price, quantity: 1 }], + totalAmount: 50, + stripeSessionId: 'sess_2', + paymentStatus: 'completed', + deliveryStatus: 'pending' + }); + + const response = await request(app) + .post('/api/returns') + .set('Authorization', `Bearer ${userToken}`) + .send({ + orderId: order._id, + items: [{ productId: product._id.toString(), name: product.name, price: product.price, quantity: 1, reason: 'Defective' }] + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Only delivered orders can be returned'); + }); + + it('should fail if delivery date is more than 30 days ago', async () => { + const oldDate = new Date(); + oldDate.setDate(oldDate.getDate() - 35); // 35 days ago + + const order = await Order.create({ + user: user._id, + items: [{ product: product._id, name: product.name, price: product.price, quantity: 1 }], + totalAmount: 50, + stripeSessionId: 'sess_3', + paymentStatus: 'completed', + deliveryStatus: 'delivered', + deliveryDate: oldDate + }); + + const response = await request(app) + .post('/api/returns') + .set('Authorization', `Bearer ${userToken}`) + .send({ + orderId: order._id, + items: [{ productId: product._id.toString(), name: product.name, price: product.price, quantity: 1, reason: 'Defective' }] + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Return window of 30 days has expired'); + }); + + it('should fail if trying to return more quantity than ordered', async () => { + const order = await Order.create({ + user: user._id, + items: [{ product: product._id, name: product.name, price: product.price, quantity: 1 }], + totalAmount: 50, + stripeSessionId: 'sess_4', + paymentStatus: 'completed', + deliveryStatus: 'delivered', + deliveryDate: new Date() + }); + + const response = await request(app) + .post('/api/returns') + .set('Authorization', `Bearer ${userToken}`) + .send({ + orderId: order._id, + items: [{ productId: product._id.toString(), name: product.name, price: product.price, quantity: 2, reason: 'Defective' }] + }); + + expect(response.status).toBe(400); + expect(response.body.message).toContain('Cannot return more quantity than ordered'); + }); + + it('should fail if item was already returned', async () => { + const order = await Order.create({ + user: user._id, + items: [{ product: product._id, name: product.name, price: product.price, quantity: 2 }], + totalAmount: 100, + stripeSessionId: 'sess_5', + paymentStatus: 'completed', + deliveryStatus: 'delivered', + deliveryDate: new Date() + }); + + // First return + await request(app) + .post('/api/returns') + .set('Authorization', `Bearer ${userToken}`) + .send({ + orderId: order._id, + items: [{ productId: product._id.toString(), name: product.name, price: product.price, quantity: 1, reason: 'Defective' }] + }); + + // Second return of same item + const response = await request(app) + .post('/api/returns') + .set('Authorization', `Bearer ${userToken}`) + .send({ + orderId: order._id, + items: [{ productId: product._id.toString(), name: product.name, price: product.price, quantity: 1, reason: 'Changed Mind' }] + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('A return request already exists for one or more of these items.'); + }); + }); + + describe('Admin Routes', () => { + it('should allow admin to fetch all returns', async () => { + const response = await request(app) + .get('/api/returns') + .set('Authorization', `Bearer ${adminToken}`); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(Array.isArray(response.body.returns)).toBe(true); + }); + + it('should reject non-admin users from fetching all returns', async () => { + const response = await request(app) + .get('/api/returns') + .set('Authorization', `Bearer ${userToken}`); + + expect(response.status).toBe(403); + }); + + it('should allow admin to update return status', async () => { + const order = await Order.create({ + user: user._id, + items: [{ product: product._id, name: product.name, price: product.price, quantity: 1 }], + totalAmount: 50, + stripeSessionId: 'sess_6', + paymentStatus: 'completed', + deliveryStatus: 'delivered', + deliveryDate: new Date() + }); + + const retRes = await request(app) + .post('/api/returns') + .set('Authorization', `Bearer ${userToken}`) + .send({ + orderId: order._id, + items: [{ productId: product._id.toString(), name: product.name, price: product.price, quantity: 1, reason: 'Defective' }] + }); + + const returnId = retRes.body.returnRequest._id; + + const updateRes = await request(app) + .put(`/api/returns/${returnId}/status`) + .set('Authorization', `Bearer ${adminToken}`) + .send({ + status: 'Approved', + adminComments: 'Looks good' + }); + + expect(updateRes.status).toBe(200); + expect(updateRes.body.returnRequest.status).toBe('Approved'); + expect(updateRes.body.returnRequest.adminComments).toBe('Looks good'); + }); + }); +}); diff --git a/FRONTEND/applyDemo.cjs b/FRONTEND/applyDemo.cjs new file mode 100644 index 0000000..07e068d --- /dev/null +++ b/FRONTEND/applyDemo.cjs @@ -0,0 +1,48 @@ +const fs = require('fs'); + +// App.jsx +let appPath = 'src/App.jsx'; +let appContent = fs.readFileSync(appPath, 'utf8'); +appContent = appContent.replace('import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";', 'import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";\nimport { ToastContainer } from "./utils/toastService";'); +appContent = appContent.replace('', '\n '); +fs.writeFileSync(appPath, appContent); + +// ErrorBoundary.jsx +let ebPath = 'src/components/ui/ErrorBoundary.jsx'; +let ebContent = fs.readFileSync(ebPath, 'utf8'); +ebContent = ebContent.replace('console.error("ErrorBoundary caught:", error, info.componentStack);', 'console.error("ErrorBoundary caught:", error, info.componentStack);\n // Sentry.captureException(error, { extra: info });\n // LogRocket.captureException(error, { extra: info });'); +fs.writeFileSync(ebPath, ebContent); + +// HomePage.jsx +let hpPath = 'src/pages/HomePage.jsx'; +let hpContent = fs.readFileSync(hpPath, 'utf8'); +hpContent = hpContent.replace('import FilterPanel from "../components/ui/FilterPanel";', 'import FilterPanel from "../components/ui/FilterPanel";\nimport { notify } from "../utils/toastService";\n\nconst CrashTest = ({ shouldCrash }) => {\n if (shouldCrash) {\n throw new Error("Simulated Frontend Crash for Demo!");\n }\n return null;\n};\n'); +hpContent = hpContent.replace('const [totalProducts, setTotalProducts] = useState(0);', 'const [totalProducts, setTotalProducts] = useState(0);\n const [shouldCrash, setShouldCrash] = useState(false);'); + +let buttonsStr = ` + + Current Products🚀 + + + + + + + `; + +hpContent = hpContent.replace(' \n \n Current Products🚀\n ', buttonsStr); +fs.writeFileSync(hpPath, hpContent); + +console.log("Applied demo changes"); diff --git a/FRONTEND/cleanup.cjs b/FRONTEND/cleanup.cjs new file mode 100644 index 0000000..3309797 --- /dev/null +++ b/FRONTEND/cleanup.cjs @@ -0,0 +1,24 @@ +const fs = require('fs'); + +let hpPath = 'src/pages/HomePage.jsx'; +let c = fs.readFileSync(hpPath, 'utf8'); + +const importRegex = /import \{ notify \} from "\.\.\/utils\/toastService";\r?\n\r?\nconst CrashTest = \(\{ shouldCrash \}\) => \{[\s\S]*?if \(shouldCrash\) \{[\s\S]*?throw new Error\("Simulated Frontend Crash for Demo!"\);[\s\S]*?\}[\s\S]*?return null;[\s\S]*?\};\r?\n/; +c = c.replace(importRegex, ''); + +const stateRegex = /const \[shouldCrash, setShouldCrash\] = useState\(false\);\r?\n\s*/; +c = c.replace(stateRegex, ''); + +const buttonsRegex = /[\s\S]*? + + + + + ); +}; + +export default CreateReturnModal; diff --git a/FRONTEND/src/components/ui/ErrorBoundary.jsx b/FRONTEND/src/components/ui/ErrorBoundary.jsx index e5e2cb2..b6c2935 100644 --- a/FRONTEND/src/components/ui/ErrorBoundary.jsx +++ b/FRONTEND/src/components/ui/ErrorBoundary.jsx @@ -21,6 +21,16 @@ class ErrorBoundary extends React.Component { componentDidCatch(error, info) { console.error("ErrorBoundary caught:", error, info.componentStack); + // Sentry.captureException(error, { extra: info }); + // LogRocket.captureException(error, { extra: info }); + + // ========================================== + // Centralized Error Logging Integration + // ========================================== + // + // Sentry.captureException(error, { extra: info }); + // LogRocket.captureException(error, { extra: info }); + // ========================================== } handleReset = () => { diff --git a/FRONTEND/src/pages/HomePage.jsx b/FRONTEND/src/pages/HomePage.jsx index baecb37..05bfb29 100644 --- a/FRONTEND/src/pages/HomePage.jsx +++ b/FRONTEND/src/pages/HomePage.jsx @@ -17,6 +17,8 @@ import { formatPrice } from '../utils/currency'; import RecentlyViewedCarousel from "../components/ui/RecentlyViewedCarousel"; import FilterPanel from "../components/ui/FilterPanel"; + + const ProductCardSkeleton = () => { const bg = useColorModeValue("white", "gray.800"); const borderColor = useColorModeValue("gray.200", "gray.700"); @@ -52,6 +54,7 @@ const HomePage = () => { const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalProducts, setTotalProducts] = useState(0); + const limit = 10; const [filters, setFilters] = useState({ diff --git a/FRONTEND/src/pages/MyReturnsPage.jsx b/FRONTEND/src/pages/MyReturnsPage.jsx new file mode 100644 index 0000000..12ee33d --- /dev/null +++ b/FRONTEND/src/pages/MyReturnsPage.jsx @@ -0,0 +1,185 @@ +import { useEffect, useState } from 'react'; +import { + Container, + VStack, + Heading, + Text, + Box, + Spinner, + Badge, + HStack, + Divider, + Image, + useColorModeValue, + Alert, + AlertIcon, +} from '@chakra-ui/react'; +import Breadcrumbs from "../components/ui/Breadcrumbs"; + +const STATUS_COLORS = { + Requested: 'blue', + 'Under Review': 'purple', + Approved: 'green', + Rejected: 'red', + 'Refund Initiated': 'teal', + Completed: 'gray', +}; + +const MyReturnsPage = () => { + const [returns, setReturns] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const bg = useColorModeValue('white', 'gray.800'); + const cardBg = useColorModeValue('gray.50', 'gray.700'); + const textColor = useColorModeValue('gray.600', 'gray.300'); + const borderColor = useColorModeValue('gray.200', 'gray.600'); + + useEffect(() => { + const fetchReturns = async () => { + try { + const token = localStorage.getItem('authToken'); + const res = await fetch('/api/returns/my-returns', { + headers: { Authorization: `Bearer ${token}` }, + }); + const data = await res.json(); + if (!data.success) { + setError(data.message || 'Failed to load returns'); + } else { + setReturns(data.returns); + } + } catch { + setError('Network error — please try again'); + } finally { + setLoading(false); + } + }; + + fetchReturns(); + }, []); + + if (loading) { + return ( + + + + ); + } + + return ( + + + + + My Returns + + + {error && ( + + + {error} + + )} + + {!error && returns.length === 0 && ( + + + You haven't requested any returns yet. + + + )} + + {returns.map((ret) => ( + + + + + + Return ID + + + {ret._id} + + + + + Order ID + + + {ret.orderId?._id || "Unknown"} + + + + + Date + + + {new Date(ret.createdAt).toLocaleDateString()} + + + + + Status + + + {ret.status} + + + + + + } px={6} py={2}> + {ret.items.map((item, idx) => ( + + + {item.name} + + Reason: {item.reason} | Condition: {item.condition} + + + + Qty: {item.quantity} + ${(item.quantity * item.price).toFixed(2)} + + + ))} + + + {(ret.adminComments || ret.refundAmount > 0) && ( + + + {ret.adminComments ? ( + + Admin Note + {ret.adminComments} + + ) : } + + Expected Refund + ${ret.refundAmount.toFixed(2)} + + + + )} + + ))} + + + ); +}; + +export default MyReturnsPage; diff --git a/FRONTEND/src/pages/admin/ReturnsAdminPage.jsx b/FRONTEND/src/pages/admin/ReturnsAdminPage.jsx new file mode 100644 index 0000000..cf92202 --- /dev/null +++ b/FRONTEND/src/pages/admin/ReturnsAdminPage.jsx @@ -0,0 +1,227 @@ +import { useEffect, useState } from 'react'; +import { + Container, + VStack, + Heading, + Text, + Box, + Spinner, + Badge, + HStack, + Divider, + useColorModeValue, + Alert, + AlertIcon, + Button, + Select, + Textarea +} from '@chakra-ui/react'; +import Breadcrumbs from "../../components/ui/Breadcrumbs"; +import { notify } from '../../utils/toastService'; + + +const STATUS_COLORS = { + Requested: 'blue', + 'Under Review': 'purple', + Approved: 'green', + Rejected: 'red', + 'Refund Initiated': 'teal', + Completed: 'gray', +}; + +const ReturnsAdminPage = () => { + const [returns, setReturns] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + + const bg = useColorModeValue('white', 'gray.800'); + const cardBg = useColorModeValue('gray.50', 'gray.700'); + const textColor = useColorModeValue('gray.600', 'gray.300'); + const borderColor = useColorModeValue('gray.200', 'gray.600'); + + const fetchReturns = async () => { + try { + const token = localStorage.getItem('authToken'); + const res = await fetch('/api/returns', { + headers: { Authorization: `Bearer ${token}` }, + }); + const data = await res.json(); + if (!data.success) { + setError(data.message || 'Failed to load returns'); + } else { + setReturns(data.returns); + } + } catch { + setError('Network error — please try again'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchReturns(); + }, []); + + const handleUpdateStatus = async (id, newStatus, adminComments) => { + try { + const token = localStorage.getItem('authToken'); + const res = await fetch(`/api/returns/${id}/status`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ status: newStatus, adminComments }) + }); + const data = await res.json(); + if (data.success) { + notify.success('Success', 'Operation successful'); + fetchReturns(); + } else { + notify.error('Error', 'An error occurred'); + } + } catch (err) { + notify.error('Error', 'An error occurred'); + } + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + + + Manage Return Requests + + + {error && ( + + + {error} + + )} + + {!error && returns.length === 0 && ( + + + No return requests found. + + + )} + + {returns.map((ret) => ( + + + + + Return ID + {ret._id} + + + User + {ret.userId?.name} ({ret.userId?.email}) + + + Order ID + {ret.orderId?._id || "Unknown"} + + + Status + + {ret.status} + + + + Refund Amount + ${ret.refundAmount.toFixed(2)} + + + + + } px={6} py={2}> + {ret.items.map((item, idx) => ( + + + {item.name} + Reason: {item.reason} | Condition: {item.condition} + + + Qty: {item.quantity} + ${(item.quantity * item.price).toFixed(2)} + + + ))} + + + + + Admin Actions + + + Update Status: + + + + Comments (Visible to User): +