diff --git a/.env.example b/.env.example deleted file mode 100644 index 6c85547..0000000 --- a/.env.example +++ /dev/null @@ -1,22 +0,0 @@ -PORT=8080 -MODE=dev - -# DATABASE -# DATABASE_URL="postgresql://postgres:password@localhost:5432/zafnat?schema=public" -DATABASE_URL= - -# FIREBASE -TYPE= -PROJECT_ID= -PRIVATE_KEY_ID= -PRIVATE_KEY= -CLIENT_EMAIL= -CLIENT_ID= -CLIENT_CERT= - -# EMAIL -EMAIL_SMPT_HOST= -EMAIL_SMPT_PORT= -EMAIL_SMPT_SERVICE= -EMAIL_SMPT_MAIL= -EMAIL_SMPT_APP_PASS=" " \ No newline at end of file diff --git a/src/controllers/InvoicesController.js b/src/controllers/InvoicesController.js index 53e92d7..2885bf9 100644 --- a/src/controllers/InvoicesController.js +++ b/src/controllers/InvoicesController.js @@ -3,7 +3,38 @@ import prisma from "../config/prisma.js"; class InvoicesController { findAll = async (req, res) => { + const invoices = await this.getInvoices(req); + res.json(invoices); + } + + getInvoices = async (req) => { + const { startYear, endYear, startMonth, endMonth } = req.body; + + const dateFilters = {}; + + if (startYear && startMonth && startMonth >= 1 && startMonth <= 12) { + const startDate = new Date(`${startYear}-${String(startMonth).padStart(2, '0')}-01T00:00:00Z`); + if (!isNaN(startDate.getTime())) { + dateFilters.gte = startDate; + } else { + return "Invalid Date"; + } + } + + if (endYear && endMonth && endMonth >= 1 && endMonth <= 12) { + const endDate = new Date(`${(endMonth === 12 ? endYear + 1 : endYear)}-${String((endMonth % 12) + 1).padStart(2, '0')}-01T00:00:00Z`); + endDate.setTime(endDate.getTime() - 1); + if (!isNaN(endDate.getTime())) { + dateFilters.lt = endDate; + } else { + return "Invalid Date"; + } + } + const invoices = await prisma.invoice.findMany({ + where: { + createdAt: dateFilters, + }, include: { order: { include: { @@ -11,8 +42,9 @@ class InvoicesController { } } } - }) - res.json(invoices); + }); + + return invoices; } findById = async (req, res) => { diff --git a/src/controllers/ReportController.js b/src/controllers/ReportController.js new file mode 100644 index 0000000..690e44a --- /dev/null +++ b/src/controllers/ReportController.js @@ -0,0 +1,90 @@ +import StatsService from "../services/StatsService.js"; +import InvoicesController from "./InvoicesController.js"; + +class ReportController { + + constructor() { + this.statsService = new StatsService(); + this.invoicesController = new InvoicesController(); + } + + 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); + }; + + invoicesReport = async (req, res) => { + const invoices = await this.invoicesController.getInvoices(req); + const formattedInvoices = invoices.map(invoice => ({ + id: invoice.id, + transaction_id: invoice.transaction_id, + client: invoice.order.user.firstName, + total: Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP' }).format(invoice.amount), + status: invoice.status, + date: invoice.createdAt, + })); + + const csvData = []; + + csvData.push(['Reporte de facturas ZaFNat']); + csvData.push([]); + csvData.push(['Detalles de Facturas']); + csvData.push(['ID', 'Numero de transaccion', 'Cliente', 'Total', 'Estado de pago', 'Fecha']); + formattedInvoices.forEach(invoice => csvData.push([invoice.id, invoice.transaction_id, invoice.client, invoice.total, invoice.status, invoice.date])); + 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 7de4961..91661d9 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"; import invoiceRouter from "./routes/InvoiceRouter.js"; const app = express(); @@ -47,6 +49,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); app.use("/api/invoices", invoiceRouter); // New routes diff --git a/src/routes/InvoiceRouter.js b/src/routes/InvoiceRouter.js index afde2b6..14d2ff4 100644 --- a/src/routes/InvoiceRouter.js +++ b/src/routes/InvoiceRouter.js @@ -1,11 +1,13 @@ import express from "express"; import InvoicesController from "../controllers/InvoicesController.js"; +import verifyToken from '../middlewares/verifyToken.js'; +import checkPermission from '../middlewares/rbac.js'; const router = express.Router(); const invoicesController = new InvoicesController(); -router.get("/", invoicesController.findAll); -router.get("/:id", invoicesController.findById); +router.get("/", verifyToken, checkPermission("ADMIN"), invoicesController.findAll); +router.get("/:id", verifyToken, checkPermission("ADMIN"), invoicesController.findById); export default router; \ No newline at end of file diff --git a/src/routes/ReportRouter.js b/src/routes/ReportRouter.js new file mode 100644 index 0000000..4101143 --- /dev/null +++ b/src/routes/ReportRouter.js @@ -0,0 +1,12 @@ +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', verifyToken, checkPermission("ADMIN"), reportController.salesReport); +router.get('/invoices', verifyToken, checkPermission("ADMIN"), reportController.invoicesReport); + +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: {