From 1ac39116c0276499d6fe636dab173a96b1a7590e Mon Sep 17 00:00:00 2001 From: Moran Date: Thu, 23 Apr 2026 19:46:23 -0300 Subject: [PATCH 1/7] Relatorio tombo com coordenada fora do poligono da cidade --- src/controllers/relatorios-controller.js | 99 ++++++++++++++++++++++++ src/routes/relatorio.js | 11 +++ 2 files changed, 110 insertions(+) diff --git a/src/controllers/relatorios-controller.js b/src/controllers/relatorios-controller.js index ee4cc3b6..63145f89 100644 --- a/src/controllers/relatorios-controller.js +++ b/src/controllers/relatorios-controller.js @@ -885,3 +885,102 @@ export const obtemDadosDoRelatorioDeQuantidade = async (req, res, next) => { next(error); } }; + +/// ////// Relatório de Coordenadas Fora do Polígono ////////// +export const obtemDadosDoRelatorioDeCoordenadaForaPoligono = async (req, res, next) => { + const { incluirSemCoordenadas } = req.query; + const incluirSem = incluirSemCoordenadas === 'true'; + + try { + let query = ` + SELECT + t.hcf, + t.latitude, + t.longitude, + t.nome_cientifico, + c.nome AS cidade_nome, + e.nome AS estado_nome, + e.sigla AS estado_sigla, + CASE + WHEN t.latitude IS NULL OR t.longitude IS NULL THEN 'SEM_COORDENADA' + WHEN c.poligono IS NULL THEN 'SEM_POLIGONO' + ELSE 'FORA_DO_POLIGONO' + END AS motivo + FROM tombos t + LEFT JOIN cidades c ON t.cidade_id = c.id + LEFT JOIN estados e ON c.estado_id = e.id + WHERE t.rascunho = false + AND t.ativo = true + AND ( + ( + t.latitude IS NOT NULL + AND t.longitude IS NOT NULL + AND c.poligono IS NOT NULL + AND ST_Contains( + c.poligono, + ST_SetSRID(ST_Point(t.longitude, t.latitude), 4674) + ) = false + ) + `; + + if (incluirSem) { + query += ` + OR (t.latitude IS NULL OR t.longitude IS NULL) + `; + } + + query += ` + ) + ORDER BY e.nome ASC, c.nome ASC, t.hcf ASC + `; + + const results = await sequelize.query(query, { + type: models.Sequelize.QueryTypes.SELECT, + }); + + // Agrupar por estado e cidade + const agrupado = {}; + results.forEach(row => { + const estadoKey = row.estado_nome || 'Sem estado'; + const cidadeKey = row.cidade_nome || 'Sem cidade'; + + if (!agrupado[estadoKey]) { + agrupado[estadoKey] = { + estado: estadoKey, + sigla: row.estado_sigla || '', + cidades: {}, + }; + } + + if (!agrupado[estadoKey].cidades[cidadeKey]) { + agrupado[estadoKey].cidades[cidadeKey] = { + cidade: cidadeKey, + tombos: [], + }; + } + + agrupado[estadoKey].cidades[cidadeKey].tombos.push({ + hcf: row.hcf, + latitude: row.latitude, + longitude: row.longitude, + nome_cientifico: row.nome_cientifico, + motivo: row.motivo, + }); + }); + + // Converter para array + const resultado = Object.values(agrupado).map(estado => ({ + ...estado, + cidades: Object.values(estado.cidades), + })); + + res.status(codigosHttp.LISTAGEM).json({ + metadados: { + total: results.length, + }, + resultado, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/routes/relatorio.js b/src/routes/relatorio.js index a0782bc6..26b6f837 100644 --- a/src/routes/relatorio.js +++ b/src/routes/relatorio.js @@ -397,4 +397,15 @@ export default app => { listagensMiddleware, controller.obtemDadosDoRelatorioDeQuantidade, ]); + + app.route('/relatorio/coordenadas-fora-poligono') + .get([ + tokensMiddleware([ + TIPOS_USUARIOS.CURADOR, + TIPOS_USUARIOS.OPERADOR, + TIPOS_USUARIOS.IDENTIFICADOR, + ]), + listagensMiddleware, + controller.obtemDadosDoRelatorioDeCoordenadaForaPoligono, + ]); }; From 1dff166fee1d8c30556c2452f98de3c35f7281f2 Mon Sep 17 00:00:00 2001 From: Moran Date: Tue, 5 May 2026 21:39:26 -0300 Subject: [PATCH 2/7] Fixes/ adicao pdf --- src/controllers/relatorios-controller.js | 33 +++++- .../templates/CoordenadaForaPoligono.tsx | 106 ++++++++++++++++++ src/routes/relatorio.js | 9 ++ 3 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 src/reports/templates/CoordenadaForaPoligono.tsx diff --git a/src/controllers/relatorios-controller.js b/src/controllers/relatorios-controller.js index 63145f89..2210fc66 100644 --- a/src/controllers/relatorios-controller.js +++ b/src/controllers/relatorios-controller.js @@ -21,6 +21,7 @@ import ReportFamiliasGeneros from '~/reports/templates/RelacaoFamiliasGenero'; import ReportQtd from '~/reports/templates/RelacaoFamiliasGeneroQtd'; import ReportColetaModelo1 from '~/reports/templates/RelacaoTombos'; import ReportColetaModelo2 from '~/reports/templates/RelacaoTombosComColeta'; +import ReportCoordenadaForaPoligono from '~/reports/templates/CoordenadaForaPoligono'; import codigosHttp from '~/resources/codigos-http'; import models from '../models'; @@ -974,12 +975,32 @@ export const obtemDadosDoRelatorioDeCoordenadaForaPoligono = async (req, res, ne cidades: Object.values(estado.cidades), })); - res.status(codigosHttp.LISTAGEM).json({ - metadados: { - total: results.length, - }, - resultado, - }); + if (req.method === 'GET') { + res.status(codigosHttp.LISTAGEM).json({ + metadados: { + total: results.length, + }, + resultado, + }); + return; + } + + try { + const buffer = await generateReport( + ReportCoordenadaForaPoligono, { + dados: resultado, + total: results.length, + }); + const readable = new Readable(); + + readable._read = () => { }; + readable.push(buffer); + readable.push(null); + res.setHeader('Content-Type', 'application/pdf'); + readable.pipe(res); + } catch (e) { + next(e); + } } catch (error) { next(error); } diff --git a/src/reports/templates/CoordenadaForaPoligono.tsx b/src/reports/templates/CoordenadaForaPoligono.tsx new file mode 100644 index 00000000..d08aab9d --- /dev/null +++ b/src/reports/templates/CoordenadaForaPoligono.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { Page } from "../components/Page"; + +interface TomboItem { + hcf: number; + latitude: number | null; + longitude: number | null; + nome_cientifico: string | null; + motivo: string; +} + +interface CidadeGroup { + cidade: string; + tombos: TomboItem[]; +} + +interface EstadoGroup { + estado: string; + sigla: string; + cidades: CidadeGroup[]; +} + +interface RelatorioCoordenadaForaPoligonoProps { + dados: EstadoGroup[]; + total?: number; +} + +function renderMotivo(motivo: string) { + switch (motivo) { + case 'SEM_COORDENADA': + return 'Sem coordenada'; + case 'SEM_POLIGONO': + return 'Sem polígono'; + case 'FORA_DO_POLIGONO': + return 'Fora do polígono'; + default: + return motivo; + } +} + +function formatCoord(val: number | null | undefined): string { + if (val === null || val === undefined) return '—'; + return val.toFixed(6); +} + +function RelatorioCoordenadaForaPoligono({ dados, total }: RelatorioCoordenadaForaPoligonoProps) { + + const renderTable = (tombos: TomboItem[]) => { + return ( + + + + + + + + + + + + {tombos.map((tombo, i) => ( + + + + + + + + ))} + +
HCFNome CientíficoLatitudeLongitudeMotivo
{tombo.hcf}{tombo.nome_cientifico || '—'}{formatCoord(tombo.latitude)}{formatCoord(tombo.longitude)}{renderMotivo(tombo.motivo)}
+ ); + }; + + const renderCidade = (cidade: CidadeGroup, estadoKey: string) => { + return ( +
+

Município: {cidade.cidade} ({cidade.tombos.length} tombo(s))

+ {renderTable(cidade.tombos)} +
+ ); + }; + + const renderEstado = (estado: EstadoGroup) => { + const totalTombos = estado.cidades.reduce((acc, c) => acc + c.tombos.length, 0); + return ( +
+

+ {estado.estado} ({estado.sigla}) — {totalTombos} tombo(s) +

+ {estado.cidades.map(cidade => renderCidade(cidade, estado.estado))} +
+ ); + }; + + return ( + + {dados.map(renderEstado)} +
+ Total geral: {total || 0} registro(s) +
+
+ ); +} + +export default RelatorioCoordenadaForaPoligono; diff --git a/src/routes/relatorio.js b/src/routes/relatorio.js index 26b6f837..1aed8712 100644 --- a/src/routes/relatorio.js +++ b/src/routes/relatorio.js @@ -407,5 +407,14 @@ export default app => { ]), listagensMiddleware, controller.obtemDadosDoRelatorioDeCoordenadaForaPoligono, + ]) + .post([ + tokensMiddleware([ + TIPOS_USUARIOS.CURADOR, + TIPOS_USUARIOS.OPERADOR, + TIPOS_USUARIOS.IDENTIFICADOR, + ]), + listagensMiddleware, + controller.obtemDadosDoRelatorioDeCoordenadaForaPoligono, ]); }; From 4d00078dbcbae41fc4b77f33c48ee467cfce4627 Mon Sep 17 00:00:00 2001 From: Moran Date: Sat, 9 May 2026 17:58:17 -0300 Subject: [PATCH 3/7] Novo estilo PDF --- .../templates/CoordenadaForaPoligono.tsx | 120 ++++++++++++------ 1 file changed, 82 insertions(+), 38 deletions(-) diff --git a/src/reports/templates/CoordenadaForaPoligono.tsx b/src/reports/templates/CoordenadaForaPoligono.tsx index d08aab9d..c7fef19d 100644 --- a/src/reports/templates/CoordenadaForaPoligono.tsx +++ b/src/reports/templates/CoordenadaForaPoligono.tsx @@ -45,50 +45,88 @@ function formatCoord(val: number | null | undefined): string { function RelatorioCoordenadaForaPoligono({ dados, total }: RelatorioCoordenadaForaPoligonoProps) { - const renderTable = (tombos: TomboItem[]) => { - return ( - - - - - - - - + const renderTabelaTombos = (tombos: TomboItem[]) => ( +
HCFNome CientíficoLatitudeLongitudeMotivo
+ + + + + + + + + + + + + + + + + + {tombos.map((tombo, i) => ( + + + + + + - - - {tombos.map((tombo, i) => ( - - - - - - - - ))} - -
HCFNome CientíficoLatitudeLongitudeMotivo
{tombo.hcf}{tombo.nome_cientifico || '—'}{formatCoord(tombo.latitude)}{formatCoord(tombo.longitude)}{renderMotivo(tombo.motivo)}
{tombo.hcf}{tombo.nome_cientifico || '—'}{formatCoord(tombo.latitude)}{formatCoord(tombo.longitude)}{renderMotivo(tombo.motivo)}
- ); - }; + ))} + + + ); - const renderCidade = (cidade: CidadeGroup, estadoKey: string) => { - return ( -
-

Município: {cidade.cidade} ({cidade.tombos.length} tombo(s))

- {renderTable(cidade.tombos)} + const renderCidade = (cidade: CidadeGroup, estadoSigla: string) => ( +
+
+ + Município: {cidade.cidade} + + + {cidade.tombos.length} tombo(s) +
- ); - }; + {renderTabelaTombos(cidade.tombos)} +
+ ); const renderEstado = (estado: EstadoGroup) => { const totalTombos = estado.cidades.reduce((acc, c) => acc + c.tombos.length, 0); return ( -
-

- {estado.estado} ({estado.sigla}) — {totalTombos} tombo(s) -

- {estado.cidades.map(cidade => renderCidade(cidade, estado.estado))} +
+
+ + {estado.estado} ({estado.sigla}) + + + {totalTombos} tombo(s) + +
+ {estado.cidades.map(cidade => renderCidade(cidade, estado.sigla))}
); }; @@ -96,7 +134,13 @@ function RelatorioCoordenadaForaPoligono({ dados, total }: RelatorioCoordenadaFo return ( {dados.map(renderEstado)} -
+
Total geral: {total || 0} registro(s)
From 607b5937770a4bb7b60d8b2022f069679f368331 Mon Sep 17 00:00:00 2001 From: Moran Date: Mon, 11 May 2026 13:32:55 -0300 Subject: [PATCH 4/7] =?UTF-8?q?Adi=C3=A7=C3=A3o=20filtro=20Pa=C3=ADs,=20Es?= =?UTF-8?q?tado=20e=20Cidade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/relatorios-controller.js | 22 ++++++++++ .../templates/CoordenadaForaPoligono.tsx | 44 ++++++++++++++----- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/controllers/relatorios-controller.js b/src/controllers/relatorios-controller.js index 2210fc66..01302731 100644 --- a/src/controllers/relatorios-controller.js +++ b/src/controllers/relatorios-controller.js @@ -910,6 +910,7 @@ export const obtemDadosDoRelatorioDeCoordenadaForaPoligono = async (req, res, ne FROM tombos t LEFT JOIN cidades c ON t.cidade_id = c.id LEFT JOIN estados e ON c.estado_id = e.id + LEFT JOIN paises p ON e.pais_id = p.id WHERE t.rascunho = false AND t.ativo = true AND ( @@ -932,11 +933,32 @@ export const obtemDadosDoRelatorioDeCoordenadaForaPoligono = async (req, res, ne query += ` ) + `; + + const replacements = {}; + + if (req.query.pais_id) { + query += ` AND p.id = :pais_id`; + replacements.pais_id = req.query.pais_id; + } + + if (req.query.estado_id) { + query += ` AND e.id = :estado_id`; + replacements.estado_id = req.query.estado_id; + } + + if (req.query.cidade_id) { + query += ` AND c.id = :cidade_id`; + replacements.cidade_id = req.query.cidade_id; + } + + query += ` ORDER BY e.nome ASC, c.nome ASC, t.hcf ASC `; const results = await sequelize.query(query, { type: models.Sequelize.QueryTypes.SELECT, + replacements, }); // Agrupar por estado e cidade diff --git a/src/reports/templates/CoordenadaForaPoligono.tsx b/src/reports/templates/CoordenadaForaPoligono.tsx index c7fef19d..b7cec5f3 100644 --- a/src/reports/templates/CoordenadaForaPoligono.tsx +++ b/src/reports/templates/CoordenadaForaPoligono.tsx @@ -38,9 +38,23 @@ function renderMotivo(motivo: string) { } } -function formatCoord(val: number | null | undefined): string { - if (val === null || val === undefined) return '—'; - return val.toFixed(6); +function decimalParaDMS(decimal: number | null | undefined, isLatitude: boolean): string { + if (decimal === null || decimal === undefined) return '—'; + + const abs = Math.abs(decimal); + const graus = Math.floor(abs); + const minutosDecimal = (abs - graus) * 60; + const minutos = Math.floor(minutosDecimal); + const segundos = ((minutosDecimal - minutos) * 60).toFixed(1); + + let hemisferio: string; + if (isLatitude) { + hemisferio = decimal >= 0 ? 'N' : 'S'; + } else { + hemisferio = decimal >= 0 ? 'L' : 'O'; + } + + return `${graus}° ${String(minutos).padStart(2, '0')}' ${String(segundos).padStart(4, '0')}" ${hemisferio}`; } function RelatorioCoordenadaForaPoligono({ dados, total }: RelatorioCoordenadaForaPoligonoProps) { @@ -48,10 +62,10 @@ function RelatorioCoordenadaForaPoligono({ dados, total }: RelatorioCoordenadaFo const renderTabelaTombos = (tombos: TomboItem[]) => ( - - - - + + + + @@ -71,8 +85,12 @@ function RelatorioCoordenadaForaPoligono({ dados, total }: RelatorioCoordenadaFo > - - + + ))} @@ -83,8 +101,10 @@ function RelatorioCoordenadaForaPoligono({ dados, total }: RelatorioCoordenadaFo const renderCidade = (cidade: CidadeGroup, estadoSigla: string) => (
+ {/* pageBreakInside: avoid só no cabeçalho — evita que o nome da cidade + fique sozinho no final de uma página, mas a tabela pode quebrar livremente */}
Município: {cidade.cidade} @@ -132,7 +154,7 @@ function RelatorioCoordenadaForaPoligono({ dados, total }: RelatorioCoordenadaFo }; return ( - + {dados.map(renderEstado)}
Date: Sat, 16 May 2026 19:08:20 -0300 Subject: [PATCH 5/7] fix/funcao duplicada removida --- src/helpers/coordenadas.js | 21 ++++++++++++++++ .../templates/CoordenadaForaPoligono.tsx | 24 +++---------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/helpers/coordenadas.js b/src/helpers/coordenadas.js index 06f8f79e..36e86e6b 100644 --- a/src/helpers/coordenadas.js +++ b/src/helpers/coordenadas.js @@ -23,6 +23,27 @@ export const converteParaDecimal = coordenada => { return sinal * (graus + (minutos / 60) + (segundos / 3600)); }; +export const converteDecimalParaDMS = (decimal, isLatitude = true) => { + if (decimal === null || decimal === undefined || decimal === '') { + return ''; + } + + const abs = Math.abs(decimal); + const graus = Math.floor(abs); + const minutosDecimal = (abs - graus) * 60; + const minutos = Math.floor(minutosDecimal); + const segundos = ((minutosDecimal - minutos) * 60).toFixed(2); + + let hemisferio; + if (isLatitude) { + hemisferio = decimal >= 0 ? 'N' : 'S'; + } else { + hemisferio = decimal >= 0 ? 'E' : 'W'; + } + + return `${graus}° ${minutos}' ${segundos}" ${hemisferio}`; +}; + export const converteDecimalParaGraus = (decimal, isLat) => { let max = 180; diff --git a/src/reports/templates/CoordenadaForaPoligono.tsx b/src/reports/templates/CoordenadaForaPoligono.tsx index b7cec5f3..cd24b312 100644 --- a/src/reports/templates/CoordenadaForaPoligono.tsx +++ b/src/reports/templates/CoordenadaForaPoligono.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Page } from "../components/Page"; +import { converteDecimalParaDMS } from "../../helpers/coordenadas"; interface TomboItem { hcf: number; @@ -38,25 +39,6 @@ function renderMotivo(motivo: string) { } } -function decimalParaDMS(decimal: number | null | undefined, isLatitude: boolean): string { - if (decimal === null || decimal === undefined) return '—'; - - const abs = Math.abs(decimal); - const graus = Math.floor(abs); - const minutosDecimal = (abs - graus) * 60; - const minutos = Math.floor(minutosDecimal); - const segundos = ((minutosDecimal - minutos) * 60).toFixed(1); - - let hemisferio: string; - if (isLatitude) { - hemisferio = decimal >= 0 ? 'N' : 'S'; - } else { - hemisferio = decimal >= 0 ? 'L' : 'O'; - } - - return `${graus}° ${String(minutos).padStart(2, '0')}' ${String(segundos).padStart(4, '0')}" ${hemisferio}`; -} - function RelatorioCoordenadaForaPoligono({ dados, total }: RelatorioCoordenadaForaPoligonoProps) { const renderTabelaTombos = (tombos: TomboItem[]) => ( @@ -86,10 +68,10 @@ function RelatorioCoordenadaForaPoligono({ dados, total }: RelatorioCoordenadaFo
From c006e74b4931ac5d91ff4db57c51d0a7af957d15 Mon Sep 17 00:00:00 2001 From: Moran <105233020+feliperm17@users.noreply.github.com> Date: Sun, 17 May 2026 10:45:11 -0300 Subject: [PATCH 6/7] Ajuste lint --- src/controllers/relatorios-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/relatorios-controller.js b/src/controllers/relatorios-controller.js index de8f5965..49762259 100644 --- a/src/controllers/relatorios-controller.js +++ b/src/controllers/relatorios-controller.js @@ -17,13 +17,13 @@ import { agruparPorGenero, } from '~/helpers/formata-dados-relatorio'; import { generateReport } from '~/reports/reports'; +import ReportCoordenadaForaPoligono from '~/reports/templates/CoordenadaForaPoligono'; import ReportInventario from '~/reports/templates/InventarioEspecies'; import ReportLocalColeta from '~/reports/templates/LocaisColeta'; import ReportFamiliasGeneros from '~/reports/templates/RelacaoFamiliasGenero'; import ReportQtd from '~/reports/templates/RelacaoFamiliasGeneroQtd'; import ReportColetaModelo1 from '~/reports/templates/RelacaoTombos'; import ReportColetaModelo2 from '~/reports/templates/RelacaoTombosComColeta'; -import ReportCoordenadaForaPoligono from '~/reports/templates/CoordenadaForaPoligono'; import ReportTombosPorCidade from '~/reports/templates/TombosPorCidade'; import codigosHttp from '~/resources/codigos-http'; From 5850aae5feadc34c6b8e305efac0cbe83d48bf32 Mon Sep 17 00:00:00 2001 From: feliperm17 Date: Sun, 17 May 2026 11:04:08 -0300 Subject: [PATCH 7/7] Ajustes lint --- src/controllers/relatorios-controller.js | 6 +++--- src/controllers/tombos-controller.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/controllers/relatorios-controller.js b/src/controllers/relatorios-controller.js index 49762259..897ce1ca 100644 --- a/src/controllers/relatorios-controller.js +++ b/src/controllers/relatorios-controller.js @@ -1087,17 +1087,17 @@ export const obtemDadosDoRelatorioDeCoordenadaForaPoligono = async (req, res, ne const replacements = {}; if (req.query.pais_id) { - query += ` AND p.id = :pais_id`; + query += ' AND p.id = :pais_id'; replacements.pais_id = req.query.pais_id; } if (req.query.estado_id) { - query += ` AND e.id = :estado_id`; + query += ' AND e.id = :estado_id'; replacements.estado_id = req.query.estado_id; } if (req.query.cidade_id) { - query += ` AND c.id = :cidade_id`; + query += ' AND c.id = :cidade_id'; replacements.cidade_id = req.query.cidade_id; } diff --git a/src/controllers/tombos-controller.js b/src/controllers/tombos-controller.js index 7c780f42..61809627 100644 --- a/src/controllers/tombos-controller.js +++ b/src/controllers/tombos-controller.js @@ -8,16 +8,16 @@ import { converteParaDecimal, converteDecimalParaGraus, converteDecimalParaGMSGrau, converteDecimalParaGMSMinutos, converteDecimalParaGMSSegundos, } from '../helpers/coordenadas'; import pick from '../helpers/pick'; - -const SRID = { - SIRGAS_2000: 4674, -}; import { converteInteiroParaRomano } from '../helpers/tombo'; import models from '../models'; import codigos from '../resources/codigos-http'; import verifyRecaptcha from '../utils/verify-recaptcha'; import { aprovarPendencia } from './pendencias-controller'; +const SRID = { + SIRGAS_2000: 4674, +}; + const { Solo, Relevo, Cidade, Estado, Vegetacao, FaseSucessional, Pais, Tipo, LocalColeta, Familia, sequelize, Genero, Subfamilia, Autor, Coletor, Variedade, Subespecie, TomboFoto, Identificador,
{tombo.hcf} {tombo.nome_cientifico || '—'}{formatCoord(tombo.latitude)}{formatCoord(tombo.longitude)} + {decimalParaDMS(tombo.latitude, true)} + + {decimalParaDMS(tombo.longitude, false)} + {renderMotivo(tombo.motivo)}
{tombo.hcf} {tombo.nome_cientifico || '—'} - {decimalParaDMS(tombo.latitude, true)} + {converteDecimalParaDMS(tombo.latitude, true)} - {decimalParaDMS(tombo.longitude, false)} + {converteDecimalParaDMS(tombo.longitude, false)} {renderMotivo(tombo.motivo)}