diff --git a/backend/src/controllers/reportController.ts b/backend/src/controllers/reportController.ts new file mode 100644 index 0000000..faf1f19 --- /dev/null +++ b/backend/src/controllers/reportController.ts @@ -0,0 +1,49 @@ +import { NextFunction, Request, Response } from 'express'; +import ReportService from '../services/reportService'; +import { HttpCode } from "../erros/erro.config"; +import { InvalidDateRange } from '../erros/ReportErros'; + + +export default class ReportController{ + + public static async getMonthlySpongeReport(req: Request, res: Response, next: NextFunction): Promise{ + const { startDate, endDate } = req.body; + + let sDate: Date; + let eDate: Date; + + try { + if (startDate) { + const parsedStart = new Date(startDate); + if (isNaN(parsedStart.getTime())) throw new Error("startDate inválida"); + // Ajusta para primeiro dia do mês + sDate = new Date(parsedStart.getFullYear(), parsedStart.getMonth(), 1); + } else { + sDate = new Date("2025-01-01"); // data mínima possível + } + + if (endDate) { + const parsedEnd = new Date(endDate); + if (isNaN(parsedEnd.getTime())) throw new Error("endDate inválida"); + // Ajusta para primeiro dia do mês seguinte (fim exclusivo) + eDate = new Date(parsedEnd.getFullYear(), parsedEnd.getMonth() + 1, 1); + } else { + const now = new Date(); + eDate = new Date(now.getFullYear(), now.getMonth() + 1, 1); + } + + if(startDate > endDate){ + throw new InvalidDateRange(); + } + + const report = await ReportService.getMonthlySpongeReport( + sDate, + eDate + ); + + res.status(HttpCode.OK).json(report); + }catch(e: any){ + next(e); + } + } +} \ No newline at end of file diff --git a/backend/src/erros/ReportErros.ts b/backend/src/erros/ReportErros.ts new file mode 100644 index 0000000..da2cf09 --- /dev/null +++ b/backend/src/erros/ReportErros.ts @@ -0,0 +1,21 @@ +import { HttpCode, HttpError } from "./erro.config"; + +/** + * @extends HttpError + * @description Erro de relatório não encontrado (nenhum dado retornado) + */ +export class ReportNotFound extends HttpError { + constructor(message: string = 'Nenhum dado encontrado para o período informado') { + super({ status: HttpCode.NOT_FOUND, message }); + } +} + +/** + * @extends HttpError + * @description Erro de validação de data (startDate maior que endDate) + */ +export class InvalidDateRange extends HttpError { + constructor(message: string = 'A data de início não pode ser maior que a data de fim.') { + super({ status: HttpCode.BAD_REQUEST, message }); + } +} \ No newline at end of file diff --git a/backend/src/router.ts b/backend/src/router.ts index d5b7e64..71622ec 100644 --- a/backend/src/router.ts +++ b/backend/src/router.ts @@ -14,6 +14,7 @@ import path from 'path'; import ErrorHandler from './middlewares/errorHandler'; import metricsRouter from './routes/metricsRoutes'; import registrationRouter from './routes/registrationRoutes'; +import reportRouter from './routes/reportRoutes'; /** * Define endpoints mapeados @@ -41,6 +42,7 @@ export default (app: Express): void => { .use(eventRouter) .use(metricsRouter) .use(registrationRouter) + .use(reportRouter) // Rota padrão diff --git a/backend/src/routes/reportRoutes.ts b/backend/src/routes/reportRoutes.ts new file mode 100644 index 0000000..a2b5792 --- /dev/null +++ b/backend/src/routes/reportRoutes.ts @@ -0,0 +1,28 @@ +import { Router } from 'express'; +import ReportController from '../controllers/reportController'; +import AuthMiddleware from '../middlewares/authMiddleware'; + +const reportRouter = Router(); +/** + * @route POST /reports/monthly-sponge + * @description Retorna o total de esponjas coletadas por mês, agrupando os depósitos aprovados + * por ano e mês com base na data de criação (`created_at`). + * + * Os resultados serão ordenados por **ano** e **mês** em ordem **ascendente**, + * ou seja, do mês mais antigo para o mais recente. + * + * **Parâmetros Recebidos** | **Resultado** + * --------------------------------|------------------------------------- + * Nenhum | Retorna todo o histórico de depósitos aprovados. + * `startDate` (formato `YYYY-MM`) | Retorna os dados desde o mês e ano informados até o presente momento. + * `startDate` e `endDate` (formato `YYYY-MM`) | Retorna o intervalo entre os meses e anos informados. + * + * @param {string} [startDate] Data inicial no formato `YYYY-MM` (opcional) + * @param {string} [endDate] Data final no formato `YYYY-MM` (opcional) + * + * @returns {Array<{ year: number, month: number, total_sponges_collected: number }>} + */ +reportRouter.post('/reports/monthly-sponge', AuthMiddleware.ensureAdmin, ReportController.getMonthlySpongeReport); + + +export default reportRouter; \ No newline at end of file diff --git a/backend/src/routes/userRoutes.ts b/backend/src/routes/userRoutes.ts index f1d6b26..1fdb5da 100644 --- a/backend/src/routes/userRoutes.ts +++ b/backend/src/routes/userRoutes.ts @@ -1,4 +1,4 @@ -import { RequestHandler, Router } from 'express'; +import { Router } from 'express'; import UserController from '../controllers/userController'; import AuthMiddleware from '../middlewares/authMiddleware'; diff --git a/backend/src/services/reportService.ts b/backend/src/services/reportService.ts new file mode 100644 index 0000000..c97d13c --- /dev/null +++ b/backend/src/services/reportService.ts @@ -0,0 +1,92 @@ +import DatabaseConnection from '../database/connection/DatabaseConnection'; +import { ReportNotFound } from '../erros/ReportErros'; + +const knex = DatabaseConnection.getInstance(); + + +export default class ReportService { + + /** + * @description Retorna o total de esponjas coletadas por mês, agrupando os depósitos aprovados + * por ano e mês com base na data de criação (`created_at`). + * + * Os resultados serão ordenados por **ano** e **mês** em ordem **ascendente**, + * ou seja, do mês mais antigo para o mais recente. + * + * **Parâmetros Recebidos** | **Resultado** + * --------------------------------|------------------------------------- + * Nenhum | Retorna todo o histórico de depósitos aprovados. + * `startDate` (formato `YYYY-MM`) | Retorna os dados desde o mês e ano informados até o presente momento. + * `startDate` e `endDate` (formato `YYYY-MM`) | Retorna o intervalo entre os meses e anos informados. + * + * @param {string} [startDate] Data inicial no formato `YYYY-MM` (opcional) + * @param {string} [endDate] Data final no formato `YYYY-MM` (opcional) + * + * @returns {Promise>} + */ + public static async getMonthlySpongeReport( + startDate: Date, + endDate: Date + ): Promise> + { + const rows = await knex('Deposit') + .select( + knex.raw('EXTRACT(YEAR FROM created_at) AS year'), + knex.raw('EXTRACT(MONTH FROM created_at) AS month'), + ) + .sum('amountSponges AS total_sponges_collected') + .where('status', 'APROVADO') + .andWhere('created_at', '>=', startDate) + .andWhere('created_at', '<', endDate) + .groupByRaw('year, month') + .orderByRaw('year, month'); + + if (!rows || rows.length === 0) { + throw new ReportNotFound(); + } + + const filledResults = []; + + // Início do primeiro mês + let current = new Date(startDate.getFullYear(), startDate.getMonth(), 1); + + for (const row of rows) { + const rowYear = Number(row.year); + const rowMonth = Number(row.month); + + // Data do mês retornado + const rowDate = new Date(rowYear, rowMonth - 1, 1); + + // Preencher meses faltando até o mês atual da row + while (current < rowDate) { + filledResults.push({ + year: current.getFullYear(), + month: current.getMonth() + 1, + total_sponges_collected: 0, + }); + current.setMonth(current.getMonth() + 1); + } + + // Adiciona o mês retornado do banco + filledResults.push({ + year: rowYear, + month: rowMonth, + total_sponges_collected: Number(row.total_sponges_collected), + }); + + // Move para o próximo mês + current.setMonth(current.getMonth() + 1); + } + + // Preenche até endDate, se necessário + while (current < endDate) { + filledResults.push({ + year: current.getFullYear(), + month: current.getMonth() + 1, + total_sponges_collected: 0, + }); + current.setMonth(current.getMonth() + 1); + } + return filledResults; + } +} \ No newline at end of file