From 49cb339e5a4fc8d5a92d78ea5750f08640945311 Mon Sep 17 00:00:00 2001 From: rahat0078 Date: Sat, 16 May 2026 23:30:05 +0600 Subject: [PATCH] feat(analytics:admin): andmin analytics dashboard api done --- package.json | 1 + prisma/schema/profile.schema.prisma | 1 - .../modules/analytics/analytics.controller.ts | 34 ++++ src/app/modules/analytics/analytics.route.ts | 25 +++ .../modules/analytics/analytics.service.ts | 190 ++++++++++++++++++ .../modules/analytics/analytics.swagger.ts | 92 +++++++++ .../modules/analytics/analytics.validation.ts | 10 + src/routes.ts | 2 + src/swaggerOptions.ts | 2 + 9 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 src/app/modules/analytics/analytics.controller.ts create mode 100644 src/app/modules/analytics/analytics.route.ts create mode 100644 src/app/modules/analytics/analytics.service.ts create mode 100644 src/app/modules/analytics/analytics.swagger.ts create mode 100644 src/app/modules/analytics/analytics.validation.ts diff --git a/package.json b/package.json index 6326e8f..3ae514b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "cloudinary": "^2.7.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "dayjs": "^1.11.20", "dotenv": "^17.3.1", "express": "^5.1.0", "ioredis": "^5.10.1", diff --git a/prisma/schema/profile.schema.prisma b/prisma/schema/profile.schema.prisma index b5959a9..08859ec 100644 --- a/prisma/schema/profile.schema.prisma +++ b/prisma/schema/profile.schema.prisma @@ -8,7 +8,6 @@ model Profile { shopAddress String? shopMapLocation String? shopCategory String? - } diff --git a/src/app/modules/analytics/analytics.controller.ts b/src/app/modules/analytics/analytics.controller.ts new file mode 100644 index 0000000..d366da6 --- /dev/null +++ b/src/app/modules/analytics/analytics.controller.ts @@ -0,0 +1,34 @@ +import { Request, Response } from "express"; +import catchAsync from "../../utils/catch_async.js"; +import manageResponse from "../../utils/manage_response.js"; +import { analytics_service } from "./analytics.service.js"; + +const getOverview = catchAsync(async (req: Request, res: Response) => { + const range = (req.query.range as "7d" | "30d") || "7d"; + + const result = await analytics_service.getOverview(range); + manageResponse(res, { + success: true, + statusCode: 200, + message: "Analytics overview fetched successfully.", + data: result, + meta: {}, + }); +}); + +const getLastSevenDaysRevenue = catchAsync( + async (req: Request, res: Response) => { + const result = await analytics_service.getLastSevenDaysRevenue(); + manageResponse(res, { + success: true, + statusCode: 200, + message: "Analytics overview fetched successfully.", + data: result, + meta: {}, + }); + }, +); +export const analytics_controller = { + getOverview, + getLastSevenDaysRevenue, +}; diff --git a/src/app/modules/analytics/analytics.route.ts b/src/app/modules/analytics/analytics.route.ts new file mode 100644 index 0000000..ee5f750 --- /dev/null +++ b/src/app/modules/analytics/analytics.route.ts @@ -0,0 +1,25 @@ +import { Router } from "express"; +// import RequestValidator from "../../middlewares/request_validator.js"; +import { analytics_controller } from "./analytics.controller.js"; +import auth from "../../middlewares/auth.js"; +// import { analytics_validations } from "./analytics.validation.js"; + +//! "/admin/analytics" + +const router = Router(); + +router.get("/overview", auth("ADMIN"), analytics_controller.getOverview); +router.get("/revenue-overview", auth("ADMIN"), analytics_controller.getLastSevenDaysRevenue); + + + + +// router.post( +// "/", +// RequestValidator(analytics_validations.create_analytics), +// analytics_controller.create_analytics, +// ); + + +export default router; + \ No newline at end of file diff --git a/src/app/modules/analytics/analytics.service.ts b/src/app/modules/analytics/analytics.service.ts new file mode 100644 index 0000000..d0b6427 --- /dev/null +++ b/src/app/modules/analytics/analytics.service.ts @@ -0,0 +1,190 @@ +import { Request } from "express"; +import { prisma } from "../../lib/prisma.js"; +import dayjs from "dayjs"; + +const getOverview = async (range: "7d" | "30d") => { + const days = range === "30d" ? 30 : 7; + + const now = dayjs(); + + const currentStartDate = now.subtract(days, "day").startOf("day").toDate(); + const currentEndDate = now.endOf("day").toDate(); + + const previousStartDate = now + .subtract(days * 2, "day") + .startOf("day") + .toDate(); + + const previousEndDate = now.subtract(days, "day").endOf("day").toDate(); + + const rangeFilter = (start: Date, end: Date) => ({ + gte: start, + lte: end, + }); + + const [ + currentActiveStores, + + previousActiveStores, + + currentRevenue, + previousRevenue, + + currentSignups, + previousSignups, + + currentPendingActions, + previousPendingActions, + ] = await Promise.all([ + prisma.profile.count({ + where: { + account: { + isAccountVerified: true, + }, + }, + }), + + prisma.profile.count({ + where: { + account: { + isDeleted: false, + isAccountVerified: true, + createdAt: rangeFilter(previousStartDate, previousEndDate), + }, + }, + }), + + prisma.order.aggregate({ + _sum: { + productPrice: true, + }, + where: { + status: "DELIVERED", + createdAt: rangeFilter(currentStartDate, currentEndDate), + }, + }), + + prisma.order.aggregate({ + _sum: { + productPrice: true, + }, + where: { + status: "DELIVERED", + createdAt: rangeFilter(previousStartDate, previousEndDate), + }, + }), + + prisma.account.count({ + where: { + createdAt: rangeFilter(currentStartDate, currentEndDate), + }, + }), + + prisma.account.count({ + where: { + createdAt: rangeFilter(previousStartDate, previousEndDate), + }, + }), + + prisma.order.count({ + where: { + status: { + in: ["INITIATED", "CONFIRMED"], + }, + createdAt: rangeFilter(currentStartDate, currentEndDate), + }, + }), + + prisma.order.count({ + where: { + status: { + in: ["INITIATED", "CONFIRMED"], + }, + createdAt: rangeFilter(previousStartDate, previousEndDate), + }, + }), + ]); + + const percentage = (current: number, previous: number) => { + // avoid division by zero + if (previous === 0) return current > 0 ? 100 : 0; + + return Number((((current - previous) / previous) * 100).toFixed(2)); + }; + + const currentRevenueTotal = currentRevenue._sum.productPrice ?? 0; + const previousRevenueTotal = previousRevenue._sum.productPrice ?? 0; + + return { + activeStores: { + total: currentActiveStores, + changePercentage: percentage(currentActiveStores, previousActiveStores), + }, + + revenue: { + total: currentRevenueTotal, + changePercentage: percentage(currentRevenueTotal, previousRevenueTotal), + }, + + signups: { + total: currentSignups, + changePercentage: percentage(currentSignups, previousSignups), + }, + + pendingActions: { + total: currentPendingActions, + changePercentage: percentage( + currentPendingActions, + previousPendingActions, + ), + }, + }; +}; + +const getLastSevenDaysRevenue = async () => { + const startDate = dayjs().subtract(6, "day").startOf("day").toDate(); + + const endDate = dayjs().endOf("day").toDate(); + + const orders = await prisma.order.findMany({ + where: { + status: "DELIVERED", + + createdAt: { + gte: startDate, + lte: endDate, + }, + }, + + select: { + productPrice: true, + productQuantity: true, + createdAt: true, + }, + }); + + const revenueMap: Record = {}; + + for (let i = 0; i < 7; i++) { + const date = dayjs(startDate).add(i, "day"); + + revenueMap[date.format("dddd")] = 0; + } + + for (const order of orders) { + const day = dayjs(order.createdAt).format("dddd"); + + revenueMap[day] += order.productPrice * order.productQuantity; + } + + const data = Object.entries(revenueMap).map(([day, revenue]) => ({ + day, + revenue, + })); + + return data; +}; + +export const analytics_service = { + getOverview, getLastSevenDaysRevenue +}; diff --git a/src/app/modules/analytics/analytics.swagger.ts b/src/app/modules/analytics/analytics.swagger.ts new file mode 100644 index 0000000..532a8bb --- /dev/null +++ b/src/app/modules/analytics/analytics.swagger.ts @@ -0,0 +1,92 @@ +export const analyticsSwaggerDocs = { + "/api/admin/analytics/overview": { + get: { + tags: ["Analytics"], + summary: "Get analytics overview (7d / 30d comparison)", + description: + "Returns aggregated analytics including active stores, revenue, signups, and percentage change compared to previous period.", + parameters: [ + { + name: "range", + in: "query", + required: true, + description: "Time range for analytics overview", + schema: { + type: "string", + enum: ["7d", "30d"], + example: "7d", + }, + }, + ], + responses: { + 200: { + description: "Analytics overview fetched successfully", + content: { + "application/json": { + schema: { + type: "object", + properties: { + activeStores: { + type: "object", + properties: { + total: { type: "number", example: 120 }, + changePercentage: { + type: "number", + example: 5.23, + }, + }, + }, + revenue: { + type: "object", + properties: { + total: { type: "number", example: 45230 }, + changePercentage: { + type: "number", + example: -3.45, + }, + }, + }, + signups: { + type: "object", + properties: { + total: { type: "number", example: 340 }, + changePercentage: { + type: "number", + example: 12.1, + }, + }, + }, + }, + }, + }, + }, + }, + 401: { + description: "Unauthorized", + }, + 500: { + description: "Internal server error", + }, + }, + }, + }, + + "/api/admin/analytics/revenue-overview": { + get: { + tags: ["Analytics"], + summary: "Get analytics revenue overview (last 7 days)", + description: "", + responses: { + 200: { + description: "Revenue overview fetched successfully", + }, + 401: { + description: "Unauthorized", + }, + 500: { + description: "Internal server error", + }, + }, + }, + }, +}; diff --git a/src/app/modules/analytics/analytics.validation.ts b/src/app/modules/analytics/analytics.validation.ts new file mode 100644 index 0000000..e3b6d9c --- /dev/null +++ b/src/app/modules/analytics/analytics.validation.ts @@ -0,0 +1,10 @@ + +import { z } from "zod"; + +const create_analytics = z.object({}); +const update_analytics = z.object({}); + +export const analytics_validations = { + create_analytics, + update_analytics, +}; diff --git a/src/routes.ts b/src/routes.ts index ab69280..2eb1ef6 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -6,10 +6,12 @@ import profileRoute from "./app/modules/profile/profile.route.js"; import supportRoute from "./app/modules/support/support.route.js"; import templateRoute from "./app/modules/template/template.route.js"; import { staticticsRoute } from "./app/modules/statictics/statictics.route.js"; +import analyticsRoute from "./app/modules/analytics/analytics.route.js"; const appRouter = Router(); const moduleRoutes = [ + { path: "/admin/analytics", route: analyticsRoute }, { path: "/template", route: templateRoute }, { path: "/statictics", route: staticticsRoute }, { path: "/order", route: orderRoute }, diff --git a/src/swaggerOptions.ts b/src/swaggerOptions.ts index d317fed..331cb43 100644 --- a/src/swaggerOptions.ts +++ b/src/swaggerOptions.ts @@ -8,6 +8,7 @@ import { profileSwaggerDocs } from "./app/modules/profile/profile.swagger.js"; import { supportSwaggerDocs } from "./app/modules/support/support.swagger.js"; import { templateSwaggerDocs } from "./app/modules/template/template.swagger.js"; import { staticticsSwaggerDocs } from "./app/modules/statictics/statictics.swagger.js"; +import { analyticsSwaggerDocs } from "./app/modules/analytics/analytics.swagger"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -28,6 +29,7 @@ export const swaggerOptions = { ...supportSwaggerDocs, ...templateSwaggerDocs, ...staticticsSwaggerDocs, + ...analyticsSwaggerDocs, }, servers: configs.env === "production"