diff --git a/backend/controllers/shop/sub-controllers/adminController.js b/backend/controllers/shop/sub-controllers/adminController.js new file mode 100644 index 00000000..61f9054d --- /dev/null +++ b/backend/controllers/shop/sub-controllers/adminController.js @@ -0,0 +1,44 @@ +const Payment = require("../models/payment"); +const Invoice = require("../models/invoice"); + +// Admin view all payments +const adminViewPayments = async (req, res) => { + try { + const payments = await Payment.find() + .populate("orderId") + .populate("userId"); + res.status(200).json(payments); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +// Admin view all invoices +const adminViewInvoices = async (req, res) => { + try { + const invoices = await Invoice.find() + .populate("orderId") + .populate("userId"); + res.status(200).json(invoices); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +// Admin filter/sort payments +const adminFilterPayments = async (req, res) => { + const { status, startDate, endDate } = req.query; + const filters = {}; + if (status) filters.paymentStatus = status; + if (startDate && endDate) + filters.paymentDate = { $gte: startDate, $lte: endDate }; + + try { + const payments = await Payment.find(filters); + res.status(200).json(payments); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +module.exports = { adminViewPayments, adminViewInvoices, adminFilterPayments }; diff --git a/backend/controllers/shop/sub-controllers/invoiceController.js b/backend/controllers/shop/sub-controllers/invoiceController.js new file mode 100644 index 00000000..41cc8388 --- /dev/null +++ b/backend/controllers/shop/sub-controllers/invoiceController.js @@ -0,0 +1,217 @@ +const Invoice = require("../models/invoice"); +const Joi = require("joi"); + +// Validation Schemas +const invoiceSchema = Joi.object({ + orderId: Joi.string().required(), + userId: Joi.string().required(), + amount: Joi.number().min(0).required(), + dueDate: Joi.date().required(), + paymentId: Joi.string().optional(), +}); + +const statusUpdateSchema = Joi.object({ + status: Joi.string() + .valid("Paid", "Unpaid", "Overdue", "Cancelled") + .required(), +}); + +// Generate invoice after payment confirmation +const generateInvoice = async (req, res) => { + const { error, value } = invoiceSchema.validate(req.body); + if (error) return res.status(400).json({ message: error.message }); + + const { orderId, userId, amount, dueDate, paymentId } = value; + + const newInvoice = new Invoice({ + invoiceId: `INV${Date.now()}`, + orderId, + userId, + amount, + dueDate, + paymentId, + }); + + try { + const invoice = await newInvoice.save(); + res.status(201).json(invoice); + } catch (error) { + console.error(`Error generating invoice: ${error.message}`); + res.status(500).json({ message: "Internal server error" }); + } +}; + +// Retrieve invoices by user, order, status, or date range with pagination +const getInvoices = async (req, res) => { + const { + userId, + orderId, + status, + startDate, + endDate, + page = 1, + limit = 10, + sortBy = "invoiceDate", + order = "desc", + } = req.query; + + try { + const filters = {}; + if (userId) filters.userId = userId; + if (orderId) filters.orderId = orderId; + if (status) filters.status = status; + + if (startDate || endDate) { + filters.invoiceDate = {}; + if (startDate) filters.invoiceDate.$gte = new Date(startDate); + if (endDate) filters.invoiceDate.$lte = new Date(endDate); + } + + const invoices = await Invoice.find(filters) + .sort({ [sortBy]: order === "asc" ? 1 : -1 }) + .skip((page - 1) * limit) + .limit(Number(limit)); + + res.status(200).json(invoices); + } catch (error) { + console.error(`Error fetching invoices: ${error.message}`); + res.status(500).json({ message: "Internal server error" }); + } +}; + +// Retrieve a single invoice by ID +const getInvoiceById = async (req, res) => { + try { + const invoice = await Invoice.findById(req.params.id); + if (!invoice) return res.status(404).json({ message: "Invoice not found" }); + res.status(200).json(invoice); + } catch (error) { + console.error(`Error fetching invoice by ID: ${error.message}`); + res.status(500).json({ message: "Internal server error" }); + } +}; + +// Update invoice status with validation +const updateInvoiceStatus = async (req, res) => { + const { error, value } = statusUpdateSchema.validate(req.body); + if (error) return res.status(400).json({ message: error.message }); + + try { + const invoice = await Invoice.findByIdAndUpdate( + req.params.id, + { status: value.status }, + { new: true } + ); + if (!invoice) return res.status(404).json({ message: "Invoice not found" }); + res.status(200).json(invoice); + } catch (error) { + console.error(`Error updating invoice status: ${error.message}`); + res.status(500).json({ message: "Internal server error" }); + } +}; + +// Delete an invoice by ID +const deleteInvoice = async (req, res) => { + try { + const invoice = await Invoice.findByIdAndDelete(req.params.id); + if (!invoice) return res.status(404).json({ message: "Invoice not found" }); + res.status(200).json({ message: "Invoice deleted successfully" }); + } catch (error) { + console.error(`Error deleting invoice: ${error.message}`); + res.status(500).json({ message: "Internal server error" }); + } +}; + +// Mark overdue invoices +const markOverdueInvoices = async (req, res) => { + try { + const overdueInvoices = await Invoice.updateMany( + { dueDate: { $lt: new Date() }, status: "Unpaid" }, + { status: "Overdue" } + ); + res + .status(200) + .json({ + message: `${overdueInvoices.nModified} invoices marked as overdue`, + }); + } catch (error) { + console.error(`Error marking overdue invoices: ${error.message}`); + res.status(500).json({ message: "Internal server error" }); + } +}; + +// Summary of invoices by status +const invoiceSummary = async (req, res) => { + const { userId } = req.query; + + try { + const match = userId ? { userId } : {}; + + const summary = await Invoice.aggregate([ + { $match: match }, + { + $group: { + _id: "$status", + totalAmount: { $sum: "$amount" }, + count: { $sum: 1 }, + }, + }, + ]); + + const summaryData = summary.reduce((acc, item) => { + acc[item._id] = { totalAmount: item.totalAmount, count: item.count }; + return acc; + }, {}); + + res.status(200).json(summaryData); + } catch (error) { + console.error(`Error generating invoice summary: ${error.message}`); + res.status(500).json({ message: "Internal server error" }); + } +}; + +// Bulk invoice generation +const generateBulkInvoices = async (req, res) => { + const { invoices } = req.body; + + if (!Array.isArray(invoices) || invoices.length === 0) { + return res.status(400).json({ message: "Invalid invoices data" }); + } + + const validationErrors = invoices + .map((invoiceData, index) => { + const { error } = invoiceSchema.validate(invoiceData); + return error ? { index, message: error.message } : null; + }) + .filter(Boolean); + + if (validationErrors.length > 0) { + return res + .status(400) + .json({ message: "Validation errors", errors: validationErrors }); + } + + try { + const invoiceDocuments = invoices.map((invoiceData) => ({ + ...invoiceData, + invoiceId: `INV${Date.now() + Math.floor(Math.random() * 1000)}`, + })); + + const createdInvoices = await Invoice.insertMany(invoiceDocuments); + res.status(201).json(createdInvoices); + } catch (error) { + console.error(`Error generating bulk invoices: ${error.message}`); + res.status(500).json({ message: "Internal server error" }); + } +}; + +module.exports = { + generateInvoice, + getInvoices, + getInvoiceById, + updateInvoiceStatus, + deleteInvoice, + markOverdueInvoices, + invoiceSummary, + generateBulkInvoices, +}; diff --git a/backend/controllers/shop/sub-controllers/paymentController.js b/backend/controllers/shop/sub-controllers/paymentController.js new file mode 100644 index 00000000..0bc6569d --- /dev/null +++ b/backend/controllers/shop/sub-controllers/paymentController.js @@ -0,0 +1,161 @@ +const Payment = require("../models/payment"); + +// Retrieve all payments +const getAllPayments = async (req, res) => { + try { + const payments = await Payment.find(); + res.status(200).json(payments); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +// Retrieve payment by ID +const getPaymentById = async (req, res) => { + try { + const payment = await Payment.findById(req.params.id); + if (!payment) return res.status(404).json({ message: "Payment not found" }); + res.status(200).json(payment); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +// Create a new payment +const createPayment = async (req, res) => { + const { orderId, userId, amount, paymentMethod, transactionId } = req.body; + + const newPayment = new Payment({ + paymentId: `PAY${Date.now()}`, + orderId, + userId, + amount, + paymentMethod, + transactionId, + }); + + try { + const payment = await newPayment.save(); + res.status(201).json(payment); + } catch (error) { + res.status(400).json({ message: error.message }); + } +}; + +// Update payment status +const updatePaymentStatus = async (req, res) => { + const { paymentStatus } = req.body; + try { + const payment = await Payment.findByIdAndUpdate( + req.params.id, + { paymentStatus }, + { new: true } + ); + if (!payment) return res.status(404).json({ message: "Payment not found" }); + res.status(200).json(payment); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; +const Payment = require("../models/payment"); + +// Update Payment Method +const updatePaymentMethod = async (req, res) => { + const { paymentMethod } = req.body; + try { + if (!["Credit Card", "PayPal", "Bank Transfer"].includes(paymentMethod)) { + return res.status(400).json({ message: "Invalid payment method" }); + } + const payment = await Payment.findByIdAndUpdate( + req.params.id, + { paymentMethod }, + { new: true } + ); + if (!payment) return res.status(404).json({ message: "Payment not found" }); + res.status(200).json(payment); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +// Retrieve Payments by Date Range +const getPaymentsByDateRange = async (req, res) => { + const { startDate, endDate } = req.query; + try { + const payments = await Payment.find({ + paymentDate: { + $gte: new Date(startDate), + $lte: new Date(endDate), + }, + }); + if (!payments.length) + return res + .status(404) + .json({ message: "No payments found in this date range" }); + res.status(200).json(payments); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +// Retrieve Payments by Amount Range +const getPaymentsByAmountRange = async (req, res) => { + const { minAmount, maxAmount } = req.query; + try { + const payments = await Payment.find({ + amount: { $gte: minAmount, $lte: maxAmount }, + }); + if (!payments.length) + return res + .status(404) + .json({ message: "No payments found in this amount range" }); + res.status(200).json(payments); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +// Retrieve Payment by Transaction ID +const getPaymentByTransactionId = async (req, res) => { + try { + const payment = await Payment.findOne({ + transactionId: req.params.transactionId, + }); + if (!payment) + return res + .status(404) + .json({ message: "Payment not found with this transaction ID" }); + res.status(200).json(payment); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +// Count Payments by Status +const countPaymentsByStatus = async (req, res) => { + try { + const counts = await Payment.aggregate([ + { + $group: { + _id: "$paymentStatus", + count: { $sum: 1 }, + }, + }, + ]); + res.status(200).json(counts); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +module.exports = { + getAllPayments, + getPaymentById, + createPayment, + updatePaymentStatus, + updatePaymentMethod, + getPaymentsByDateRange, + getPaymentsByAmountRange, + getPaymentByTransactionId, + countPaymentsByStatus, +}; diff --git a/backend/controllers/shop/sub-controllers/supplierController.js b/backend/controllers/shop/sub-controllers/supplierController.js new file mode 100644 index 00000000..c088b5b5 --- /dev/null +++ b/backend/controllers/shop/sub-controllers/supplierController.js @@ -0,0 +1,176 @@ +const Supplier = require("../models/Supplier"); + +exports.createSupplier = async (req, res) => { + try { + const { name, contactInfo, address, productsSupplied, status } = req.body; + const supplier = new Supplier({ + name, + contactInfo, + address, + productsSupplied, + status, + }); + await supplier.save(); + res.status(201).json(supplier); + } catch (err) { + res.status(500).json({ message: "Error creating supplier", error: err }); + } +}; + +// Get all Suppliers with pagination and sorting +exports.getAllSuppliers = async (req, res) => { + try { + const { page = 1, limit = 10, sort = "createdAt" } = req.query; + const suppliers = await Supplier.find() + .sort(sort) + .skip((page - 1) * limit) + .limit(Number(limit)); + res.json(suppliers); + } catch (err) { + res.status(500).json({ message: "Error fetching suppliers", error: err }); + } +}; + +// Get a Supplier by ID +exports.getSupplierById = async (req, res) => { + try { + const supplier = await Supplier.findById(req.params.id); + if (!supplier) { + return res.status(404).json({ message: "Supplier not found" }); + } + res.json(supplier); + } catch (err) { + res.status(500).json({ message: "Error fetching supplier", error: err }); + } +}; + +// Update a Supplier +exports.updateSupplier = async (req, res) => { + try { + const { name, contactInfo, address, productsSupplied, status } = req.body; + const supplier = await Supplier.findByIdAndUpdate( + req.params.id, + { + name, + contactInfo, + address, + productsSupplied, + status, + updatedAt: Date.now(), + }, + { new: true } + ); + if (!supplier) { + return res.status(404).json({ message: "Supplier not found" }); + } + res.json(supplier); + } catch (err) { + res.status(500).json({ message: "Error updating supplier", error: err }); + } +}; + +// Soft delete a Supplier (set status to inactive) +exports.deleteSupplier = async (req, res) => { + try { + const supplier = await Supplier.findByIdAndUpdate( + req.params.id, + { status: "inactive" }, + { new: true } + ); + if (!supplier) { + return res.status(404).json({ message: "Supplier not found" }); + } + res.json(supplier); + } catch (err) { + res.status(500).json({ message: "Error deleting supplier", error: err }); + } +}; + +const Supplier = require("../models/Supplier"); +const Product = require("../models/Product"); // Assuming there's a Product model + +// Add a product to a supplier +exports.addProductToSupplier = async (req, res) => { + try { + const { productId } = req.body; + const supplier = await Supplier.findById(req.params.id); + if (!supplier) { + return res.status(404).json({ message: "Supplier not found" }); + } + + supplier.productsSupplied.push(productId); + await supplier.save(); + + res.status(200).json(supplier); + } catch (err) { + res + .status(500) + .json({ message: "Error adding product to supplier", error: err }); + } +}; + +// Remove a product from a supplier +exports.removeProductFromSupplier = async (req, res) => { + try { + const { productId } = req.body; + const supplier = await Supplier.findById(req.params.id); + if (!supplier) { + return res.status(404).json({ message: "Supplier not found" }); + } + + supplier.productsSupplied = supplier.productsSupplied.filter( + (product) => product.toString() !== productId + ); + await supplier.save(); + + res.status(200).json(supplier); + } catch (err) { + res + .status(500) + .json({ message: "Error removing product from supplier", error: err }); + } +}; + +// Get all products supplied by a supplier +exports.getProductsSupplied = async (req, res) => { + try { + const supplier = await Supplier.findById(req.params.id).populate( + "productsSupplied" + ); + if (!supplier) { + return res.status(404).json({ message: "Supplier not found" }); + } + res.json(supplier.productsSupplied); + } catch (err) { + res.status(500).json({ message: "Error fetching products", error: err }); + } +}; + +exports.searchSuppliers = async (req, res) => { + try { + const { name, status, email, phone } = req.query; + const query = {}; + + if (name) query.name = { $regex: name, $options: "i" }; + if (status) query.status = status; + if (email) query["contactInfo.email"] = email; + if (phone) query["contactInfo.phone"] = phone; + + const suppliers = await Supplier.find(query); + res.json(suppliers); + } catch (err) { + res.status(500).json({ message: "Error searching suppliers", error: err }); + } +}; + +exports.getSuppliersByStatus = async (req, res) => { + try { + const { status } = req.params; + const suppliers = await Supplier.find({ status }); + res.json(suppliers); + } catch (err) { + res + .status(500) + .json({ message: "Error fetching suppliers by status", error: err }); + } +}; diff --git a/backend/middleware/sub-ware/authSupplierMiddleware.js b/backend/middleware/sub-ware/authSupplierMiddleware.js new file mode 100644 index 00000000..adce51f9 --- /dev/null +++ b/backend/middleware/sub-ware/authSupplierMiddleware.js @@ -0,0 +1,10 @@ +const isAdmin = (req, res, next) => { + // Assuming user object is attached to request after authentication + if (req.user && req.user.role === "admin") { + next(); + } else { + return res.status(403).json({ message: "Access forbidden: Admins only" }); + } +}; + +module.exports = isAdmin; diff --git a/backend/middleware/sub-ware/validatePaymentInvoice.js b/backend/middleware/sub-ware/validatePaymentInvoice.js new file mode 100644 index 00000000..e208773e --- /dev/null +++ b/backend/middleware/sub-ware/validatePaymentInvoice.js @@ -0,0 +1,25 @@ +const validatePaymentData = (req, res, next) => { + const { orderId, userId, amount, paymentMethod, transactionId } = req.body; + + if (!orderId || !userId || !amount || !paymentMethod || !transactionId) { + return res + .status(400) + .json({ message: "All fields are required for payment" }); + } + + next(); +}; + +const validateInvoiceData = (req, res, next) => { + const { orderId, userId, amount, dueDate, paymentId } = req.body; + + if (!orderId || !userId || !amount || !dueDate || !paymentId) { + return res + .status(400) + .json({ message: "All fields are required for invoice" }); + } + + next(); +}; + +module.exports = { validatePaymentData, validateInvoiceData }; diff --git a/backend/middleware/sub-ware/validationMiddleware.js b/backend/middleware/sub-ware/validationMiddleware.js new file mode 100644 index 00000000..c569037d --- /dev/null +++ b/backend/middleware/sub-ware/validationMiddleware.js @@ -0,0 +1,28 @@ +const Joi = require('joi'); + +// Validation schema for supplier data +const supplierValidationSchema = Joi.object({ + name: Joi.string().required(), + contactInfo: Joi.object({ + email: Joi.string().email().required(), + phone: Joi.string().required(), + }).required(), + address: Joi.object({ + street: Joi.string().required(), + city: Joi.string().required(), + state: Joi.string().required(), + postalCode: Joi.string().required(), + }).required(), + productsSupplied: Joi.array().items(Joi.string()).required(), + status: Joi.string().valid('active', 'inactive').default('active'), +}); + +const validateSupplierData = (req, res, next) => { + const { error } = supplierValidationSchema.validate(req.body); + if (error) { + return res.status(400).json({ message: 'Validation Error', details: error.details }); + } + next(); +}; + +module.exports = validateSupplierData; diff --git a/backend/model/shop/sub-model/Supplier.js b/backend/model/shop/sub-model/Supplier.js new file mode 100644 index 00000000..7d466d78 --- /dev/null +++ b/backend/model/shop/sub-model/Supplier.js @@ -0,0 +1,58 @@ +const mongoose = require("mongoose"); + +const supplierSchema = new mongoose.Schema( + { + name: { + type: String, + required: true, + }, + contactInfo: { + email: { + type: String, + required: true, + unique: true, + }, + phone: { + type: String, + required: true, + }, + }, + address: { + street: { + type: String, + required: true, + }, + city: { + type: String, + required: true, + }, + state: { + type: String, + required: true, + }, + postalCode: { + type: String, + required: true, + }, + }, + productsSupplied: [ + { type: mongoose.Schema.Types.ObjectId, ref: "Product" }, + ], + status: { + type: String, + required: true, + default: "active", + }, + createdAt: { + type: Date, + default: Date.now, + }, + updatedAt: { + type: Date, + default: Date.now, + }, + }, + { timestamps: true } +); + +module.exports = mongoose.model("Supplier", supplierSchema); diff --git a/backend/model/shop/sub-model/invoice.js b/backend/model/shop/sub-model/invoice.js new file mode 100644 index 00000000..5ea2f010 --- /dev/null +++ b/backend/model/shop/sub-model/invoice.js @@ -0,0 +1,47 @@ +const mongoose = require("mongoose"); + + +const invoiceSchema = new mongoose.Schema( + { + invoiceId: { type: String, required: true, unique: true }, + orderId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Order", + required: true, + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + index: true, // Adds index for faster query on user invoices + }, + amount: { type: Number, required: true, min: 0 }, + invoiceDate: { type: Date, default: Date.now }, + dueDate: { + type: Date, + required: true, + default: function () { + return new Date(+new Date() + 7 * 24 * 60 * 60 * 1000); // Default due date is 7 days from invoice date + }, + }, + status: { + type: String, + enum: ["Paid", "Unpaid", "Overdue", "Cancelled"], + default: "Unpaid", + }, + paymentId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Payment", + sparse: true, // Allows null value indexing for unpaid invoices + }, + }, + { timestamps: true } +); + +// Virtual field to check if invoice is overdue +invoiceSchema.virtual("isOverdue").get(function () { + return this.status === "Unpaid" && this.dueDate < new Date(); +}); + +const Invoice = mongoose.model("Invoice", invoiceSchema); +module.exports = Invoice; diff --git a/backend/model/shop/sub-model/payment.js b/backend/model/shop/sub-model/payment.js new file mode 100644 index 00000000..44182dd7 --- /dev/null +++ b/backend/model/shop/sub-model/payment.js @@ -0,0 +1,34 @@ +const mongoose = require("mongoose"); + +const paymentSchema = new mongoose.Schema( + { + paymentId: { type: String, required: true, unique: true }, + orderId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Order", + required: true, + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + amount: { type: Number, required: true }, + paymentMethod: { + type: String, + enum: ["Credit Card", "PayPal", "Bank Transfer"], + required: true, + }, + paymentStatus: { + type: String, + default: "Pending", + enum: ["Pending", "Completed", "Failed"], + }, + paymentDate: { type: Date, default: Date.now }, + transactionId: { type: String, required: true }, + }, + { timestamps: true } +); + +const Payment = mongoose.model("Payment", paymentSchema); +module.exports = Payment; diff --git a/backend/routes/sub-routes/invoiceRoutes.js b/backend/routes/sub-routes/invoiceRoutes.js new file mode 100644 index 00000000..a6f30c3d --- /dev/null +++ b/backend/routes/sub-routes/invoiceRoutes.js @@ -0,0 +1,39 @@ +const express = require("express"); +const router = express.Router(); + +const { + generateInvoice, + getInvoices, + getInvoiceById, + updateInvoiceStatus, + deleteInvoice, + markOverdueInvoices, + invoiceSummary, + generateBulkInvoices, +} = require("../controllers/invoiceController"); + +// Route to generate a new invoice after payment confirmation +router.post("/generate", generateInvoice); + +// Route to retrieve invoices by user, order, status, or date range with pagination and sorting +router.get("/", getInvoices); + +// Route to retrieve a specific invoice by its ID +router.get("/:id", getInvoiceById); + +// Route to update an invoice's status +router.patch("/:id/status", updateInvoiceStatus); + +// Route to delete an invoice by its ID +router.delete("/:id", deleteInvoice); + +// Route to mark overdue invoices (based on the current date) +router.patch("/mark-overdue", markOverdueInvoices); + +// Route to get an invoice summary (by status, optionally by user) +router.get("/summary", invoiceSummary); + +// Route to generate bulk invoices at once +router.post("/generate-bulk", generateBulkInvoices); + +module.exports = router; diff --git a/backend/routes/sub-routes/paymentRoutes.js b/backend/routes/sub-routes/paymentRoutes.js new file mode 100644 index 00000000..47c456df --- /dev/null +++ b/backend/routes/sub-routes/paymentRoutes.js @@ -0,0 +1,34 @@ +const express = require("express"); +const router = express.Router(); +const { + getAllPayments, + getPaymentById, + getPaymentsByUserId, + getPaymentsByStatus, + createPayment, + updatePaymentStatus, + deletePayment, + updatePaymentMethod, + getPaymentsByDateRange, + getPaymentsByAmountRange, + getPaymentByTransactionId, + countPaymentsByStatus, +} = require("../controllers/paymentController"); + +// Payment routes +router.get("/", getAllPayments); // Retrieve all payments +router.get("/:id", getPaymentById); // Retrieve payment by ID +router.get("/user/:userId", getPaymentsByUserId); // Retrieve payments by User ID +router.get("/status/:status", getPaymentsByStatus); // Retrieve payments by Status +router.post("/", createPayment); // Create a new payment +router.patch("/:id/status", updatePaymentStatus); // Update payment status +router.patch("/:id/method", updatePaymentMethod); // Update payment method +router.delete("/:id", deletePayment); // Delete payment + +// Additional routes for edge cases +router.get("/date-range", getPaymentsByDateRange); // Retrieve payments by date range +router.get("/amount-range", getPaymentsByAmountRange); // Retrieve payments by amount range +router.get("/transaction/:transactionId", getPaymentByTransactionId); // Retrieve payment by transaction ID +router.get("/count/status", countPaymentsByStatus); // Count payments by status + +module.exports = router; diff --git a/backend/routes/sub-routes/supplierRoutes.js b/backend/routes/sub-routes/supplierRoutes.js new file mode 100644 index 00000000..b0fd6295 --- /dev/null +++ b/backend/routes/sub-routes/supplierRoutes.js @@ -0,0 +1,50 @@ +const express = require("express"); +const router = express.Router(); +const supplierController = require("../controllers/supplierController"); +const productController = require("../controllers/productController"); +const supplierSearchController = require("../controllers/supplierSearchController"); +const supplierFilterController = require("../controllers/supplierFilterController"); +const validateSupplierData = require("../middlewares/validationMiddleware"); +const isAdmin = require("../middlewares/authMiddleware"); + +// Routes for suppliers +router.post( + "/api/suppliers", + isAdmin, + validateSupplierData, + supplierController.createSupplier +); // Admin only +router.get("/api/suppliers", supplierController.getAllSuppliers); +router.get("/api/suppliers/:id", supplierController.getSupplierById); +router.put( + "/api/suppliers/:id", + isAdmin, + validateSupplierData, + supplierController.updateSupplier +); // Admin only +router.delete("/api/suppliers/:id", isAdmin, supplierController.deleteSupplier); // Admin only + +// Routes for managing products supplied by a supplier +router.post( + "/api/suppliers/:id/products", + isAdmin, + productController.addProductToSupplier +); // Admin only +router.delete( + "/api/suppliers/:id/products", + isAdmin, + productController.removeProductFromSupplier +); // Admin only +router.get( + "/api/suppliers/:id/products", + productController.getProductsSupplied +); + +// Routes for searching and filtering suppliers +router.get("/api/suppliers/search", supplierSearchController.searchSuppliers); +router.get( + "/api/suppliers/status/:status", + supplierFilterController.getSuppliersByStatus +); + +module.exports = router; diff --git a/frontend/src/AgroShopAI/components/Pages/Admin-Dashboard.jsx b/frontend/src/AgroShopAI/components/Pages/Admin-Dashboard.jsx index c35d0fc1..61dbc155 100644 --- a/frontend/src/AgroShopAI/components/Pages/Admin-Dashboard.jsx +++ b/frontend/src/AgroShopAI/components/Pages/Admin-Dashboard.jsx @@ -5,6 +5,7 @@ import { DashboardStats } from "./components/DashboardStats"; import { useState } from "react"; import StatisticComponent from "./components/StatisticComponent"; import ReturnPanel from "./components/ReturnPage"; +import AdminProductManagement from "./ProductManagement"; export default function AdminDashboard() { const [activeView, setActiveView] = useState("dashboard"); // Track active view @@ -23,6 +24,7 @@ export default function AdminDashboard() { {activeView === "grievances" && } {activeView === "analytics" && } {activeView === "return" && } + {activeView === "product" && } ); diff --git a/frontend/src/AgroShopAI/components/Pages/Affiliate.jsx b/frontend/src/AgroShopAI/components/Pages/Affiliate.jsx new file mode 100644 index 00000000..1ea85323 --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/Affiliate.jsx @@ -0,0 +1,43 @@ +import React, { useState } from 'react'; +import AffiliateHeader from './components/AffiliateHeader'; +import AffiliateJoinSection from './components/AffiliateJoinSection'; +import AffiliateDashboard from './components/AffiliateDashboard'; +import AffiliateMarketingMaterials from './components/AffiliateMarketingMaterials'; +import AffiliateTerms from './components/AffiliateTerms'; + + +export default function AffiliateProgramPage() { + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [dateRange, setDateRange] = useState('last7days'); + + const dummyData = { + clicks: 1250, + signUps: 75, + conversions: 30, + commissions: 450.0, + }; + + const handleLogin = (e) => { + e.preventDefault(); + setIsLoggedIn(true); + }; + + return ( +
+ +
+ + + + +
+ +
+ ); +} diff --git a/frontend/src/AgroShopAI/components/Pages/ProductForm.jsx b/frontend/src/AgroShopAI/components/Pages/ProductForm.jsx new file mode 100644 index 00000000..492e78d2 --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/ProductForm.jsx @@ -0,0 +1,202 @@ +// ProductForm.js +import React, { useState } from 'react' + +export default function ProductForm({ selectedProduct, isCreating, isEditing, handleSubmit }) { + const [product, setProduct] = useState(selectedProduct) + const [errors, setErrors] = useState({}) + + const handleInputChange = (e) => { + const { name, value } = e.target + setProduct(prev => ({ + ...prev, + [name]: name === 'price' || name === 'stockQuantity' ? parseFloat(value) : value + })) + setErrors(prev => ({ ...prev, [name]: '' })) + } + + const handleImageUpload = (e) => { + if (e.target.files) { + const newImages = Array.from(e.target.files).map(file => URL.createObjectURL(file)) + setProduct(prev => ({ + ...prev, + images: [...prev.images, ...newImages] + })) + } + } + + const removeImage = (index) => { + const newImages = product.images.filter((_, i) => i !== index) + setProduct(prev => ({ + ...prev, + images: newImages + })) + } + + const validateForm = () => { + const newErrors = {} + if (!product.name) newErrors.name = 'Product name is required' + if (!product.category) newErrors.category = 'Category is required' + if (product.price <= 0) newErrors.price = 'Price must be greater than 0' + if (!product.description) newErrors.description = 'Description is required' + if (!product.sku) newErrors.sku = 'SKU is required' + if (product.stockQuantity < 0) newErrors.stockQuantity = 'Stock quantity cannot be negative' + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const onSubmit = (e) => { + e.preventDefault() + if (!validateForm()) return + handleSubmit(product) + } + + return ( +
+

