Compare commits
7 Commits
61fd639faf
..
rahat
| Author | SHA1 | Date | |
|---|---|---|---|
| 49cb339e5a | |||
| 64a0a80a2a | |||
| d2b320f3b1 | |||
| 7d89f3f4ea | |||
| b09fdfc255 | |||
| 47d30d96eb | |||
| 86b2292272 |
@@ -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",
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Template" (
|
||||
"id" TEXT NOT NULL,
|
||||
"language" TEXT NOT NULL,
|
||||
"deliveryCharge" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Template_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Banner" (
|
||||
"id" TEXT NOT NULL,
|
||||
"isVisible" BOOLEAN NOT NULL,
|
||||
"bannerTitle" TEXT NOT NULL,
|
||||
"bannerDesc" TEXT NOT NULL,
|
||||
"bannerImage" TEXT NOT NULL,
|
||||
"templateId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Banner_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Address" (
|
||||
"id" TEXT NOT NULL,
|
||||
"isVisible" BOOLEAN NOT NULL,
|
||||
"phoneNumber" TEXT NOT NULL,
|
||||
"shopLocation" TEXT NOT NULL,
|
||||
"shopEmail" TEXT NOT NULL,
|
||||
"templateId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Address_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Ingredient" (
|
||||
"id" TEXT NOT NULL,
|
||||
"isVisible" BOOLEAN NOT NULL,
|
||||
"templateId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Ingredient_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ingredientOptions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ingrImg" TEXT NOT NULL,
|
||||
"ingrTitle" TEXT NOT NULL,
|
||||
"ingrDes" TEXT NOT NULL,
|
||||
"ingredientId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "ingredientOptions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Instruction" (
|
||||
"id" TEXT NOT NULL,
|
||||
"isVisible" BOOLEAN NOT NULL,
|
||||
"instBanner" TEXT NOT NULL,
|
||||
"templateId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Instruction_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "InstructionOptions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"hint" TEXT NOT NULL,
|
||||
"detail" TEXT NOT NULL,
|
||||
"instructionId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "InstructionOptions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "FAQ" (
|
||||
"id" TEXT NOT NULL,
|
||||
"isVisible" BOOLEAN NOT NULL,
|
||||
"templateId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "FAQ_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "FAQOptions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"index" INTEGER NOT NULL,
|
||||
"text" TEXT NOT NULL,
|
||||
"faqId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "FAQOptions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Tips" (
|
||||
"id" TEXT NOT NULL,
|
||||
"isVisible" BOOLEAN NOT NULL,
|
||||
"tipsBanner" TEXT NOT NULL,
|
||||
"templateId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Tips_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TipsOption" (
|
||||
"id" TEXT NOT NULL,
|
||||
"index" INTEGER NOT NULL,
|
||||
"text" TEXT NOT NULL,
|
||||
"tipsId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "TipsOption_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PriceCombo" (
|
||||
"id" TEXT NOT NULL,
|
||||
"isVisible" BOOLEAN NOT NULL,
|
||||
"templateId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "PriceCombo_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PriceOption" (
|
||||
"id" TEXT NOT NULL,
|
||||
"quantity" TEXT NOT NULL,
|
||||
"price" TEXT NOT NULL,
|
||||
"comboId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "PriceOption_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Product" (
|
||||
"id" TEXT NOT NULL,
|
||||
"isVisible" BOOLEAN NOT NULL,
|
||||
"price" TEXT NOT NULL,
|
||||
"discount" INTEGER NOT NULL,
|
||||
"productName" TEXT NOT NULL,
|
||||
"templateId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Banner_templateId_key" ON "Banner"("templateId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Address_templateId_key" ON "Address"("templateId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Ingredient_templateId_key" ON "Ingredient"("templateId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Instruction_templateId_key" ON "Instruction"("templateId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "FAQ_templateId_key" ON "FAQ"("templateId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Tips_templateId_key" ON "Tips"("templateId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PriceCombo_templateId_key" ON "PriceCombo"("templateId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Product_templateId_key" ON "Product"("templateId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Banner" ADD CONSTRAINT "Banner_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Address" ADD CONSTRAINT "Address_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Ingredient" ADD CONSTRAINT "Ingredient_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ingredientOptions" ADD CONSTRAINT "ingredientOptions_ingredientId_fkey" FOREIGN KEY ("ingredientId") REFERENCES "Ingredient"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Instruction" ADD CONSTRAINT "Instruction_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InstructionOptions" ADD CONSTRAINT "InstructionOptions_instructionId_fkey" FOREIGN KEY ("instructionId") REFERENCES "Instruction"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "FAQ" ADD CONSTRAINT "FAQ_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "FAQOptions" ADD CONSTRAINT "FAQOptions_faqId_fkey" FOREIGN KEY ("faqId") REFERENCES "FAQ"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Tips" ADD CONSTRAINT "Tips_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TipsOption" ADD CONSTRAINT "TipsOption_tipsId_fkey" FOREIGN KEY ("tipsId") REFERENCES "Tips"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PriceCombo" ADD CONSTRAINT "PriceCombo_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PriceOption" ADD CONSTRAINT "PriceOption_comboId_fkey" FOREIGN KEY ("comboId") REFERENCES "PriceCombo"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Product" ADD CONSTRAINT "Product_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -8,7 +8,6 @@ model Profile {
|
||||
shopAddress String?
|
||||
shopMapLocation String?
|
||||
shopCategory String?
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
model Template {
|
||||
id String @id @default(uuid())
|
||||
language String
|
||||
deliveryCharge String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
address Address?
|
||||
banner Banner?
|
||||
ingredient Ingredient?
|
||||
instruction Instruction?
|
||||
faq FAQ?
|
||||
tips Tips?
|
||||
priceCombo PriceCombo?
|
||||
product Product?
|
||||
}
|
||||
|
||||
// banner model
|
||||
model Banner {
|
||||
id String @id @default(uuid())
|
||||
isVisible Boolean
|
||||
bannerTitle String
|
||||
bannerDesc String
|
||||
bannerImage String
|
||||
templateId String @unique
|
||||
template Template @relation(fields: [templateId], references: [id])
|
||||
}
|
||||
|
||||
// address model
|
||||
model Address {
|
||||
id String @id @default(uuid())
|
||||
isVisible Boolean
|
||||
phoneNumber String
|
||||
shopLocation String
|
||||
shopEmail String
|
||||
templateId String @unique
|
||||
template Template @relation(fields: [templateId], references: [id])
|
||||
}
|
||||
|
||||
//ingredient model
|
||||
model Ingredient {
|
||||
id String @id @default(uuid())
|
||||
isVisible Boolean
|
||||
templateId String @unique
|
||||
template Template @relation(fields: [templateId], references: [id])
|
||||
options ingredientOptions[]
|
||||
}
|
||||
|
||||
//ingredient options model
|
||||
model ingredientOptions {
|
||||
id String @id @default(uuid())
|
||||
ingrImg String
|
||||
ingrTitle String
|
||||
ingrDes String
|
||||
ingredientId String
|
||||
ingredient Ingredient @relation(fields: [ingredientId], references: [id])
|
||||
}
|
||||
|
||||
//instruction model & instruction options
|
||||
model Instruction {
|
||||
id String @id @default(uuid())
|
||||
isVisible Boolean
|
||||
instBanner String
|
||||
templateId String @unique
|
||||
template Template @relation(fields: [templateId], references: [id])
|
||||
options InstructionOptions[]
|
||||
}
|
||||
|
||||
model InstructionOptions {
|
||||
id String @id @default(uuid())
|
||||
hint String
|
||||
detail String
|
||||
instructionId String
|
||||
instruction Instruction @relation(fields: [instructionId], references: [id])
|
||||
}
|
||||
|
||||
//FAQ & FAQOptions model
|
||||
model FAQ {
|
||||
id String @id @default(uuid())
|
||||
isVisible Boolean
|
||||
templateId String @unique
|
||||
template Template @relation(fields: [templateId], references: [id])
|
||||
options FAQOptions[]
|
||||
}
|
||||
|
||||
model FAQOptions {
|
||||
id String @id @default(uuid())
|
||||
index Int
|
||||
text String
|
||||
|
||||
faqId String
|
||||
faq FAQ @relation(fields: [faqId], references: [id])
|
||||
}
|
||||
|
||||
//Tips & Tips options model
|
||||
model Tips {
|
||||
id String @id @default(uuid())
|
||||
isVisible Boolean
|
||||
tipsBanner String
|
||||
templateId String @unique
|
||||
template Template @relation(fields: [templateId], references: [id])
|
||||
options TipsOption[]
|
||||
}
|
||||
|
||||
model TipsOption {
|
||||
id String @id @default(uuid())
|
||||
index Int
|
||||
text String
|
||||
|
||||
tipsId String
|
||||
tips Tips @relation(fields: [tipsId], references: [id])
|
||||
}
|
||||
|
||||
//PriceCombo & PriceComboOptions model
|
||||
model PriceCombo {
|
||||
id String @id @default(uuid())
|
||||
isVisible Boolean
|
||||
templateId String @unique
|
||||
template Template @relation(fields: [templateId], references: [id])
|
||||
options PriceOption[]
|
||||
}
|
||||
|
||||
model PriceOption {
|
||||
id String @id @default(uuid())
|
||||
quantity String
|
||||
price String
|
||||
comboId String
|
||||
combo PriceCombo @relation(fields: [comboId], references: [id])
|
||||
}
|
||||
|
||||
//product model
|
||||
model Product {
|
||||
id String @id @default(uuid())
|
||||
isVisible Boolean
|
||||
price String
|
||||
discount Int
|
||||
productName String
|
||||
templateId String @unique
|
||||
template Template @relation(fields: [templateId], references: [id])
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -93,6 +93,14 @@ const get_all_order_from_db = async (req: Request) => {
|
||||
where: {
|
||||
AND: andCondition,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
customerName: true,
|
||||
productQuantity: true,
|
||||
productPrice: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: {
|
||||
[sortBy as string]: sortOrder,
|
||||
},
|
||||
|
||||
@@ -5,7 +5,16 @@ import { AppError } from "../../utils/app_error.js";
|
||||
|
||||
const get_all_plan_from_db = async (req: Request) => {
|
||||
// define your own login here
|
||||
const result = await prisma.plan.findMany();
|
||||
const result = await prisma.plan.findMany({
|
||||
select:{
|
||||
planName: true,
|
||||
price: true,
|
||||
planType: true,
|
||||
planDesc: true,
|
||||
planFeatures: true,
|
||||
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import manageResponse from "../../utils/manage_response";
|
||||
import { Request, Response } from "express";
|
||||
import catchAsync from "../../utils/catch_async";
|
||||
import { statictics_service } from "./statictics.service";
|
||||
|
||||
const get_seller_stats = catchAsync(async (req: Request, res: Response) => {
|
||||
const shopAccountId = req.user?.accountId;
|
||||
if (!shopAccountId) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Unauthorized",
|
||||
});
|
||||
}
|
||||
const range = (req.query.range as "7d" | "30d" | "all") || "7d";
|
||||
|
||||
const result = await statictics_service.get_seller_stats_fromDb(
|
||||
shopAccountId,
|
||||
range,
|
||||
);
|
||||
|
||||
manageResponse(res, {
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
message: "All statictics fetched successfully.",
|
||||
data: result,
|
||||
meta: {},
|
||||
});
|
||||
});
|
||||
|
||||
// const get_all_statictics = catchAsync(async (req, res) => {
|
||||
// const result = await statictics_service.get_all_statictics_from_db(req);
|
||||
// manageResponse(res, {
|
||||
// success: true,
|
||||
// statusCode: 200,
|
||||
// message: "All statictics fetched successfully.",
|
||||
// data: result,
|
||||
// meta: {},
|
||||
// });
|
||||
// });
|
||||
|
||||
// const get_single_statictics = catchAsync(async (req, res) => {
|
||||
// const result = await statictics_service.get_single_statictics_from_db(req);
|
||||
// manageResponse(res, {
|
||||
// success: true,
|
||||
// statusCode: 200,
|
||||
// message: "Single statictics fetched successfully.",
|
||||
// data: result,
|
||||
// meta: {},
|
||||
// });
|
||||
// });
|
||||
|
||||
// const create_statictics = catchAsync(async (req, res) => {
|
||||
// const result = await statictics_service.create_statictics_into_db(req);
|
||||
// manageResponse(res, {
|
||||
// success: true,
|
||||
// statusCode: 200,
|
||||
// message: "statictics created successfully.",
|
||||
// data: result,
|
||||
// meta: {},
|
||||
// });
|
||||
// });
|
||||
|
||||
// const update_statictics = catchAsync(async (req, res) => {
|
||||
// const result = await statictics_service.update_statictics_into_db(req);
|
||||
// manageResponse(res, {
|
||||
// success: true,
|
||||
// statusCode: 200,
|
||||
// message: "statictics updated successfully.",
|
||||
// data: result,
|
||||
// meta: {},
|
||||
// });
|
||||
// });
|
||||
|
||||
// const delete_statictics = catchAsync(async (req, res) => {
|
||||
// const result = await statictics_service.delete_statictics_from_db(req);
|
||||
// manageResponse(res, {
|
||||
// success: true,
|
||||
// statusCode: 200,
|
||||
// message: "statictics deleted successfully.",
|
||||
// data: result,
|
||||
// meta: {},
|
||||
// });
|
||||
// });
|
||||
|
||||
export const statictics_controller = {
|
||||
get_seller_stats,
|
||||
// get_all_statictics,
|
||||
// get_single_statictics,
|
||||
// create_statictics,
|
||||
// update_statictics,
|
||||
// delete_statictics,
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Router } from "express";
|
||||
import auth from "../../middlewares/auth.js";
|
||||
import { statictics_controller } from "./statictics.controller.js";
|
||||
// import { statictics_controller } from "./statictics.controller";
|
||||
// import { statictics_validations } from "./statictics.validation";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/seller", auth("USER"), statictics_controller.get_seller_stats);
|
||||
|
||||
// router.post(
|
||||
// "/",
|
||||
// RequestValidator(statictics_validations.create_statictics),
|
||||
// statictics_controller.create_statictics,
|
||||
// );
|
||||
// router.get("/:id", statictics_controller.get_single_statictics);
|
||||
// router.patch(
|
||||
// "/:id",
|
||||
// RequestValidator(statictics_validations.update_statictics),
|
||||
// statictics_controller.update_statictics,
|
||||
// );
|
||||
// router.delete("/:id", statictics_controller.delete_statictics);
|
||||
|
||||
export const staticticsRoute = router;
|
||||
@@ -0,0 +1,151 @@
|
||||
// import { Request } from "express";
|
||||
// import { prisma } from "../../lib/prisma";
|
||||
|
||||
import { prisma } from "../../lib/prisma";
|
||||
|
||||
type Range = "7d" | "30d" | "all";
|
||||
|
||||
const get_seller_stats_fromDb = async (shopAccountId: string, range: Range) => {
|
||||
let createdAtFilter: any = {};
|
||||
|
||||
if (range !== "all") {
|
||||
const days = range === "7d" ? 7 : 30;
|
||||
const from = new Date();
|
||||
from.setDate(from.getDate() - days);
|
||||
|
||||
createdAtFilter = { gte: from };
|
||||
}
|
||||
|
||||
const baseWhere: any = {
|
||||
shopAccountId,
|
||||
...(range !== "all" && { createdAt: createdAtFilter }),
|
||||
};
|
||||
|
||||
const [
|
||||
totalOrders,
|
||||
completedOrders,
|
||||
pendingOrders,
|
||||
rejectedOrders,
|
||||
revenueResult,
|
||||
last7DaysRejected,
|
||||
ordersForChart,
|
||||
] = await Promise.all([
|
||||
prisma.order.count({ where: baseWhere }),
|
||||
|
||||
prisma.order.count({
|
||||
where: { ...baseWhere, status: "DELIVERED" },
|
||||
}),
|
||||
|
||||
prisma.order.count({
|
||||
where: {
|
||||
...baseWhere,
|
||||
status: { in: ["INITIATED", "CONFIRMED", "ONGOING"] },
|
||||
},
|
||||
}),
|
||||
|
||||
prisma.order.count({
|
||||
where: { ...baseWhere, status: "CANCELLED" },
|
||||
}),
|
||||
|
||||
prisma.order.aggregate({
|
||||
where: { ...baseWhere, status: "DELIVERED" },
|
||||
_sum: { productPrice: true },
|
||||
}),
|
||||
|
||||
prisma.order.count({
|
||||
where: {
|
||||
shopAccountId,
|
||||
status: "CANCELLED",
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
range !== "all"
|
||||
? prisma.order.findMany({
|
||||
where: {
|
||||
shopAccountId,
|
||||
status: "DELIVERED",
|
||||
createdAt: createdAtFilter,
|
||||
},
|
||||
select: {
|
||||
productPrice: true,
|
||||
createdAt: true,
|
||||
},
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const totalRevenue = revenueResult._sum.productPrice || 0;
|
||||
|
||||
const avgOrderValue =
|
||||
completedOrders > 0 ? totalRevenue / completedOrders : 0;
|
||||
|
||||
const dailyMap: Record<string, number> = {};
|
||||
|
||||
for (const o of ordersForChart) {
|
||||
const date = o.createdAt.toISOString().split("T")[0];
|
||||
if (!dailyMap[date]) dailyMap[date] = 0;
|
||||
dailyMap[date] += o.productPrice;
|
||||
}
|
||||
|
||||
const dailyRevenue = Object.entries(dailyMap).map(([date, revenue]) => ({
|
||||
date,
|
||||
revenue,
|
||||
}));
|
||||
|
||||
return {
|
||||
totalOrders,
|
||||
completedOrders,
|
||||
pendingOrders,
|
||||
rejectedOrders,
|
||||
totalRevenue,
|
||||
avgOrderValue,
|
||||
last7DaysRejected,
|
||||
dailyRevenue,
|
||||
};
|
||||
};
|
||||
|
||||
// const get_all_statictics_from_db = async (req: Request) => {
|
||||
// // define your own login here
|
||||
// const result = await prisma.statictics.findMany();
|
||||
// return result;
|
||||
// };
|
||||
|
||||
// const get_single_statictics_from_db = async (req: Request) => {
|
||||
// // define your own login here
|
||||
// const { id } = req.params;
|
||||
// const result = await prisma.statictics.findUnique({where:{id}});
|
||||
// return result;
|
||||
// };
|
||||
|
||||
// const create_statictics_into_db = async (req: Request) => {
|
||||
// // define your own login here
|
||||
// const result = await prisma.statictics.create({data:req.body});
|
||||
// return result;
|
||||
// };
|
||||
|
||||
// const update_statictics_into_db = async (req: Request) => {
|
||||
// // define your own login here
|
||||
// const { id } = req.params;
|
||||
// const result = await prisma.statictics.update({where:{id},data:req.body});
|
||||
// return result;
|
||||
// };
|
||||
|
||||
// const delete_statictics_from_db = async (req: Request) => {
|
||||
// // define your own login here
|
||||
// const { id } = req.params;
|
||||
// const result = await prisma.statictics.delete({where:{id}});
|
||||
// return result;
|
||||
// };
|
||||
|
||||
export const statictics_service = {
|
||||
get_seller_stats_fromDb,
|
||||
|
||||
// get_all_statictics_from_db,
|
||||
// get_single_statictics_from_db,
|
||||
// create_statictics_into_db,
|
||||
// update_statictics_into_db,
|
||||
// delete_statictics_from_db,
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
|
||||
export const staticticsSwaggerDocs = {
|
||||
"/api/statictics": {
|
||||
post: {
|
||||
tags: ["statictics"],
|
||||
summary: "Create new statictics",
|
||||
description: "",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({}), // put your request body
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: "statictics created successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
get: {
|
||||
tags: ["statictics"],
|
||||
summary: "Get all statictics",
|
||||
description: "",
|
||||
parameters: [
|
||||
{
|
||||
name: "page",
|
||||
in: "query",
|
||||
required: false,
|
||||
schema: { type: "number" },
|
||||
},
|
||||
{
|
||||
name: "limit",
|
||||
in: "query",
|
||||
required: false,
|
||||
schema: { type: "number" },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: { description: "statictics fetched successfully" },
|
||||
401: { description: "unauthorized" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/statictics/seller": {
|
||||
get: {
|
||||
tags: ["statistics"],
|
||||
summary: "Get seller statistics",
|
||||
description: "Fetch seller dashboard stats (7d, 30d, all)",
|
||||
|
||||
parameters: [
|
||||
{
|
||||
name: "range",
|
||||
in: "query",
|
||||
required: false,
|
||||
schema: {
|
||||
type: "string",
|
||||
enum: ["7d", "30d", "all"],
|
||||
default: "7d",
|
||||
},
|
||||
description: "Time range for statistics",
|
||||
},
|
||||
],
|
||||
|
||||
responses: {
|
||||
200: {
|
||||
description: "Statistics fetched successfully",
|
||||
},
|
||||
401: {
|
||||
description: "Unauthorized",
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
"/api/statictics/{id}": {
|
||||
get: {
|
||||
tags: ["statictics"],
|
||||
summary: "Get single statictics",
|
||||
description: "",
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: { description: "statictics fetched successfully" },
|
||||
401: { description: "unauthorized" },
|
||||
},
|
||||
},
|
||||
patch: {
|
||||
tags: ["statictics"],
|
||||
summary: "Update statictics",
|
||||
description: "",
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({}), // put your request body
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: { description: "statictics updated successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
tags: ["statictics"],
|
||||
summary: "Delete statictics",
|
||||
description: "",
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: { description: "statictics delete successfully" },
|
||||
401: { description: "unauthorized" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
const create_statictics = z.object({});
|
||||
const update_statictics = z.object({});
|
||||
|
||||
export const statictics_validations = {
|
||||
create_statictics,
|
||||
update_statictics,
|
||||
};
|
||||
@@ -1,17 +1,14 @@
|
||||
|
||||
import { Request, Response } from "express";
|
||||
import catchAsync from "../../utils/catch_async.js";
|
||||
import manageResponse from "../../utils/manage_response.js";
|
||||
import { support_service } from "./support.service.js";
|
||||
|
||||
|
||||
|
||||
const createSupport = catchAsync(async (req: Request, res: Response) => {
|
||||
const id = req?.user?.accountId;
|
||||
const data = {
|
||||
...req.body,
|
||||
storeAccountId: id as string
|
||||
}
|
||||
storeAccountId: id as string,
|
||||
};
|
||||
|
||||
const result = await support_service.createSupportIntoDB(data);
|
||||
manageResponse(res, {
|
||||
@@ -23,31 +20,43 @@ const createSupport = catchAsync(async (req: Request, res: Response) => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const getAllSupport = catchAsync(async (req: Request, res: Response) => {
|
||||
const role = req?.user?.role;
|
||||
const id = req?.user?.accountId;
|
||||
const search = req?.query?.search;
|
||||
const type = req?.query?.type;
|
||||
const status = req?.query?.status;
|
||||
const page = Number(req?.query?.page) || 1;
|
||||
const limit = Number(req?.query?.limit) || 10;
|
||||
|
||||
const result = await support_service.getAllSupportFromDB(id as string, role as string, search as string, type as string, status as string);
|
||||
const result = await support_service.getAllSupportFromDB(
|
||||
id as string,
|
||||
role as string,
|
||||
search as string,
|
||||
type as string,
|
||||
status as string,
|
||||
page,
|
||||
limit
|
||||
);
|
||||
manageResponse(res, {
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
message: "All support fetched successfully.",
|
||||
data: result,
|
||||
meta: {},
|
||||
data: result.data,
|
||||
meta: result.meta,
|
||||
});
|
||||
});
|
||||
|
||||
const get_single_support = catchAsync(async (req, res) => {
|
||||
const {id} = req.params;
|
||||
const { id } = req.params;
|
||||
const userId = req?.user?.accountId;
|
||||
const role = req?.user?.role
|
||||
const role = req?.user?.role;
|
||||
|
||||
|
||||
const result = await support_service.getSingleSupportFromDB(id as string, userId as string, role as string);
|
||||
const result = await support_service.getSingleSupportFromDB(
|
||||
id as string,
|
||||
userId as string,
|
||||
role as string,
|
||||
);
|
||||
manageResponse(res, {
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
@@ -57,15 +66,18 @@ const get_single_support = catchAsync(async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const update_support = catchAsync(async (req, res) => {
|
||||
const {id} = req.params;
|
||||
const { id } = req.params;
|
||||
const userId = req?.user?.accountId;
|
||||
const role = req?.user?.role;
|
||||
const data = req.body
|
||||
const data = req.body;
|
||||
|
||||
|
||||
const result = await support_service.updateSupportIntoDB(id as string, userId as string, role as string, data);
|
||||
const result = await support_service.updateSupportIntoDB(
|
||||
id as string,
|
||||
userId as string,
|
||||
role as string,
|
||||
data,
|
||||
);
|
||||
manageResponse(res, {
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
@@ -76,11 +88,15 @@ const update_support = catchAsync(async (req, res) => {
|
||||
});
|
||||
|
||||
const delete_support = catchAsync(async (req, res) => {
|
||||
const {id} = req.params;
|
||||
const { id } = req.params;
|
||||
const userId = req?.user?.accountId;
|
||||
const role = req?.user?.role
|
||||
const role = req?.user?.role;
|
||||
|
||||
const result = await support_service.deleteSupportFromDB(id as string, userId as string, role as string);
|
||||
const result = await support_service.deleteSupportFromDB(
|
||||
id as string,
|
||||
userId as string,
|
||||
role as string,
|
||||
);
|
||||
manageResponse(res, {
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
|
||||
@@ -13,6 +13,8 @@ const getAllSupportFromDB = async (
|
||||
search?: string,
|
||||
type?: string,
|
||||
status?: string,
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
) => {
|
||||
const andCondition: Prisma.SupportWhereInput[] = [];
|
||||
|
||||
@@ -55,14 +57,31 @@ const getAllSupportFromDB = async (
|
||||
const whereCondition: Prisma.SupportWhereInput =
|
||||
andCondition.length > 0 ? { AND: andCondition } : {};
|
||||
|
||||
const result = await prisma.support.findMany({
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.support.findMany({
|
||||
where: whereCondition,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
}),
|
||||
prisma.support.count({
|
||||
where: whereCondition,
|
||||
}),
|
||||
]);
|
||||
|
||||
return result;
|
||||
return {
|
||||
meta: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPage: Math.ceil(total / limit),
|
||||
},
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
const getSingleSupportFromDB = async (
|
||||
@@ -125,8 +144,8 @@ const deleteSupportFromDB = async (
|
||||
}
|
||||
|
||||
const result = await prisma.support.delete({
|
||||
where: {id}
|
||||
})
|
||||
where: { id },
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
|
||||
export const supportSwaggerDocs = {
|
||||
export const supportSwaggerDocs = {
|
||||
"/api/support": {
|
||||
post: {
|
||||
tags: ["support"],
|
||||
summary: "Create new support",
|
||||
description: "",
|
||||
description:
|
||||
"type must be: TECHNICAL | BILLING | DOMAIN | TEMPLATE | PAYMENT | ACCOUNT | FEATURE_REQUEST | BUG | OTHER",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({
|
||||
"issueName": "Your issue name",
|
||||
"description": "Issue description",
|
||||
"type": "Issue Type"
|
||||
issueName: "Your issue name",
|
||||
description: "Issue description",
|
||||
type: "Issue Type",
|
||||
}), // put your request body
|
||||
},
|
||||
},
|
||||
@@ -39,6 +39,45 @@
|
||||
required: false,
|
||||
schema: { type: "number" },
|
||||
},
|
||||
{
|
||||
name: "search",
|
||||
in: "query",
|
||||
required: false,
|
||||
description: "Search by issue name or description",
|
||||
schema: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "type",
|
||||
in: "query",
|
||||
required: false,
|
||||
description: "Filter by support type",
|
||||
schema: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"TECHNICAL",
|
||||
"BILLING",
|
||||
"DOMAIN",
|
||||
"TEMPLATE",
|
||||
"PAYMENT",
|
||||
"ACCOUNT",
|
||||
"FEATURE_REQUEST",
|
||||
"BUG",
|
||||
"OTHER",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "status",
|
||||
in: "query",
|
||||
required: false,
|
||||
description: "Filter by support status",
|
||||
schema: {
|
||||
type: "string",
|
||||
enum: ["OPEN", "IN_PROGRESS", "RESOLVED", "REJECTED"],
|
||||
},
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: { description: "support fetched successfully" },
|
||||
@@ -68,7 +107,11 @@
|
||||
patch: {
|
||||
tags: ["support"],
|
||||
summary: "Update support",
|
||||
description: "",
|
||||
description: `type(enum): TECHNICAL | BILLING | DOMAIN | TEMPLATE | PAYMENT | ACCOUNT | FEATURE_REQUEST | BUG | OTHER \n
|
||||
status(enum): OPEN
|
||||
| IN_PROGRESS
|
||||
| RESOLVED
|
||||
| REJECTED`,
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
@@ -81,7 +124,14 @@
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({}), // put your request body
|
||||
example: JSON.stringify({
|
||||
issueName: "Your issue name",
|
||||
description: "Issue description",
|
||||
type: "Issue Type",
|
||||
status: "issue current status",
|
||||
resolvedBy: "dataTime()",
|
||||
resolvedAt: "dateTime()",
|
||||
}), // put your request body
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -109,6 +159,3 @@
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import catchAsync from "../../utils/catch_async.js";
|
||||
import manageResponse from "../../utils/manage_response.js";
|
||||
import { template_service } from "./template.service.js";
|
||||
|
||||
const get_all_template = catchAsync(async (req, res) => {
|
||||
const result = await template_service.get_all_template_from_db(req);
|
||||
manageResponse(res, {
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
message: "All template fetched successfully.",
|
||||
data: result,
|
||||
meta: {},
|
||||
});
|
||||
});
|
||||
|
||||
const get_single_template = catchAsync(async (req, res) => {
|
||||
const result = await template_service.get_single_template_from_db(req);
|
||||
manageResponse(res, {
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
message: "Single template fetched successfully.",
|
||||
data: result,
|
||||
meta: {},
|
||||
});
|
||||
});
|
||||
|
||||
const create_template = catchAsync(async (req, res) => {
|
||||
const result = await template_service.create_template_into_db(req);
|
||||
manageResponse(res, {
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
message: "template created successfully.",
|
||||
data: result,
|
||||
meta: {},
|
||||
});
|
||||
});
|
||||
|
||||
const update_template = catchAsync(async (req, res) => {
|
||||
const result = await template_service.update_template_into_db(req);
|
||||
manageResponse(res, {
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
message: "template updated successfully.",
|
||||
data: result,
|
||||
meta: {},
|
||||
});
|
||||
});
|
||||
|
||||
const delete_template = catchAsync(async (req, res) => {
|
||||
const result = await template_service.delete_template_from_db(req);
|
||||
manageResponse(res, {
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
message: "template deleted successfully.",
|
||||
data: result,
|
||||
meta: {},
|
||||
});
|
||||
});
|
||||
|
||||
export const template_controller = {
|
||||
get_all_template,
|
||||
get_single_template,
|
||||
create_template,
|
||||
update_template,
|
||||
delete_template,
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Router } from "express";
|
||||
import RequestValidator from "../../middlewares/request_validator.js";
|
||||
import { template_controller } from "./template.controller.js";
|
||||
import { template_validations } from "./template.validation.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", template_controller.get_all_template);
|
||||
router.post(
|
||||
"/",
|
||||
RequestValidator(template_validations.create_template),
|
||||
template_controller.create_template,
|
||||
);
|
||||
router.get("/:id", template_controller.get_single_template);
|
||||
router.patch(
|
||||
"/:id",
|
||||
RequestValidator(template_validations.update_template),
|
||||
template_controller.update_template,
|
||||
);
|
||||
router.delete("/:id", template_controller.delete_template);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Request } from "express";
|
||||
import { prisma } from "../../lib/prisma.js";
|
||||
|
||||
const get_all_template_from_db = async (req: Request) => {
|
||||
// define your own login here
|
||||
const result = await prisma.template.findMany({});
|
||||
return result;
|
||||
};
|
||||
|
||||
const get_single_template_from_db = async (req: Request) => {
|
||||
// define your own login here
|
||||
const { id } = req.params as { id: string };
|
||||
const result = await prisma.template.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
language: true,
|
||||
deliveryCharge: true,
|
||||
banner: true,
|
||||
address: true,
|
||||
ingredient: true,
|
||||
instruction: true,
|
||||
faq: true,
|
||||
tips: true,
|
||||
priceCombo: true,
|
||||
product: true,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const create_template_into_db = async (req: Request) => {
|
||||
// define your own login here
|
||||
const payload = req.body;
|
||||
console.log(payload);
|
||||
const result = await prisma.template.create({
|
||||
data: {
|
||||
language: payload.language,
|
||||
deliveryCharge: payload.deliveryCharge,
|
||||
|
||||
banner: {
|
||||
create: payload.banner,
|
||||
},
|
||||
|
||||
address: {
|
||||
create: payload.address,
|
||||
},
|
||||
|
||||
ingredient: {
|
||||
create: {
|
||||
isVisible: payload.ingredient.isVisible,
|
||||
options: {
|
||||
create: payload.ingredient.options,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
instruction: {
|
||||
create: {
|
||||
isVisible: payload.instruction.isVisible,
|
||||
instBanner: payload.instruction.instBanner,
|
||||
options: {
|
||||
create: payload.instruction.options,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
faq: {
|
||||
create: {
|
||||
isVisible: payload.faq.isVisible,
|
||||
options: {
|
||||
create: payload.faq.options,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
tips: {
|
||||
create: {
|
||||
isVisible: payload.tips.isVisible,
|
||||
tipsBanner: payload.tips.tipsBanner,
|
||||
options: {
|
||||
create: payload.tips.options,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
priceCombo: {
|
||||
create: {
|
||||
isVisible: payload.priceCombo.isVisible,
|
||||
options: {
|
||||
create: payload.priceCombo.options,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
product: {
|
||||
create: {
|
||||
isVisible: payload.product.isVisible,
|
||||
price: payload.product.price,
|
||||
discount: payload.product.discount,
|
||||
productName: payload.product.productName,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
include: {
|
||||
banner: true,
|
||||
address: true,
|
||||
ingredient: { include: { options: true } },
|
||||
instruction: { include: { options: true } },
|
||||
faq: { include: { options: true } },
|
||||
tips: { include: { options: true } },
|
||||
priceCombo: { include: { options: true } },
|
||||
product: true,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const update_template_into_db = async (req: Request) => {
|
||||
// define your own login here
|
||||
const { id } = req.params as { id: string };
|
||||
const result = await prisma.template.update({
|
||||
where: { id },
|
||||
data: req.body,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const delete_template_from_db = async (req: Request) => {
|
||||
// define your own login here
|
||||
const { id } = req.params as { id: string };
|
||||
const result = await prisma.template.delete({ where: { id } });
|
||||
return result;
|
||||
};
|
||||
|
||||
export const template_service = {
|
||||
get_all_template_from_db,
|
||||
get_single_template_from_db,
|
||||
create_template_into_db,
|
||||
update_template_into_db,
|
||||
delete_template_from_db,
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
export const templateSwaggerDocs = {
|
||||
"/api/template": {
|
||||
post: {
|
||||
tags: ["template"],
|
||||
summary: "Create new template",
|
||||
description: "",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({
|
||||
language: "en",
|
||||
deliveryCharge: "60",
|
||||
banner: {
|
||||
isVisible: true,
|
||||
bannerTitle: "Delicious Homemade Food",
|
||||
bannerDesc:
|
||||
"Fresh, healthy and tasty meals delivered to your door.",
|
||||
bannerImage: "https://example.com/banner.jpg",
|
||||
},
|
||||
address: {
|
||||
isVisible: true,
|
||||
phoneNumber: "+8801712345678",
|
||||
shopLocation: "Dhaka, Bangladesh",
|
||||
shopEmail: "foodshop@example.com",
|
||||
},
|
||||
ingredient: {
|
||||
isVisible: true,
|
||||
options: [
|
||||
{
|
||||
ingrImg: "https://example.com/tomato.jpg",
|
||||
ingrTitle: "Tomato",
|
||||
ingrDes: "Fresh organic tomatoes",
|
||||
},
|
||||
{
|
||||
ingrImg: "https://example.com/chicken.jpg",
|
||||
ingrTitle: "Chicken",
|
||||
ingrDes: "Premium quality chicken",
|
||||
},
|
||||
],
|
||||
},
|
||||
instruction: {
|
||||
isVisible: true,
|
||||
instBanner: "https://example.com/instruction-banner.jpg",
|
||||
options: [
|
||||
{
|
||||
hint: "Step 1",
|
||||
detail: "Wash all ingredients properly",
|
||||
},
|
||||
{
|
||||
hint: "Step 2",
|
||||
detail: "Cook on medium heat for 20 minutes",
|
||||
},
|
||||
],
|
||||
},
|
||||
faq: {
|
||||
isVisible: true,
|
||||
options: [
|
||||
{
|
||||
index: 1,
|
||||
text: "Is the food fresh?",
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
text: "Do you offer home delivery?",
|
||||
},
|
||||
],
|
||||
},
|
||||
tips: {
|
||||
isVisible: true,
|
||||
tipsBanner: "https://example.com/tips-banner.jpg",
|
||||
options: [
|
||||
{
|
||||
index: 1,
|
||||
text: "Use fresh ingredients for best taste",
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
text: "Serve hot for better flavor",
|
||||
},
|
||||
],
|
||||
},
|
||||
priceCombo: {
|
||||
isVisible: true,
|
||||
options: [
|
||||
{
|
||||
quantity: "1",
|
||||
price: "120",
|
||||
},
|
||||
{
|
||||
quantity: "2",
|
||||
price: "220",
|
||||
},
|
||||
],
|
||||
},
|
||||
product: {
|
||||
isVisible: true,
|
||||
price: "120",
|
||||
discount: 10,
|
||||
productName: "Chicken Burger",
|
||||
},
|
||||
}), // put your request body
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: "template created successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
get: {
|
||||
tags: ["template"],
|
||||
summary: "Get all template",
|
||||
description: "",
|
||||
parameters: [
|
||||
{
|
||||
name: "page",
|
||||
in: "query",
|
||||
required: false,
|
||||
schema: { type: "number" },
|
||||
},
|
||||
{
|
||||
name: "limit",
|
||||
in: "query",
|
||||
required: false,
|
||||
schema: { type: "number" },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: { description: "template fetched successfully" },
|
||||
401: { description: "unauthorized" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"/api/template/{id}": {
|
||||
get: {
|
||||
tags: ["template"],
|
||||
summary: "Get single template",
|
||||
description: "",
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: { description: "template fetched successfully" },
|
||||
401: { description: "unauthorized" },
|
||||
},
|
||||
},
|
||||
patch: {
|
||||
tags: ["template"],
|
||||
summary: "Update template",
|
||||
description: "",
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({}), // put your request body
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: { description: "template updated successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
tags: ["template"],
|
||||
summary: "Delete template",
|
||||
description: "",
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: { description: "template delete successfully" },
|
||||
401: { description: "unauthorized" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Template Options that we need to use for validation.
|
||||
|
||||
// Ingredient Option
|
||||
const ingredientOptionSchema = z.object({
|
||||
ingrImg: z.string().url().or(z.string()),
|
||||
ingrTitle: z.string().min(1),
|
||||
ingrDes: z.string().min(1),
|
||||
});
|
||||
|
||||
// Instruction Option
|
||||
const instructionOptionSchema = z.object({
|
||||
hint: z.string().min(1),
|
||||
detail: z.string().min(1),
|
||||
});
|
||||
|
||||
// FAQ Option
|
||||
const faqOptionSchema = z.object({
|
||||
index: z.number(),
|
||||
text: z.string().min(1),
|
||||
});
|
||||
|
||||
// Tips Option
|
||||
const tipsOptionSchema = z.object({
|
||||
index: z.number(),
|
||||
text: z.string().min(1),
|
||||
});
|
||||
|
||||
// Price Option
|
||||
const priceOptionSchema = z.object({
|
||||
quantity: z.string().min(1),
|
||||
price: z.string().min(1),
|
||||
});
|
||||
|
||||
|
||||
// Create the main template schema validation
|
||||
const create_template = z.object({
|
||||
language: z.string().min(1),
|
||||
deliveryCharge: z.string().min(1),
|
||||
banner: z.object({
|
||||
isVisible: z.boolean(),
|
||||
bannerTitle: z.string().min(1),
|
||||
bannerDesc: z.string().min(1),
|
||||
bannerImage: z.string(),
|
||||
}),
|
||||
address: z.object({
|
||||
isVisible: z.boolean(),
|
||||
phoneNumber: z.string().min(1),
|
||||
shopLocation: z.string().min(1),
|
||||
shopEmail: z.string().email(),
|
||||
}),
|
||||
ingredient: z.object({
|
||||
isVisible: z.boolean(),
|
||||
options: z.array(ingredientOptionSchema).min(1),
|
||||
}),
|
||||
instruction: z.object({
|
||||
isVisible: z.boolean(),
|
||||
instBanner: z.string(),
|
||||
options: z.array(instructionOptionSchema).min(1),
|
||||
}),
|
||||
faq: z.object({
|
||||
isVisible: z.boolean(),
|
||||
options: z.array(faqOptionSchema).min(1),
|
||||
}),
|
||||
tips: z.object({
|
||||
isVisible: z.boolean(),
|
||||
tipsBanner: z.string(),
|
||||
options: z.array(tipsOptionSchema).min(1),
|
||||
}),
|
||||
priceCombo: z.object({
|
||||
isVisible: z.boolean(),
|
||||
options: z.array(priceOptionSchema).min(1),
|
||||
}),
|
||||
product: z.object({
|
||||
isVisible: z.boolean(),
|
||||
price: z.string().min(1),
|
||||
discount: z.number().min(0),
|
||||
productName: z.string().min(1),
|
||||
}),
|
||||
});
|
||||
const update_template = z.object({});
|
||||
|
||||
export const template_validations = {
|
||||
create_template,
|
||||
update_template,
|
||||
};
|
||||
@@ -4,10 +4,16 @@ import orderRoute from "./app/modules/order/order.route.js";
|
||||
import planRoute from "./app/modules/plan/plan.route.js";
|
||||
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 },
|
||||
{ path: "/support", route: supportRoute },
|
||||
{ path: "/plan", route: planRoute },
|
||||
|
||||
@@ -6,6 +6,9 @@ import { orderSwaggerDocs } from "./app/modules/order/order.swagger.js";
|
||||
import { planSwaggerDocs } from "./app/modules/plan/plan.swagger.js";
|
||||
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);
|
||||
@@ -24,6 +27,9 @@ export const swaggerOptions = {
|
||||
...orderSwaggerDocs,
|
||||
...profileSwaggerDocs,
|
||||
...supportSwaggerDocs,
|
||||
...templateSwaggerDocs,
|
||||
...staticticsSwaggerDocs,
|
||||
...analyticsSwaggerDocs,
|
||||
},
|
||||
servers:
|
||||
configs.env === "production"
|
||||
|
||||
Reference in New Issue
Block a user