diff --git a/package-lock.json b/package-lock.json index a2ce2c04..0a49d0f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "cloudinary": "^2.4.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "date-fns": "^4.1.0", "dotenv": "^16.4.5", "express": "^4.21.1", "express-session": "^1.18.1", @@ -5544,6 +5545,15 @@ "integrity": "sha512-RhGS1u2vavcO7ay7ZNAPo4xeDh/VYeGof3x5ZLJBQgYhLegxr3s5IykvWmJ94FTU6mcbtp4sloqZ54mP6R4Utw==", "license": "BSD-3-Clause" }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/package.json b/package.json index 483880de..9abfd916 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "cloudinary": "^2.4.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "date-fns": "^4.1.0", "dotenv": "^16.4.5", "express": "^4.21.1", "express-session": "^1.18.1", diff --git a/src/controllers/admin/index.ts b/src/controllers/admin/index.ts new file mode 100644 index 00000000..2441c623 --- /dev/null +++ b/src/controllers/admin/index.ts @@ -0,0 +1,98 @@ +import { Request, Response } from "express"; +import Admin from "../../models/Admin"; +import { decodeAdminToken, generateAdminToken } from "../../helpers/jwt"; + +export const registerAdmin = async (req: Request, res: Response) => { + const { email, password } = req.body; + + try { + const existingAdmin = await Admin.findOne({ email }); + if (existingAdmin) { + return res.status(400).json({ message: "Admin already exists." }); + } + + const newAdmin = await Admin.create({ email, password }); + return res.status(201).json({ message: "Admin registered successfully.", admin: newAdmin }); + } catch (error) { + return res.status(500).json({ message: "An error occurred.", error }); + } +}; + +export const loginAdmin = async (req: Request, res: Response) => { + const { email, password } = req.body; + + try { + const admin = await Admin.findOne({ email }); + if (!admin) { + return res.status(404).json({ message: "Admin not found." }); + } + + const isPasswordMatch = admin.password === password + if (!isPasswordMatch) { + return res.status(401).json({ message: "Invalid credentials." }); + } + + const token = generateAdminToken(admin) + return res.status(200).json({ message: "Login successful.", token,admin }); + } catch (error) { + return res.status(500).json({ message: "An error occurred.", error }); + } +}; + +export const deactivateAdmin = async (req: Request, res: Response) => { + const { id } = req.params; + try { + const admin = await Admin.findByIdAndUpdate(id, { isActive: false }, { new: true }); + if (!admin) { + return res.status(404).json({ message: "Admin not found." }); + } + + return res.status(200).json({ message: "Admin deactivated successfully.", admin }); + } catch (error) { + return res.status(500).json({ message: "An error occurred.", error }); + } +}; + +export const activateAdmin = async (req: Request, res: Response) => { + const { id } = req.params; + + try { + const admin = await Admin.findByIdAndUpdate(id, { isActive: true }, { new: true }); + if (!admin) { + return res.status(404).json({ message: "Admin not found." }); + } + + return res.status(200).json({ message: "Admin activated successfully.", admin }); + } catch (error) { + return res.status(500).json({ message: "An error occurred.", error }); + } +}; + +export const getAdminInfo = async (req: Request, res: Response) => { + const token = req.headers.authorization?.split(" ")[1]; + + if (!token) { + return res.status(401).json({ message: "Authorization token missing." }); + } + + try { + const admin = await decodeAdminToken(token) + + if (!admin) { + return res.status(404).json({ message: "Admin not found." }); + } + + return res.status(200).json(admin); + } catch (error) { + return res.status(401).json({ message: "Invalid or expired token.", error }); + } +}; + +export const getAlladmins = async (req: Request, res: Response) => { + try { + const admins = await Admin.find(); + return res.status(200).json(admins); + } catch (error) { + return res.status(500).json({ message: "An error occurred while fetching admins.", error }); + } +} \ No newline at end of file diff --git a/src/controllers/statistics/restricted-countries-statistics.ts b/src/controllers/statistics/restricted-countries-statistics.ts new file mode 100644 index 00000000..db35d8da --- /dev/null +++ b/src/controllers/statistics/restricted-countries-statistics.ts @@ -0,0 +1,50 @@ +import { Request, Response } from "express"; +import SanctionedGeoBoundary from "../../models/misc/SanctionedGeoBoundary"; + +export const getRestrictedAreaStats = async (req: Request, res: Response) => { + try { + const restrictedAreas = await SanctionedGeoBoundary.find() + return res.status(200).json({ + restrictedAreas + }); + } catch (error) { + return res.status(500).json({ + success: false, + message: "Error fetching restricted areas", + error: error + }); + } +} + +export const createSanctionedRegion = async (req: Request, res: Response) => { + try { + const { geometry, properties } = req.body; + + const newSanctionedRegion = await SanctionedGeoBoundary.create({ + type: "Feature", + geometry: { + type: "Polygon", + coordinates: geometry.coordinates + }, + properties: { + shapeName: properties.shapeName, + shapeISO: properties.shapeISO, + shapeID: properties.shapeID, + shapeGroup: properties.shapeGroup, + shapeType: properties.shapeType + } + }); + + return res.status(201).json({ + success: true, + message: "Sanctioned region created successfully", + data: newSanctionedRegion + }); + } catch (error) { + return res.status(500).json({ + success: false, + message: "Error creating sanctioned region", + error: error + }); + } +} \ No newline at end of file diff --git a/src/controllers/statistics/review-statistics.ts b/src/controllers/statistics/review-statistics.ts new file mode 100644 index 00000000..21a486f0 --- /dev/null +++ b/src/controllers/statistics/review-statistics.ts @@ -0,0 +1,83 @@ +import { Request, Response } from "express"; +import ReviewFeedback from "../../models/ReviewFeedback"; +import User from "../../models/User"; + +export const getReviewStatistics = async (req: Request, res: Response) => { + try { + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 20; + + const skip = (page - 1) * limit; + const totalReviews = await ReviewFeedback.countDocuments(); + const reviews = await ReviewFeedback.find() + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit); + + const totalPages = Math.ceil(totalReviews / limit); + const hasNextPage = page < totalPages; + const hasPrevPage = page > 1; + + const mappedReviews = await Promise.all( + reviews.map(async (review) => { + const reviewerUser = await User.findOne({ pi_uid: review.review_giver_id }); + const sellerUser = await User.findOne({ pi_uid: review.review_receiver_id }); + + return { + id: review._id, + reviewer: reviewerUser?.pi_username ||review.review_giver_id, + seller: sellerUser?.pi_username || review.review_receiver_id , + rating: review.rating, + comment: review.comment, + date: review.review_date.toISOString().split("T")[0], + }; + }) + ); + + + const mostReviewedUser = await ReviewFeedback.aggregate([ + { $group: { _id: "$review_receiver_id", count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 1 }, + ]); + + const user = await User.findOne({ + pi_uid: mostReviewedUser[0]?._id, + }); + + + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const startOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1); + + const currentMonthReviews = await ReviewFeedback.countDocuments({ + review_date: { $gte: startOfMonth, $lt: startOfNextMonth }, + }); + + const currentMonthReviewPercentage = + totalReviews > 0 + ? ((currentMonthReviews / totalReviews) * 100).toFixed(2) + : "0.00"; + + res.status(200).json({ + reviews:mappedReviews, + totalReviews, + mostReviewedUser: { + user: user || null, + count: mostReviewedUser[0]?.count || 0, + }, + currentMonthReviews, + currentMonthReviewPercentage: `${currentMonthReviewPercentage}`, + pagination: { + currentPage: page, + totalPages, + hasNextPage, + hasPrevPage, + totalReviews + }, + }); + } catch (error) { + res.status(500).json({ message: "Failed to fetch review statistics", error }); + } +}; + diff --git a/src/controllers/statistics/seller-statistics.ts b/src/controllers/statistics/seller-statistics.ts new file mode 100644 index 00000000..b3e968db --- /dev/null +++ b/src/controllers/statistics/seller-statistics.ts @@ -0,0 +1,130 @@ + + +import { Request, Response } from "express"; +import Seller from "../../models/Seller"; +// import logger from "../../config/loggingConfig"; + +export const getSellerStatistics = async (req: Request, res: Response) => { + try { + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 15; + + const skip = (page - 1) * limit; + const totalSellers = await Seller.countDocuments(); + const sellers = await Seller.find() + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit); + + const totalPages = Math.ceil(totalSellers / limit); + const hasNextPage = page < totalPages; + const hasPrevPage = page > 1; + + + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); + + const activeSellersCount = await Seller.countDocuments({ seller_type: "activeSeller" }); + const inactiveSellers = await Seller.countDocuments({ seller_type: "inactiveSeller" }); + const testSellers = await Seller.countDocuments({ seller_type: "testSeller" }); + + const sellerGrowthThisMonth = await Seller.aggregate([ + { + $match: { + createdAt: { $gte: startOfMonth, $lte: endOfMonth }, + }, + }, + { + $group: { + _id: "$seller_type", + count: { $sum: 1 }, + }, + }, + ]); + + const sellerGrowthByTypeThisMonth = sellerGrowthThisMonth.reduce((acc, item) => { + acc[item._id] = item.count; + return acc; + }, {}); + + const newSellersThisMonth = sellerGrowthThisMonth.reduce((sum, item) => sum + item.count, 0); + + const percentageGrowthThisMonth = + totalSellers > 0 ? (newSellersThisMonth / totalSellers) * 100 : 0; + + const months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + + const sellerGrowth = await Seller.aggregate([ + { + $group: { + _id: { $month: "$createdAt" }, + count: { $sum: 1 }, + }, + }, + { $sort: { "_id": 1 } }, + ]); + + const data = sellerGrowth.map((growth) => { + return { + month: months[growth._id - 1], + count: growth.count, + }; + }); + + const shopDetails = sellers.map(seller => ({ + name: seller.name, + owner: seller.seller_id, + category: seller.seller_type, + rating: parseFloat(seller.average_rating.toString()), + status: seller.order_online_enabled_pref ? "Active" : "Inactive", + address: seller.address, + coordinates: seller.sell_map_center.coordinates, + })); + + + const statistics = { + totalSellers, + sellers:shopDetails, + activeSellers: activeSellersCount, + inactiveSellers, + testSellers, + newSellersThisMonth, + percentageGrowthThisMonth: percentageGrowthThisMonth.toFixed(2), + sellerGrowthByTypeThisMonth: { + activeSeller: sellerGrowthByTypeThisMonth["activeSeller"] || 0, + inactiveSeller: sellerGrowthByTypeThisMonth["inactiveSeller"] || 0, + testSeller: sellerGrowthByTypeThisMonth["testSeller"] || 0, + }, + sellerGrowth: data, + pagination: { + currentPage: page, + totalPages, + hasNextPage, + hasPrevPage, + }, + }; + + // logger.info("Fetched seller statistics for the current month successfully", statistics); + + return res.status(200).json(statistics); + } catch (error) { + // logger.error("Failed to fetch seller statistics:", error); + return res + .status(500) + .json({ message: "An error occurred while fetching seller statistics; please try again later." }); + } +}; diff --git a/src/controllers/statistics/user-statistics.ts b/src/controllers/statistics/user-statistics.ts new file mode 100644 index 00000000..c02461e3 --- /dev/null +++ b/src/controllers/statistics/user-statistics.ts @@ -0,0 +1,180 @@ +import { Request, Response } from "express"; +import User from "../../models/User"; +import ReviewFeedback from "../../models/ReviewFeedback"; +import { RatingScale } from "../../models/enums/ratingScale"; +import { startOfMonth, endOfMonth, subDays, subMonths } from "date-fns"; + +export const getTotalUser = async (req: Request, res: Response) => { + try { + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 15; + + const skip = (page - 1) * limit; + const totalUsers = await User.find().countDocuments(); + const users = await User.find() + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit); + + const totalPages = Math.ceil(totalUsers / limit); + const hasNextPage = page < totalPages; + const hasPrevPage = page > 1; + + const now = new Date(); + + const sevenDaysAgo = subDays(now, 7); + const usersLast7Days = await User.find({ + createdAt: { $gte: sevenDaysAgo, $lte: now } + }).countDocuments(); + + const fourteenDaysAgo = subDays(now, 14); + const usersPrevious7Days = await User.find({ + createdAt: { $gte: fourteenDaysAgo, $lt: sevenDaysAgo } + }).countDocuments(); + + const sevenDayPercentageChange = usersPrevious7Days > 0 + ? ((usersLast7Days - usersPrevious7Days) / usersPrevious7Days) * 100 + : usersLast7Days > 0 ? 100 : 0; + + const startOfCurrentMonth = startOfMonth(now); + const endOfCurrentMonth = endOfMonth(now); + const usersThisMonth = await User.find({ + createdAt: { $gte: startOfCurrentMonth, $lte: endOfCurrentMonth }, + }).countDocuments(); + + const startOfLastMonth = startOfMonth(subMonths(now, 1)); + const endOfLastMonth = endOfMonth(subMonths(now, 1)); + const usersLastMonth = await User.find({ + createdAt: { $gte: startOfLastMonth, $lte: endOfLastMonth }, + }).countDocuments(); + + const monthOverMonthPercentageChange = usersLastMonth > 0 + ? ((usersThisMonth - usersLastMonth) / usersLastMonth) * 100 + : usersThisMonth > 0 ? 100 : 0; + + const thirtyDaysAgo = subDays(now, 30); + const activeUsers = await User.find({ + updatedAt: { $gte: thirtyDaysAgo } + }).countDocuments(); + + return res.status(200).json({ + totalUsers, + activeUsers, + usersLast7Days, + sevenDayPercentageChange: parseFloat(sevenDayPercentageChange.toFixed(2)), + monthOverMonthPercentageChange: parseFloat(monthOverMonthPercentageChange.toFixed(2)), + users, + pagination: { + currentPage: page, + totalUsers, + totalPages, + hasNextPage, + hasPrevPage, + }, + }); + } catch (error: any) { + return res.status(500).json({ + message: "Failed to fetch user data", + error: error.message, + }); + } +}; + + +export const getUserStatistics = async (req: Request, res: Response) => { + try { + const fetchUsers = async (userIds: string[]) => { + return await User.find({ pi_uid: { $in: userIds } }).select("pi_uid pi_username user_name"); + }; + + const mostReviewsReceived = await ReviewFeedback.aggregate([ + { $group: { _id: "$review_receiver_id", count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 5 }, + ]); + + const mostReviewsReceivedUsers = await fetchUsers(mostReviewsReceived.map(user => user._id)); + + const mostReviewsGiven = await ReviewFeedback.aggregate([ + { $group: { _id: "$review_giver_id", count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 5 }, + ]); + + const mostReviewsGivenUsers = await fetchUsers(mostReviewsGiven.map(user => user._id)); + + const mostTrustworthyReviewsReceived = await ReviewFeedback.aggregate([ + { $match: { rating: RatingScale.DELIGHT } }, + { $group: { _id: "$review_receiver_id", count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 5 }, + ]); + + const mostTrustworthyReviewsReceivedUsers = await fetchUsers( + mostTrustworthyReviewsReceived.map(user => user._id) + ); + + const mostTrustworthyReviewsGiven = await ReviewFeedback.aggregate([ + { $match: { rating: RatingScale.DELIGHT } }, + { $group: { _id: "$review_giver_id", count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 5 }, + ]); + + const mostTrustworthyReviewsGivenUsers = await fetchUsers( + mostTrustworthyReviewsGiven.map(user => user._id) + ); + + const mostDespairReviewsReceived = await ReviewFeedback.aggregate([ + { $match: { rating: RatingScale.DESPAIR } }, + { $group: { _id: "$review_receiver_id", count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 5 }, + ]); + + const mostDespairReviewsReceivedUsers = await fetchUsers( + mostDespairReviewsReceived.map(user => user._id) + ); + + const mostDespairReviewsGiven = await ReviewFeedback.aggregate([ + { $match: { rating: RatingScale.DESPAIR } }, + { $group: { _id: "$review_giver_id", count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 5 }, + ]); + + const mostDespairReviewsGivenUsers = await fetchUsers( + mostDespairReviewsGiven.map(user => user._id) + ); + + res.status(200).json({ + mostReviewsReceived: mostReviewsReceivedUsers.map((user, index) => ({ + ...user.toObject(), + count: mostReviewsReceived[index]?.count || 0, + })), + mostReviewsGiven: mostReviewsGivenUsers.map((user, index) => ({ + ...user.toObject(), + count: mostReviewsGiven[index]?.count || 0, + })), + mostTrustworthyReviewsReceived: mostTrustworthyReviewsReceivedUsers.map((user, index) => ({ + ...user.toObject(), + count: mostTrustworthyReviewsReceived[index]?.count || 0, + })), + mostTrustworthyReviewsGiven: mostTrustworthyReviewsGivenUsers.map((user, index) => ({ + ...user.toObject(), + count: mostTrustworthyReviewsGiven[index]?.count || 0, + })), + mostDespairReviewsReceived: mostDespairReviewsReceivedUsers.map((user, index) => ({ + ...user.toObject(), + count: mostDespairReviewsReceived[index]?.count || 0, + })), + mostDespairReviewsGiven: mostDespairReviewsGivenUsers.map((user, index) => ({ + ...user.toObject(), + count: mostDespairReviewsGiven[index]?.count || 0, + })), + }); + } catch (error) { + res.status(500).json({ message: "Failed to fetch user statistics", error }); + } +}; + diff --git a/src/helpers/jwt.ts b/src/helpers/jwt.ts index 258a1770..1fa7f6d2 100644 --- a/src/helpers/jwt.ts +++ b/src/helpers/jwt.ts @@ -1,10 +1,11 @@ import jwt from "jsonwebtoken"; -import { IUser } from "../types"; +import { IAdmin, IUser } from "../types"; import User from "../models/User"; import { env } from "../utils/env"; import logger from '../config/loggingConfig'; +import Admin from "../models/Admin"; export const generateUserToken = (user: IUser) => { try { @@ -19,6 +20,16 @@ export const generateUserToken = (user: IUser) => { throw new Error('Failed to generate user token; please try again'); } }; +export const generateAdminToken = (admin: IAdmin) => { + try { + const token = jwt.sign({ userId: admin.id }, env.JWT_SECRET, { + expiresIn: "1d", + }); + return token; + } catch (error) { + throw new Error('Failed to generate user token; please try again'); + } +}; export const decodeUserToken = async (token: string) => { try { @@ -41,3 +52,23 @@ export const decodeUserToken = async (token: string) => { throw new Error('Failed to decode user token; please try again'); } }; +export const decodeAdminToken = async (token: string) => { + try { + logger.info(`Decoding token.`); + const decoded = jwt.verify(token, env.JWT_SECRET) as { userId: string }; + if (!decoded.userId) { + logger.warn(`Invalid token: Missing userID.`); + throw new Error("Invalid token: Missing userID."); + } + logger.info(`Finding user associated with token: ${decoded.userId}`); + const currentAdmin = await Admin.findById(decoded.userId); + if (!currentAdmin) { + logger.warn(`User not found for token: ${decoded.userId}`); + throw new Error("User not found."); + } + return currentAdmin; + } catch (error) { + logger.error('Failed to decode user token:', error); + throw new Error('Failed to decode user token; please try again'); + } +}; diff --git a/src/models/Admin.ts b/src/models/Admin.ts new file mode 100644 index 00000000..9c4f5fd9 --- /dev/null +++ b/src/models/Admin.ts @@ -0,0 +1,39 @@ +import { Schema, model } from "mongoose"; +import { IAdmin } from "../types"; + + +const AdminSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + trim: true, + }, + username: { + type: String, + required: false, + unique: true, + }, + password: { + type: String, + required: true, + }, + role: { + type: String, + enum: ["superadmin", "admin"], + default: "admin", + }, + isActive: { + type: Boolean, + default: true, + }, + }, + { + timestamps: true, + } +); + +const Admin = model("Admin", AdminSchema); + +export default Admin; diff --git a/src/models/ReviewFeedback.ts b/src/models/ReviewFeedback.ts index 3c696863..77fa396f 100644 --- a/src/models/ReviewFeedback.ts +++ b/src/models/ReviewFeedback.ts @@ -37,6 +37,9 @@ const reviewFeedbackSchema = new Schema( required: true } }, + { + timestamps:true + } ); const ReviewFeedback = mongoose.model("Review-Feedback", reviewFeedbackSchema); diff --git a/src/models/User.ts b/src/models/User.ts index 08b7ab40..a01191dd 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -17,6 +17,8 @@ const userSchema = new Schema( type: String, required: true, } + }, { + timestamps: true } ); diff --git a/src/models/UserSettings.ts b/src/models/UserSettings.ts index 96428a8e..396df072 100644 --- a/src/models/UserSettings.ts +++ b/src/models/UserSettings.ts @@ -68,6 +68,9 @@ const userSettingsSchema = new Schema( required: true, default: {}, }, + }, + { + timestamps:true } ); diff --git a/src/models/misc/SanctionedGeoBoundary.ts b/src/models/misc/SanctionedGeoBoundary.ts index c34c2309..11ae2b07 100644 --- a/src/models/misc/SanctionedGeoBoundary.ts +++ b/src/models/misc/SanctionedGeoBoundary.ts @@ -29,6 +29,7 @@ const sanctionedGeoBoundarySchema = new Schema( }, { collection: "sanctioned-geo-boundaries", // 👈 This ensures it uses the correct collection + timestamps: true } ); diff --git a/src/routes/admin.routes.ts b/src/routes/admin.routes.ts new file mode 100644 index 00000000..1dcbb5cf --- /dev/null +++ b/src/routes/admin.routes.ts @@ -0,0 +1,15 @@ +import { Router } from "express"; +import { activateAdmin, deactivateAdmin, getAdminInfo, getAlladmins, loginAdmin, registerAdmin } from "../controllers/admin"; + + + + +const adminRoutes = Router(); + +adminRoutes.get("/me", getAdminInfo); +adminRoutes.post("/register", registerAdmin); +adminRoutes.post("/login", loginAdmin); +adminRoutes.put("/deactivate/:id", deactivateAdmin); +adminRoutes.put("/activate/:id", activateAdmin); + +export default adminRoutes; diff --git a/src/routes/statistics.routes.ts b/src/routes/statistics.routes.ts new file mode 100644 index 00000000..b5f4113d --- /dev/null +++ b/src/routes/statistics.routes.ts @@ -0,0 +1,17 @@ +import { Router } from "express"; +import { getTotalUser, getUserStatistics } from "../controllers/statistics/user-statistics"; +import { getSellerStatistics } from "../controllers/statistics/seller-statistics"; +import { getReviewStatistics } from "../controllers/statistics/review-statistics"; +import { getRestrictedAreaStats, createSanctionedRegion } from "../controllers/statistics/restricted-countries-statistics"; +import { verifyAdminToken } from "../middlewares/verifyToken"; + +const statisticRoutes = Router() + +statisticRoutes.get("/users",getTotalUser) +statisticRoutes.get("/top-reviewer-stats",getUserStatistics) +statisticRoutes.get("/sellers-stats",getSellerStatistics) +statisticRoutes.get("/review-stats",getReviewStatistics) +statisticRoutes.get("/banned-countries",getRestrictedAreaStats) +statisticRoutes.post("/banned-countries", verifyAdminToken, createSanctionedRegion) + +export default statisticRoutes \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 2d987a85..de7c6f05 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,7 +17,9 @@ export interface IUser extends Document { pi_uid: string; pi_username: string; user_name: string; -}; + createdAt?: Date; + updatedAt?: Date; +} export interface IUserSettings extends Document { user_settings_id: string; @@ -73,6 +75,8 @@ export interface ISeller extends Document { fulfillment_description?: string; isRestricted: boolean; lastSanctionUpdateAt: Date; + createdAt?: Date; + updatedAt?: Date; }; // Combined interface representing a seller with selected user settings @@ -105,6 +109,8 @@ export interface IReviewFeedback extends Document { comment?: string; image?: string; review_date: Date; + createdAt?: Date; + updatedAt?: Date; }; export interface CompleteFeedback { @@ -305,6 +311,8 @@ export interface ISanctionedGeoBoundary extends Document { shapeGroup: string; shapeType: string; }; + createdAt: Date; + updatedAt: Date; }; export type SanctionedUpdateResult = { @@ -325,3 +333,16 @@ export interface IToggle extends Document { createdAt: Date; updatedAt: Date; }; + +// ======================== +// ADMIN MODELS +// ======================== +export interface IAdmin extends Document { + email: string; + username: string; + password: string; + role?: string; + createdAt?: Date; + updatedAt?: Date; + isActive?: boolean; +} diff --git a/src/utils/app.ts b/src/utils/app.ts index 15674e52..0f96a5fe 100644 --- a/src/utils/app.ts +++ b/src/utils/app.ts @@ -19,6 +19,8 @@ import mapCenterRoutes from "../routes/mapCenter.routes"; import notificationRoutes from "../routes/notification.routes"; import restrictionRoutes from "../routes/restriction.routes"; import toggleRoutes from "../routes/toggle.routes"; +import adminRoutes from "../routes/admin.routes"; +import statisticRoutes from "../routes/statistics.routes"; import cronRoutes from "../routes/cron.routes"; dotenv.config(); @@ -30,7 +32,7 @@ app.use(express.json()); app.use(requestLogger); app.use(cors({ - origin: process.env.CORS_ORIGIN_URL, + origin: [`${process.env.CORS_ORIGIN_URL}`,`${process.env.ADMIN_URL}`], credentials: true })); app.use(cookieParser()); @@ -53,6 +55,10 @@ app.use("/api/v1/notifications", notificationRoutes); app.use("/api/v1/restrictions", restrictionRoutes); app.use("/api/v1/toggles", toggleRoutes); +//// + +app.use("/api/v1/admin",adminRoutes) +app.use("/api/v1/statistics",statisticRoutes) app.use("/api/v1/cron", cronRoutes); app.use("/", homeRoutes);