feat(analytics:admin): andmin analytics dashboard api done
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user