diff --git a/src/controllers/relatorios-controller.js b/src/controllers/relatorios-controller.js index 689d4fc0..897ce1ca 100644 --- a/src/controllers/relatorios-controller.js +++ b/src/controllers/relatorios-controller.js @@ -17,6 +17,7 @@ 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'; @@ -1034,3 +1035,144 @@ 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 + LEFT JOIN paises p ON e.pais_id = p.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 += ` + ) + `; + + 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 + 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), + })); + + 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/controllers/tombos-controller.js b/src/controllers/tombos-controller.js index 4384a00d..05af31fe 100644 --- a/src/controllers/tombos-controller.js +++ b/src/controllers/tombos-controller.js @@ -8,15 +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, 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 new file mode 100644 index 00000000..cd24b312 --- /dev/null +++ b/src/reports/templates/CoordenadaForaPoligono.tsx @@ -0,0 +1,154 @@ +import React from "react"; +import { Page } from "../components/Page"; +import { converteDecimalParaDMS } from "../../helpers/coordenadas"; + +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 RelatorioCoordenadaForaPoligono({ dados, total }: RelatorioCoordenadaForaPoligonoProps) { + + const renderTabelaTombos = (tombos: TomboItem[]) => ( + + + + + + + + + + + + + + + + + + + {tombos.map((tombo, i) => ( + + + + + + + + ))} + +
HCFNome CientíficoLatitudeLongitudeMotivo
{tombo.hcf}{tombo.nome_cientifico || '—'} + {converteDecimalParaDMS(tombo.latitude, true)} + + {converteDecimalParaDMS(tombo.longitude, false)} + {renderMotivo(tombo.motivo)}
+ ); + + 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} + + + {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.sigla))} +
+ ); + }; + + 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 bb209b73..e655fdc2 100644 --- a/src/routes/relatorio.js +++ b/src/routes/relatorio.js @@ -420,4 +420,23 @@ export default app => { controller.obtemDadosDoRelatorioDeQuantidade, ]); + app.route('/relatorio/coordenadas-fora-poligono') + .get([ + tokensMiddleware([ + TIPOS_USUARIOS.CURADOR, + TIPOS_USUARIOS.OPERADOR, + TIPOS_USUARIOS.IDENTIFICADOR, + ]), + listagensMiddleware, + controller.obtemDadosDoRelatorioDeCoordenadaForaPoligono, + ]) + .post([ + tokensMiddleware([ + TIPOS_USUARIOS.CURADOR, + TIPOS_USUARIOS.OPERADOR, + TIPOS_USUARIOS.IDENTIFICADOR, + ]), + listagensMiddleware, + controller.obtemDadosDoRelatorioDeCoordenadaForaPoligono, + ]); };