+ {isCreating ? 'Create New Product' : 'Edit Product'} +

+
+
+ + + {errors.name &&

{errors.name}

} +
+ +
+ + + {errors.category &&

{errors.category}

} +
+ +
+ + + {errors.price &&

{errors.price}

} +
+ +
+ + + {errors.description &&

{errors.description}

} +
+ +
+ + + {errors.sku &&

{errors.sku}

} +
+ +
+ + + {errors.stockQuantity &&

{errors.stockQuantity}

} +
+ +
+ + +
+ +
+ {selectedProduct.images.map((image, index) => ( +
+ {`Product + +
+ ))} +
+ +
+ + +
+
+
+ ) +} diff --git a/frontend/src/AgroShopAI/components/Pages/ProductList.jsx b/frontend/src/AgroShopAI/components/Pages/ProductList.jsx new file mode 100644 index 00000000..aec40dcc --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/ProductList.jsx @@ -0,0 +1,39 @@ +// ProductList.js +import React from 'react' + +export default function ProductList({ products, handleSelectProduct, handleCreateNewProduct }) { + return ( +
+
+

Product List

+ +
+
+
    + {products.map(product => ( +
  • +
    + {product.name} +
    +

    {product.name}

    +

    ${product.price.toFixed(2)} - Stock: {product.stockQuantity}

    +
    +
    + +
  • + ))} +
+
+
+ ) +} diff --git a/frontend/src/AgroShopAI/components/Pages/ProductManagement.jsx b/frontend/src/AgroShopAI/components/Pages/ProductManagement.jsx new file mode 100644 index 00000000..c00dc2c0 --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/ProductManagement.jsx @@ -0,0 +1,114 @@ +// AdminProductManagement.js +import React, { useState } from 'react' +import ProductList from './ProductList' +import ProductForm from './ProductForm' + +const dummyProducts = [ + { + id: '1', + name: 'Organic Apples', + category: 'fruits', + price: 2.99, + description: 'Fresh, juicy organic apples from local farms.', + sku: 'APP001', + stockQuantity: 100, + images: ['/placeholder.svg?height=150&width=150', '/placeholder.svg?height=150&width=150'] + }, + { + id: '2', + name: 'Farm Fresh Eggs', + category: 'dairy', + price: 4.50, + description: 'Free-range eggs from happy chickens.', + sku: 'EGG002', + stockQuantity: 50, + images: ['/placeholder.svg?height=150&width=150'] + }, + { + id: '3', + name: 'Organic Carrots', + category: 'vegetables', + price: 1.99, + description: 'Crunchy and sweet organic carrots.', + sku: 'CAR003', + stockQuantity: 200, + images: ['/placeholder.svg?height=150&width=150'] + } +] + +export default function AdminProductManagement() { + const [products, setProducts] = useState(dummyProducts) + const [selectedProduct, setSelectedProduct] = useState(null) + const [isEditing, setIsEditing] = useState(false) + const [isCreating, setIsCreating] = useState(false) + + const handleSelectProduct = (product) => { + setSelectedProduct(product) + setIsEditing(true) + setIsCreating(false) + } + + const handleCreateNewProduct = () => { + setSelectedProduct({ + id: `NEW${Date.now()}`, // Generate new unique ID + name: '', + category: '', + price: 0, + description: '', + sku: '', + stockQuantity: 0, + images: [] + }) + setIsCreating(true) + setIsEditing(false) + } + + const handleSubmit = (product) => { + if (isCreating) { + setProducts([...products, product]) + } else { + const updatedProducts = products.map(p => + p.id === product.id ? product : p + ) + setProducts(updatedProducts) + } + setIsEditing(false) + setIsCreating(false) + setSelectedProduct(null) + } + + return ( +
+
+
+

AgroShop Admin

+

Product Management Dashboard

+
+ +
+ {/* Product List */} + + + {/* Product Edit/Create Form */} + {(isEditing || isCreating) && selectedProduct && ( + + )} +
+ {/* Flashy Banner */} +
+

Boost Your Agricultural Business!

+

Manage your products efficiently with AgroShop's powerful admin tools.

+
+
+
+ ) +} diff --git a/frontend/src/AgroShopAI/components/Pages/Trending.jsx b/frontend/src/AgroShopAI/components/Pages/Trending.jsx new file mode 100644 index 00000000..a3b34f2a --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/Trending.jsx @@ -0,0 +1,120 @@ +import React, { useState } from "react"; +import Header from "./components/THeader"; +import Banner from "./components/Banner"; +import SearchAndSort from "./components/SearchAndSort"; +import ProductGrid from "./components/ProductGrid"; + +export default function TrendingProductsPage() { + const [sortBy, setSortBy] = useState("trending"); + const [searchTerm, setSearchTerm] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + // Filtering products based on search term + const filteredProducts = trendingProducts.filter((product) => + product.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + // Sorting products based on selected criteria + const sortedProducts = [...filteredProducts].sort((a, b) => { + if (sortBy === "price") return a.price - b.price; + if (sortBy === "rating") return b.rating - a.rating; + return 0; + }); + + // Simulate loading state + const fetchData = async () => { + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + }, 2000); + }; + + React.useEffect(() => { + fetchData(); + }, []); + + return ( +
+
+ +
+ {/* Search and Sort Component */} + + {/* Loading state and Product Grid */} + {isLoading ? ( +
+ Loading... +
+ ) : ( + + )} +
+
+ ); +} + +const trendingProducts = [ + { + id: 1, + name: "Smart Irrigation System", + price: 299.99, + rating: 4.8, + reviews: 120, + image: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTTkXzsfqi9x6H46rC1Ir7k2WEQ1V0lVv8ONA&s", + tag: "Trending Now", + }, + { + id: 2, + name: "Organic Fertilizer Pack", + price: 49.99, + rating: 4.5, + reviews: 85, + image: "https://m.media-amazon.com/images/I/91Y3l-BVm-L.jpg", + tag: "Hot Deal", + }, + { + id: 3, + name: "Drone Crop Sprayer", + price: 1299.99, + rating: 4.9, + reviews: 200, + image: + "https://5.imimg.com/data5/ANDROID/Default/2022/4/AA/WY/ED/14166347/product-jpeg-500x500.jpg", + tag: "Top Rated", + }, + { + id: 4, + name: "Soil pH Tester", + price: 29.99, + rating: 4.2, + reviews: 60, + image: "https://m.media-amazon.com/images/I/71933Pfn55L.jpg", + tag: "Trending Now", + }, + { + id: 5, + name: "Hydroponic Growing Kit", + price: 199.99, + rating: 4.7, + reviews: 150, + image: + "https://m.media-amazon.com/images/I/81n3GQOCOQL._AC_UF1000,1000_QL80_.jpg", + tag: "Hot Deal", + }, + { + id: 6, + name: "Weather Station", + price: 149.99, + rating: 4.6, + reviews: 95, + image: + "https://5.imimg.com/data5/YN/KT/YB/SELLER-5098190/weather-station.jpg", + tag: "Top Rated", + }, +]; diff --git a/frontend/src/AgroShopAI/components/Pages/components/AffiliateDashboard.jsx b/frontend/src/AgroShopAI/components/Pages/components/AffiliateDashboard.jsx new file mode 100644 index 00000000..bb1bd40c --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/components/AffiliateDashboard.jsx @@ -0,0 +1,63 @@ +import React from 'react'; + +export default function AffiliateDashboard({ + isLoggedIn, + handleLogin, + dateRange, + setDateRange, + dummyData, +}) { + return ( +
+

Referral Tracking Dashboard

+ {!isLoggedIn ? ( +
+
+ + +
+
+ + +
+ +
+ ) : ( +
+
+

Your Performance

+ +
+
+
+

Clicks

+

{dummyData.clicks}

+
+
+

Sign-ups

+

{dummyData.signUps}

+
+
+

Conversions

+

{dummyData.conversions}

+
+
+

Commissions

+

${dummyData.commissions.toFixed(2)}

+
+
+
+ )} +
+ ); +} diff --git a/frontend/src/AgroShopAI/components/Pages/components/AffiliateHeader.jsx b/frontend/src/AgroShopAI/components/Pages/components/AffiliateHeader.jsx new file mode 100644 index 00000000..7e01ec0b --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/components/AffiliateHeader.jsx @@ -0,0 +1,67 @@ +import React from 'react'; + +export default function AffiliateHeader() { + return ( +
+
+ {/* Logo and Title */} +
+ AgroShop Logo +
+

+ AgroShop Affiliate Program +

+

+ Partner with us to grow together in sustainable agriculture +

+
+
+ + {/* Navigation Links */} + + + {/* Call to Action */} + +
+ + {/* Subtext */} +
+

Earn commissions, access exclusive materials, and help promote sustainable farming practices!

+
+
+ ); +} diff --git a/frontend/src/AgroShopAI/components/Pages/components/AffiliateJoinSection.jsx b/frontend/src/AgroShopAI/components/Pages/components/AffiliateJoinSection.jsx new file mode 100644 index 00000000..14c49bb7 --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/components/AffiliateJoinSection.jsx @@ -0,0 +1,50 @@ +import React from 'react'; + +export default function AffiliateJoinSection() { + return ( +
+
+

Join Our Affiliate Program

+

+ Become a partner with AgroShop and earn commissions by promoting our high-quality agricultural products. + Our program offers competitive rates and simple tools to help you maximize your earnings while supporting + sustainable agriculture. +

+
+ +
+

How It Works

+
    +
  1. Sign up for our affiliate program with quick and easy registration.
  2. +
  3. Receive your unique referral link to track sales and commissions.
  4. +
  5. Promote AgroShop products through your website, blog, or social media.
  6. +
  7. Earn a commission on every sale made through your link.
  8. +
+
+ +
+

Program Benefits

+
    +
  • 10% commission on all sales - one of the highest in the industry.
  • +
  • 30-day cookie duration ensures you get credit for returning customers.
  • +
  • Monthly payouts directly to your account.
  • +
  • Access to exclusive promotions, materials, and early product launches.
  • +
  • Dedicated support to help you grow your audience and earnings.
  • +
+
+ +
+ +
+ +
+

+ Have questions? Contact us for more information + about how the AgroShop Affiliate Program can work for you. +

+
+
+ ); +} diff --git a/frontend/src/AgroShopAI/components/Pages/components/AffiliateMarketingMaterials.jsx b/frontend/src/AgroShopAI/components/Pages/components/AffiliateMarketingMaterials.jsx new file mode 100644 index 00000000..41599960 --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/components/AffiliateMarketingMaterials.jsx @@ -0,0 +1,62 @@ +import React from 'react'; + +export default function AffiliateMarketingMaterials() { + return ( +
+
+

Marketing Materials

+

+ Access a range of professional marketing tools designed to help you promote AgroShop and increase your earnings. + Use these assets to engage your audience and drive conversions. +

+
+ +
+
+

Banners

+

Choose from a variety of banners tailored to promote AgroShop products effectively.

+ Banner Example + +
+ +
+

Referral Link

+

Use your unique referral link below to start earning commissions:

+
+ + +
+
+ +
+

Social Media Content

+

Ready-to-post content for Facebook, Instagram, and Twitter.

+ +
+ +
+

Email Templates

+

Engage your subscribers with pre-written, customizable email templates.

+ +
+
+ +
+

Need custom materials? Contact our support team for assistance.

+
+
+ ); +} diff --git a/frontend/src/AgroShopAI/components/Pages/components/AffiliateTerms.jsx b/frontend/src/AgroShopAI/components/Pages/components/AffiliateTerms.jsx new file mode 100644 index 00000000..0db6a2a1 --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/components/AffiliateTerms.jsx @@ -0,0 +1,24 @@ +import React from 'react' + +export default function AffiliateTerms() { + return ( +
+

Terms and Conditions

+
+

+ By joining the AgroShop Affiliate Program, you agree to the following terms and conditions: +

+
    +
  • You will earn a 10% commission on all qualifying sales.
  • +
  • AgroShop reserves the right to modify the commission rate and terms at any time.
  • +
  • Affiliate links have a 30-day cookie duration.
  • +
  • Payment is processed monthly, with a minimum payout threshold.
  • +
  • Affiliates must adhere to AgroShop’s branding guidelines.
  • +
+

+ For any questions, please contact our affiliate support team. +

+
+
+ ) +} diff --git a/frontend/src/AgroShopAI/components/Pages/components/Banner.jsx b/frontend/src/AgroShopAI/components/Pages/components/Banner.jsx new file mode 100644 index 00000000..8c1d21e1 --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/components/Banner.jsx @@ -0,0 +1,37 @@ +export default function Banner() { + return ( +
+
+ {/* Heading */} +

+ Trending in AgroTech +

+ + {/* Description */} +

+ Discover the latest and most popular agricultural innovations! Stay + ahead with new technologies. +

+ + {/* Call to Action */} +
+ + +
+ + {/* Image Section (Optional for added visual appeal) */} +
+ Agriculture Innovations +
+
+
+ ); +} diff --git a/frontend/src/AgroShopAI/components/Pages/components/Header.jsx b/frontend/src/AgroShopAI/components/Pages/components/Header.jsx index e8f09a18..f41589d5 100644 --- a/frontend/src/AgroShopAI/components/Pages/components/Header.jsx +++ b/frontend/src/AgroShopAI/components/Pages/components/Header.jsx @@ -2,7 +2,7 @@ export const Header = () => { return (
-

Grievance Redressal

+

Admin Dashboard

Welcome, Admin + {sortedProducts.map((product) => ( + + ))} +
+ ); +} diff --git a/frontend/src/AgroShopAI/components/Pages/components/SearchAndSort.jsx b/frontend/src/AgroShopAI/components/Pages/components/SearchAndSort.jsx new file mode 100644 index 00000000..7daa7f2f --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/components/SearchAndSort.jsx @@ -0,0 +1,63 @@ +import { Search, ChevronDown } from "lucide-react"; + +export default function SearchAndSort({ + searchTerm, + setSearchTerm, + sortBy, + setSortBy, +}) { + return ( +
+ {/* Search Input */} +
+ + setSearchTerm(e.target.value)} + aria-label="Search products" + /> + +
+ + {/* Sort By Dropdown */} +
+ + + +
+ + {/* Filter Button */} +
+ +
+
+ ); +} diff --git a/frontend/src/AgroShopAI/components/Pages/components/Sidebar.jsx b/frontend/src/AgroShopAI/components/Pages/components/Sidebar.jsx index 92487471..368b45bf 100644 --- a/frontend/src/AgroShopAI/components/Pages/components/Sidebar.jsx +++ b/frontend/src/AgroShopAI/components/Pages/components/Sidebar.jsx @@ -5,7 +5,8 @@ import { Settings, Users, TrendingUp, - CornerDownLeft + CornerDownLeft, + FolderKanban } from "lucide-react"; export const Sidebar = ({ onViewChange, activeView }) => { @@ -83,6 +84,17 @@ export const Sidebar = ({ onViewChange, activeView }) => { Return +
  • + onViewChange("product")} + className={`flex items-center py-2 px-4 hover:bg-green-700 rounded ${ + activeView === "product" ? "bg-green-700" : "" + }`} + > + Product Management + +
  • + +
  • + ) +} diff --git a/frontend/src/AgroShopAI/components/Pages/components/TProductCard.jsx b/frontend/src/AgroShopAI/components/Pages/components/TProductCard.jsx new file mode 100644 index 00000000..c9d405a3 --- /dev/null +++ b/frontend/src/AgroShopAI/components/Pages/components/TProductCard.jsx @@ -0,0 +1,59 @@ +import { Star } from "lucide-react"; + +export default function ProductCard({ product }) { + return ( +
    + {/* Product Image and Tag */} +
    + {product.name} + + {product.tag} + +
    + + {/* Product Details */} +
    +
    +

    + {product.name} +

    + + {product.stock} in stock + +
    + + {/* Rating and Reviews */} +
    + + + {product.rating} + + + ({product.reviews} reviews) + +
    + + {/* Price */} +

    + ${product.price.toFixed(2)} +

    + + {/* Add to Cart Button */} + +
    + + {/* Hover Effect */} +
    + + Free shipping on orders over $50! + +
    +
    + ); +} diff --git a/frontend/src/MainContent.jsx b/frontend/src/MainContent.jsx index 3268d122..ab665580 100644 --- a/frontend/src/MainContent.jsx +++ b/frontend/src/MainContent.jsx @@ -95,6 +95,11 @@ import FlashSale from './AgroShopAI/components/Pages/components/SalePage'; import WaterManagement from './components/models/WaterManagement'; import BundledProducts from './AgroShopAI/components/Pages/BulkPage'; +import TrendingProductsPage from './AgroShopAI/components/Pages/Trending'; + +import AffiliateProgramPage from './AgroShopAI/components/Pages/Affiliate'; + + const MainContent = () => { @@ -208,12 +213,16 @@ const MainContent = () => { } /> } /> - + } /> } /> } /> + } /> + } /> + + {checkShop ? :