feat(analytics:admin): andmin analytics dashboard api done

This commit is contained in:
rahat0078
2026-05-16 23:30:05 +06:00
parent 64a0a80a2a
commit 49cb339e5a
9 changed files with 356 additions and 1 deletions
+1
View File
@@ -20,6 +20,7 @@
"cloudinary": "^2.7.0", "cloudinary": "^2.7.0",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"dayjs": "^1.11.20",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"express": "^5.1.0", "express": "^5.1.0",
"ioredis": "^5.10.1", "ioredis": "^5.10.1",
-1
View File
@@ -8,7 +8,6 @@ model Profile {
shopAddress String? shopAddress String?
shopMapLocation String? shopMapLocation String?
shopCategory String? shopCategory String?
} }
@@ -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,
};
@@ -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;
@@ -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<string, number> = {};
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
};
@@ -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",
},
},
},
},
};
@@ -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,
};
+2
View File
@@ -6,10 +6,12 @@ import profileRoute from "./app/modules/profile/profile.route.js";
import supportRoute from "./app/modules/support/support.route.js"; import supportRoute from "./app/modules/support/support.route.js";
import templateRoute from "./app/modules/template/template.route.js"; import templateRoute from "./app/modules/template/template.route.js";
import { staticticsRoute } from "./app/modules/statictics/statictics.route.js"; import { staticticsRoute } from "./app/modules/statictics/statictics.route.js";
import analyticsRoute from "./app/modules/analytics/analytics.route.js";
const appRouter = Router(); const appRouter = Router();
const moduleRoutes = [ const moduleRoutes = [
{ path: "/admin/analytics", route: analyticsRoute },
{ path: "/template", route: templateRoute }, { path: "/template", route: templateRoute },
{ path: "/statictics", route: staticticsRoute }, { path: "/statictics", route: staticticsRoute },
{ path: "/order", route: orderRoute }, { path: "/order", route: orderRoute },
+2
View File
@@ -8,6 +8,7 @@ import { profileSwaggerDocs } from "./app/modules/profile/profile.swagger.js";
import { supportSwaggerDocs } from "./app/modules/support/support.swagger.js"; import { supportSwaggerDocs } from "./app/modules/support/support.swagger.js";
import { templateSwaggerDocs } from "./app/modules/template/template.swagger.js"; import { templateSwaggerDocs } from "./app/modules/template/template.swagger.js";
import { staticticsSwaggerDocs } from "./app/modules/statictics/statictics.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 __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -28,6 +29,7 @@ export const swaggerOptions = {
...supportSwaggerDocs, ...supportSwaggerDocs,
...templateSwaggerDocs, ...templateSwaggerDocs,
...staticticsSwaggerDocs, ...staticticsSwaggerDocs,
...analyticsSwaggerDocs,
}, },
servers: servers:
configs.env === "production" configs.env === "production"