From 8d073bb01ca12b84696c57a446e111a41fbad2da Mon Sep 17 00:00:00 2001 From: Lucas Vaz Date: Sun, 29 Mar 2026 00:02:59 -0300 Subject: [PATCH 1/3] Feat: criando rota de busca de tombos por periodo --- src/controllers/tombos-controller.js | 169 ++++++++++++++++++++++++++- src/routes/tombos.js | 54 ++++++++- 2 files changed, 217 insertions(+), 6 deletions(-) diff --git a/src/controllers/tombos-controller.js b/src/controllers/tombos-controller.js index 706f21e..75fbc8a 100644 --- a/src/controllers/tombos-controller.js +++ b/src/controllers/tombos-controller.js @@ -157,7 +157,7 @@ export const cadastro = (request, response, next) => { } return undefined; }) - // //////////////CRIA COLECOES ANEXAS/////////// + // //////////////CRIA COLECOES ANEXAS/////////// .then(() => { if (colecoesAnexas) { const object = pick(colecoesAnexas, ['tipo', 'observacoes']); @@ -174,7 +174,7 @@ export const cadastro = (request, response, next) => { } return undefined; }) - // ///////// VALIDA A TAXONOMIA E A INSERE NO NOME CIENTIFICO ////////// + // ///////// VALIDA A TAXONOMIA E A INSERE NO NOME CIENTIFICO ////////// .then(() => { if (taxonomia && taxonomia.familia_id) { return Familia.findOne({ @@ -296,7 +296,7 @@ export const cadastro = (request, response, next) => { } return undefined; }) - // /////////// CADASTRA TOMBO ///////////// + // /////////// CADASTRA TOMBO ///////////// .then(() => { let jsonTombo = { numero_coleta: principal.numero_coleta, @@ -373,7 +373,7 @@ export const cadastro = (request, response, next) => { } return Tombo.create(jsonTombo, { transaction }); }) - // //////////// CADASTRA A ALTERACAO /////////// + // //////////// CADASTRA A ALTERACAO /////////// .then(tombo => { if (!tombo) { throw new BadRequestExeption(408); @@ -419,7 +419,7 @@ export const cadastro = (request, response, next) => { return alteracaoTomboCriado; }); }) - // /////////////// CADASTRA O INDETIFICADOR /////////////// + // /////////////// CADASTRA O INDETIFICADOR /////////////// .then(alteracaoTomboCriado => { if (!alteracaoTomboCriado) { throw new BadRequestExeption(409); @@ -1782,4 +1782,163 @@ export const verificarCoordenada = async (request, response, next) => { } }; +export const relatorioPorPeriodo = async (request, response, next) => { + try { + const { data_inicio, data_fim, granularidade } = request.query; + + if (!data_inicio || !data_fim || !granularidade) { + return response.status(400).json({ + error: { + message: 'Parâmetros obrigatórios: data_inicio, data_fim, granularidade', + }, + }); + } + + // Validar granularidade + if (!['dia', 'semana', 'mes', 'ano'].includes(granularidade)) { + return response.status(400).json({ + error: { + message: 'Granularidade inválida. Use: dia, semana, mes ou ano', + }, + }); + } + + const dataInicio = new Date(data_inicio); + const dataFim = new Date(data_fim); + + if (isNaN(dataInicio.getTime()) || isNaN(dataFim.getTime())) { + return response.status(400).json({ + error: { + message: 'Datas inválidas. Use o formato YYYY-MM-DD', + }, + }); + } + + if (dataInicio > dataFim) { + return response.status(400).json({ + error: { + message: 'Data de início deve ser menor ou igual à data de fim', + }, + }); + } + + // Calcular diferenças para validar granularidade + const diffMs = dataFim - dataInicio; + const diffDias = Math.ceil(diffMs / (1000 * 60 * 60 * 24)); + const diffSemanas = Math.ceil(diffDias / 7); + const diffMeses = Math.ceil((dataFim.getFullYear() - dataInicio.getFullYear()) * 12 + (dataFim.getMonth() - dataInicio.getMonth())); + const diffAnos = dataFim.getFullYear() - dataInicio.getFullYear(); + + // Validar granularidade baseada no período + let granularidadePermitida = 'ano'; + if (diffDias <= 48) { + granularidadePermitida = 'dia'; + } else if (diffSemanas <= 48) { + granularidadePermitida = 'semana'; + } else if (diffMeses <= 48) { + granularidadePermitida = 'mes'; + } else { + granularidadePermitida = 'ano'; + } + + // Mapear granularidades para verificação hierárquica + const granularidadeHierarquia = { dia: 0, semana: 1, mes: 2, ano: 3 }; + const granularidadeSolicitada = granularidadeHierarquia[granularidade]; + const granularidadeMaximaPermitida = granularidadeHierarquia[granularidadePermitida]; + + if (granularidadeSolicitada < granularidadeMaximaPermitida) { + return response.status(400).json({ + error: { + message: `Período muito grande para granularidade '${granularidade}'. Máximo: ${diffDias} dias. Use granularidade '${granularidadePermitida}' ou maior.`, + restricoes: { + difDias: diffDias, + difSemanas: diffSemanas, + difMeses: diffMeses, + difAnos: diffAnos, + granularidadePermitida: granularidadePermitida, + }, + }, + }); + } + + let query = ''; + + if (granularidade === 'dia') { + query = ` + SELECT + TO_CHAR(TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD'), 'DD/MM/YYYY') AS periodo, + COUNT(*) AS quantidade + FROM tombos + WHERE rascunho = false + AND data_coleta_ano IS NOT NULL + AND data_coleta_mes IS NOT NULL + AND data_coleta_dia IS NOT NULL + AND TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD') + BETWEEN $1 AND $2 + GROUP BY data_coleta_ano, data_coleta_mes, data_coleta_dia + ORDER BY data_coleta_ano ASC, data_coleta_mes ASC, data_coleta_dia ASC + `; + } else if (granularidade === 'semana') { + query = ` + SELECT + 'Semana ' || TO_CHAR(TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD'), 'WW') || ' de ' || data_coleta_ano::text AS periodo, + COUNT(*) AS quantidade + FROM tombos + WHERE rascunho = false + AND data_coleta_ano IS NOT NULL + AND data_coleta_mes IS NOT NULL + AND data_coleta_dia IS NOT NULL + AND TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD') + BETWEEN $1 AND $2 + GROUP BY data_coleta_ano, TO_CHAR(TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD'), 'WW') + ORDER BY data_coleta_ano ASC, TO_CHAR(TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD'), 'WW')::integer ASC + `; + } else if (granularidade === 'mes') { + query = ` + SELECT + data_coleta_mes::text || '/' || data_coleta_ano::text AS periodo, + COUNT(*) AS quantidade + FROM tombos + WHERE rascunho = false + AND data_coleta_ano IS NOT NULL + AND data_coleta_mes IS NOT NULL + AND data_coleta_ano::integer >= EXTRACT(YEAR FROM $1::date)::integer + AND data_coleta_ano::integer <= EXTRACT(YEAR FROM $2::date)::integer + GROUP BY data_coleta_ano, data_coleta_mes + ORDER BY data_coleta_ano ASC, data_coleta_mes ASC + `; + } else if (granularidade === 'ano') { + query = ` + SELECT + data_coleta_ano::text AS periodo, + COUNT(*) AS quantidade + FROM tombos + WHERE rascunho = false + AND data_coleta_ano IS NOT NULL + AND data_coleta_ano::integer >= EXTRACT(YEAR FROM $1::date)::integer + AND data_coleta_ano::integer <= EXTRACT(YEAR FROM $2::date)::integer + GROUP BY data_coleta_ano + ORDER BY data_coleta_ano ASC + `; + } + + const resultado = await sequelize.query(query, { + bind: [data_inicio, data_fim], + type: models.Sequelize.QueryTypes.SELECT, + }); + + // Filtrar valores nulos + const dados = resultado + .filter(item => item.periodo !== null) + .map(item => ({ + periodo: item.periodo, + quantidade: parseInt(item.quantidade, 10), + })); + + return response.status(200).json(dados); + } catch (err) { + return next(err); + } +}; + export default {}; diff --git a/src/routes/tombos.js b/src/routes/tombos.js index ca3de4c..c8d4fb5 100644 --- a/src/routes/tombos.js +++ b/src/routes/tombos.js @@ -9,6 +9,7 @@ import { alteracao, getNumeroColetor, getUltimoNumeroTombo, getCodigoBarraTombo, editarCodigoBarra, getUltimoNumeroCodigoBarras, postCodigoBarraTombo, verificarCoordenada, getUltimoCodigoBarra, deletarCodigoBarras, listagemTombosPorIdentificador, + relatorioPorPeriodo, } from '../controllers/tombos-controller'; import exportarTombosController from '../controllers/tombos-exportacoes-controller'; import criaJsonMiddleware from '../middlewares/json-middleware'; @@ -20,7 +21,6 @@ import coletorCadastro from '../validators/coletor-cadastro'; import cadastrarTipoEsquema from '../validators/tipo-cadastro'; import cadastrarTomboEsquema from '../validators/tombo-cadastro'; import listagemTombo from '../validators/tombo-listagem'; - /** * @swagger * tags: @@ -73,6 +73,58 @@ export default app => { ]); /** + * @swagger + * /tombos/relatorio-periodo: + * get: + * summary: Gera relatório de tombos por período com granularidade configurável + * tags: [Tombos] + * parameters: + * - in: query + * name: data_inicio + * required: true + * schema: + * type: string + * format: date + * description: Data de início do período (formato YYYY-MM-DD) + * - in: query + * name: data_fim + * required: true + * schema: + * type: string + * format: date + * description: Data de fim do período (formato YYYY-MM-DD) + * - in: query + * name: granularidade + * required: true + * schema: + * type: string + * enum: [dia, semana, mes, ano] + * description: Granularidade do agrupamento dos dados + * responses: + * 200: + * description: Relatório gerado com sucesso + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * periodo: + * type: string + * quantidade: + * type: integer + * '400': + * description: Parâmetros inválidos + * '500': + * $ref: '#/components/responses/InternalServerError' + */ + app.route('/tombos/relatorio-periodo') + .get([ + relatorioPorPeriodo, + ]); + + /** * @swagger * /tombos/numeroColetor/{idColetor}: * get: From 8622ed52b1f6186483f58419c4e7885981fe75a4 Mon Sep 17 00:00:00 2001 From: Lucas Vaz Date: Fri, 3 Apr 2026 17:54:07 -0300 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20atualizando=20granaludade=20do=20gr?= =?UTF-8?q?=C3=A1fico?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/tombos-controller.js | 134 ++++++++++++++++----------- 1 file changed, 82 insertions(+), 52 deletions(-) diff --git a/src/controllers/tombos-controller.js b/src/controllers/tombos-controller.js index 75fbc8a..f90c3a1 100644 --- a/src/controllers/tombos-controller.js +++ b/src/controllers/tombos-controller.js @@ -1828,14 +1828,14 @@ export const relatorioPorPeriodo = async (request, response, next) => { const diffSemanas = Math.ceil(diffDias / 7); const diffMeses = Math.ceil((dataFim.getFullYear() - dataInicio.getFullYear()) * 12 + (dataFim.getMonth() - dataInicio.getMonth())); const diffAnos = dataFim.getFullYear() - dataInicio.getFullYear(); - + // Validar granularidade baseada no período let granularidadePermitida = 'ano'; - if (diffDias <= 48) { + if (diffDias <= 30) { granularidadePermitida = 'dia'; - } else if (diffSemanas <= 48) { + } else if (diffSemanas <= 30) { granularidadePermitida = 'semana'; - } else if (diffMeses <= 48) { + } else if (diffMeses <= 30) { granularidadePermitida = 'mes'; } else { granularidadePermitida = 'ano'; @@ -1865,60 +1865,92 @@ export const relatorioPorPeriodo = async (request, response, next) => { if (granularidade === 'dia') { query = ` + WITH date_series AS ( + SELECT generate_series($1::date, $2::date, '1 day'::interval)::date AS data_p + ), + counts AS ( + SELECT + TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD') AS data_c, + COUNT(*) AS qtd + FROM tombos + WHERE rascunho = false + AND data_coleta_ano IS NOT NULL + AND data_coleta_mes IS NOT NULL + AND data_coleta_dia IS NOT NULL + GROUP BY 1 + ) SELECT - TO_CHAR(TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD'), 'DD/MM/YYYY') AS periodo, - COUNT(*) AS quantidade - FROM tombos - WHERE rascunho = false - AND data_coleta_ano IS NOT NULL - AND data_coleta_mes IS NOT NULL - AND data_coleta_dia IS NOT NULL - AND TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD') - BETWEEN $1 AND $2 - GROUP BY data_coleta_ano, data_coleta_mes, data_coleta_dia - ORDER BY data_coleta_ano ASC, data_coleta_mes ASC, data_coleta_dia ASC + TO_CHAR(ds.data_p, 'DD/MM/YYYY') AS periodo, + COALESCE(c.qtd, 0) AS quantidade + FROM date_series ds + LEFT JOIN counts c ON ds.data_p = c.data_c + ORDER BY ds.data_p ASC `; } else if (granularidade === 'semana') { query = ` + WITH date_series AS ( + SELECT generate_series(date_trunc('week', $1::date), date_trunc('week', $2::date), '1 week'::interval)::date AS data_p + ), + counts AS ( + SELECT + date_trunc('week', TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD'))::date AS data_c, + COUNT(*) AS qtd + FROM tombos + WHERE rascunho = false + AND data_coleta_ano IS NOT NULL + AND data_coleta_mes IS NOT NULL + AND data_coleta_dia IS NOT NULL + GROUP BY 1 + ) SELECT - 'Semana ' || TO_CHAR(TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD'), 'WW') || ' de ' || data_coleta_ano::text AS periodo, - COUNT(*) AS quantidade - FROM tombos - WHERE rascunho = false - AND data_coleta_ano IS NOT NULL - AND data_coleta_mes IS NOT NULL - AND data_coleta_dia IS NOT NULL - AND TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD') - BETWEEN $1 AND $2 - GROUP BY data_coleta_ano, TO_CHAR(TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD'), 'WW') - ORDER BY data_coleta_ano ASC, TO_CHAR(TO_DATE(CONCAT(data_coleta_ano, '-', LPAD(data_coleta_mes::text, 2, '0'), '-', LPAD(data_coleta_dia::text, 2, '0')), 'YYYY-MM-DD'), 'WW')::integer ASC + 'Semana ' || LPAD((ROW_NUMBER() OVER(ORDER BY ds.data_p))::text, 2, '0') AS periodo, + COALESCE(c.qtd, 0) AS quantidade + FROM date_series ds + LEFT JOIN counts c ON ds.data_p = c.data_c + ORDER BY ds.data_p ASC `; } else if (granularidade === 'mes') { query = ` + WITH date_series AS ( + SELECT generate_series(date_trunc('month', $1::date), date_trunc('month', $2::date), '1 month'::interval)::date AS data_p + ), + counts AS ( + SELECT + date_trunc('month', TO_DATE(CONCAT(t.data_coleta_ano, '-', LPAD(t.data_coleta_mes::text, 2, '0'), '-01'), 'YYYY-MM-DD'))::date AS data_c, + COUNT(*) AS qtd + FROM tombos t + WHERE t.rascunho = false + AND t.data_coleta_ano IS NOT NULL + AND t.data_coleta_mes IS NOT NULL + GROUP BY 1 + ) SELECT - data_coleta_mes::text || '/' || data_coleta_ano::text AS periodo, - COUNT(*) AS quantidade - FROM tombos - WHERE rascunho = false - AND data_coleta_ano IS NOT NULL - AND data_coleta_mes IS NOT NULL - AND data_coleta_ano::integer >= EXTRACT(YEAR FROM $1::date)::integer - AND data_coleta_ano::integer <= EXTRACT(YEAR FROM $2::date)::integer - GROUP BY data_coleta_ano, data_coleta_mes - ORDER BY data_coleta_ano ASC, data_coleta_mes ASC + TO_CHAR(ds.data_p, 'MM/YYYY') AS periodo, + COALESCE(c.qtd, 0) AS quantidade + FROM date_series ds + LEFT JOIN counts c ON ds.data_p = c.data_c + ORDER BY ds.data_p ASC `; } else if (granularidade === 'ano') { query = ` + WITH date_series AS ( + SELECT generate_series(date_trunc('year', $1::date), date_trunc('year', $2::date), '1 year'::interval)::date AS data_p + ), + counts AS ( + SELECT + data_coleta_ano AS ano_c, + COUNT(*) AS qtd + FROM tombos t + WHERE t.rascunho = false + AND t.data_coleta_ano IS NOT NULL + GROUP BY 1 + ) SELECT - data_coleta_ano::text AS periodo, - COUNT(*) AS quantidade - FROM tombos - WHERE rascunho = false - AND data_coleta_ano IS NOT NULL - AND data_coleta_ano::integer >= EXTRACT(YEAR FROM $1::date)::integer - AND data_coleta_ano::integer <= EXTRACT(YEAR FROM $2::date)::integer - GROUP BY data_coleta_ano - ORDER BY data_coleta_ano ASC + TO_CHAR(ds.data_p, 'YYYY') AS periodo, + COALESCE(c.qtd, 0) AS quantidade + FROM date_series ds + LEFT JOIN counts c ON EXTRACT(YEAR FROM ds.data_p) = c.ano_c + ORDER BY ds.data_p ASC `; } @@ -1927,13 +1959,11 @@ export const relatorioPorPeriodo = async (request, response, next) => { type: models.Sequelize.QueryTypes.SELECT, }); - // Filtrar valores nulos - const dados = resultado - .filter(item => item.periodo !== null) - .map(item => ({ - periodo: item.periodo, - quantidade: parseInt(item.quantidade, 10), - })); + // Mapear resultados para o formato esperado + const dados = resultado.map(item => ({ + periodo: item.periodo, + quantidade: parseInt(item.quantidade, 10), + })); return response.status(200).json(dados); } catch (err) { From 3fbdad765ea99cc285612168d9bde151b92ba05b Mon Sep 17 00:00:00 2001 From: Lucas Vaz Date: Mon, 6 Apr 2026 20:18:55 -0300 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20lint=20revis=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/tombos-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/tombos-controller.js b/src/controllers/tombos-controller.js index f90c3a1..3be9709 100644 --- a/src/controllers/tombos-controller.js +++ b/src/controllers/tombos-controller.js @@ -1828,7 +1828,7 @@ export const relatorioPorPeriodo = async (request, response, next) => { const diffSemanas = Math.ceil(diffDias / 7); const diffMeses = Math.ceil((dataFim.getFullYear() - dataInicio.getFullYear()) * 12 + (dataFim.getMonth() - dataInicio.getMonth())); const diffAnos = dataFim.getFullYear() - dataInicio.getFullYear(); - + // Validar granularidade baseada no período let granularidadePermitida = 'ano'; if (diffDias <= 30) {