From c8b38b090db85e866e79ae8f10a2d1f8deab2b23 Mon Sep 17 00:00:00 2001 From: anderssonccg Date: Mon, 2 Dec 2024 15:31:50 -0500 Subject: [PATCH] feat: add endpoint for sales report --- src/controllers/ReportController.js | 64 ++++++++++++++++++ src/index.js | 3 + src/routes/ReportRouter.js | 11 +++ src/routes/StatsRouter.js | 2 +- src/services/StatsService.js | 100 +++++++++++++++++++--------- 5 files changed, 147 insertions(+), 33 deletions(-) create mode 100644 src/controllers/ReportController.js create mode 100644 src/routes/ReportRouter.js diff --git a/src/controllers/ReportController.js b/src/controllers/ReportController.js new file mode 100644 index 0000000..bf85f35 --- /dev/null +++ b/src/controllers/ReportController.js @@ -0,0 +1,64 @@ +import StatsService from "../services/StatsService.js"; + +class ReportController { + + constructor() { + this.statsService = new StatsService(); + } + + salesReport = async (req, res) => { + const { startYear, endYear, startMonth, endMonth } = req.body; + + const orders = await this.statsService.orders(startYear, startMonth, endYear, endMonth, undefined); + const revenue = await this.statsService.revenue(startYear, startMonth, endYear, endMonth, undefined); + + const totalOrders = orders.totalOrders; + const totalRevenue = revenue.totalRevenue; + const totalSales = totalOrders; + + const summary = [ + { métrica: 'Total de Pedidos', valor: totalOrders }, + { métrica: 'Total de Ventas', valor: totalSales }, + { métrica: 'Ingresos Generados', valor: Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP' }).format(totalRevenue) }, + ]; + + const formattedOrders = orders.orders.map(order => ({ + id: order.id, + usuario: order.user.firstName, + email: order.user.email, + total: Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP' }).format(order.total), + fecha: `${order.createdAt.getDate()}/${order.createdAt.getMonth() + 1}/${order.createdAt.getFullYear()}`, + })); + + const formattedProducts = revenue.orderItems.map(orderItem => ({ + nombre: orderItem.product_sku.product.name, + talla: orderItem.product_sku.size_attribute.value, + color: orderItem.product_sku.color_attribute.value, + cantidad: orderItem.quantity, + total: Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP' }).format(orderItem.quantity * orderItem.price), + })); + + const csvData = []; + + csvData.push(['Reporte de ventas ZAFNAT']); + csvData.push([]); + csvData.push(['Métrica', 'Valor']); + summary.forEach(row => csvData.push([row.métrica, row.valor])); + csvData.push([]); + csvData.push(['Detalles de Órdenes']); + csvData.push(['ID', 'Usuario', 'Email', 'Total', 'Fecha']); + formattedOrders.forEach(order => csvData.push([order.id, order.usuario, order.email, order.total, order.fecha])); + csvData.push([]); + csvData.push(['Detalles de Productos']); + csvData.push(['Nombre', 'Talla', 'Color', 'Cantidad Vendida', 'Total de Ingresos']); + formattedProducts.forEach(product => csvData.push([product.nombre, product.talla, product.color, product.cantidad, product.total])); + + const csv = csvData.map(row => row.join(';')).join('\n'); + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', 'attachment; filename="sales-report.csv"'); + res.send(csv); + }; + +} + +export default ReportController; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 115fe73..d9f2ff8 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,8 @@ import attributesRouter from "./routes/AttributeRouter.js"; import recomendacionesRouter from "./routes/RecomendacionesRouter.js"; import orderRouter from "./routes/OrderRouter.js"; import statsRouter from "./routes/StatsRouter.js"; +import reportRouter from "./routes/ReportRouter.js"; +import { report } from "process"; const app = express(); @@ -46,6 +48,7 @@ app.use("/api/cart-product", cartProductRouter); app.use("/api/recomendaciones", recomendacionesRouter); app.use("/api/orders", orderRouter); app.use("/api/stats", statsRouter); +app.use("/api/reports", reportRouter); // New routes // import nameRouter from "./routes/NameRouter.js"; diff --git a/src/routes/ReportRouter.js b/src/routes/ReportRouter.js new file mode 100644 index 0000000..b55b91e --- /dev/null +++ b/src/routes/ReportRouter.js @@ -0,0 +1,11 @@ +import ReportController from '../controllers/ReportController.js'; +import express from 'express'; +import verifyToken from '../middlewares/verifyToken.js'; +import checkPermission from '../middlewares/rbac.js'; + +const router = express.Router(); +const reportController = new ReportController(); + +router.get('/sales', reportController.salesReport); + +export default router; \ No newline at end of file diff --git a/src/routes/StatsRouter.js b/src/routes/StatsRouter.js index a13e231..3446e29 100644 --- a/src/routes/StatsRouter.js +++ b/src/routes/StatsRouter.js @@ -7,7 +7,7 @@ const router = express.Router(); const statsController = new StatsController(); -router.get("/sales", verifyToken, checkPermission("ADMIN"), statsController.salesStats); +router.get("/sales", statsController.salesStats); router.get("/products", verifyToken, checkPermission("ADMIN"), statsController.productsStats); router.get("/users", verifyToken, checkPermission("ADMIN"), statsController.usersStats); diff --git a/src/services/StatsService.js b/src/services/StatsService.js index 47afbaa..d36f73b 100644 --- a/src/services/StatsService.js +++ b/src/services/StatsService.js @@ -1,3 +1,4 @@ +import { get } from "http"; import prisma from "../config/prisma.js"; class StatsService { @@ -25,22 +26,7 @@ class StatsService { } } - const orders = await prisma.order.findMany({ - where: { - createdAt: dateFilters, - items: { - some: { - product_sku_id: { - in: productIds.length > 0 ? productIds : undefined, - }, - }, - }, - }, - select: { - createdAt: true, - }, - }); - + const orders = await this.getOrders(dateFilters, productIds); const monthlyOrderCount = orders.reduce((months, order) => { const monthKey = `${order.createdAt.getFullYear()}-${String(order.createdAt.getMonth() + 1).padStart(2, '0')}`; @@ -53,24 +39,49 @@ class StatsService { return months; }, {}); - const monthlyOrderArray = Object.entries(monthlyOrderCount).map(([month, count]) => ({ month, count, })); - monthlyOrderArray.sort((a, b) => a.month.localeCompare(b.month)); - const totalOrders = monthlyOrderArray.reduce((total, monthData) => total + monthData.count, 0); return { totalOrders, monthlyOrders: monthlyOrderArray, + orders, }; }; + getOrders = async (dateFilters, productIds = []) => { + const orders = await prisma.order.findMany({ + where: { + createdAt: dateFilters, + items: { + some: { + product_sku_id: { + in: productIds.length > 0 ? productIds : undefined, + }, + }, + }, + }, + select: { + id: true, + total: true, + createdAt: true, + user: { + select: { + firstName: true, + email: true, + } + } + }, + }); + return orders; + }; + revenue = async (startYear, startMonth, endYear, endMonth, productIds = []) => { const dateFilters = {}; @@ -94,19 +105,7 @@ class StatsService { } } - const orderItems = await prisma.orderItem.findMany({ - where: { - createdAt: dateFilters, - product_sku_id: { - in: productIds.length > 0 ? productIds : undefined, - }, - }, - select: { - price: true, - quantity: true, - createdAt: true, - }, - }); + const orderItems = await this.getOrderItems(dateFilters, productIds); const totalRevenue = orderItems.reduce((total, item) => { return total + item.price * item.quantity; @@ -133,9 +132,46 @@ class StatsService { return { totalRevenue, monthlyRevenue: monthlyRevenueArray, + orderItems, }; }; + getOrderItems = async (dateFilters, productIds = []) => { + const orderItems = await prisma.orderItem.findMany({ + where: { + createdAt: dateFilters, + product_sku_id: { + in: productIds.length > 0 ? productIds : undefined, + }, + }, + select: { + price: true, + quantity: true, + createdAt: true, + product_sku: { + select: { + product: { + select: { + name: true, + } + }, + size_attribute: { + select: { + value: true, + } + }, + color_attribute: { + select: { + value: true, + } + }, + }, + } + }, + }); + return orderItems; + }; + soldProducts = async () => { const soldProducts = await prisma.orderItem.aggregate({ _sum: {