diff --git a/src/controllers/relatorios-controller.js b/src/controllers/relatorios-controller.js index ee4cc3b..d582f09 100644 --- a/src/controllers/relatorios-controller.js +++ b/src/controllers/relatorios-controller.js @@ -9,6 +9,8 @@ import { formatarDadosParaRelatorioDeColetaPorColetorEIntervaloDeData, formataTextFilterColetor, agruparPorLocal, + agruparPorCidade, + formataTextFilterCidade, agruparPorFamiliaGeneroEspecie, agruparPorFamiliaComContadorECodigo, agruparResultadoPorFamilia, @@ -21,6 +23,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 ReportTombosPorCidade from '~/reports/templates/TombosPorCidade'; import codigosHttp from '~/resources/codigos-http'; import models from '../models'; @@ -641,6 +644,126 @@ export const obtemDadosDoRelatorioDeLocalDeColeta = async (req, res, next) => { } }; +/// ////// Relatório de Tombos por Cidade ////////// +export const obtemDadosDoRelatorioDeTombosPorCidade = async (req, res, next) => { + const { paginacao } = req; + const { limite, pagina, offset } = paginacao; + const { cidade, showCoord } = req.query; + + let whereCidade = {}; + if (cidade) { + whereCidade = { + id: cidade, + }; + } + + try { + const tombos = await Tombo.findAndCountAll({ + attributes: [ + 'hcf', + 'numero_coleta', + 'familia_id', + 'especie_id', + 'genero_id', + 'nome_cientifico', + 'data_coleta_ano', + 'data_coleta_mes', + 'data_coleta_dia', + 'latitude', + 'longitude', + ], + include: [ + { + model: Familia, + attributes: ['id', 'nome'], + }, + { + model: Genero, + attributes: ['id', 'nome'], + }, + { + model: Especie, + attributes: ['id', 'nome'], + include: [ + { + model: Autor, + attributes: ['id', 'nome'], + as: 'autor', + }, + ], + }, + { + model: Cidade, + attributes: ['id', 'nome'], + where: Object.keys(whereCidade).length > 0 ? whereCidade : undefined, + required: Object.keys(whereCidade).length > 0, + include: [ + { + model: Estado, + attributes: ['id', 'nome', 'sigla'], + include: [ + { + model: Pais, + }, + ], + }, + ], + }, + ], + order: [ + ['familia_id', 'ASC'], + ['genero_id', 'ASC'], + ['especie_id', 'ASC'], + ], + offset, + }); + + const dadosPuros = tombos.rows.map(registro => registro.get({ plain: true })); + const dadosFormatados = agruparPorCidade(dadosPuros); + + if (req.method === 'GET') { + const cidadeNome = cidade + ? dadosPuros[0]?.cidade?.nome || cidade + : undefined; + res.json({ + metadados: { + total: tombos.count, + pagina, + limite, + }, + resultado: dadosFormatados, + filtro: formataTextFilterCidade(cidadeNome), + }); + return; + } + + try { + const cidadeNome = cidade + ? dadosPuros[0]?.cidade?.nome || cidade + : undefined; + const buffer = await generateReport( + ReportTombosPorCidade, { + dados: dadosFormatados.locais, + total: dadosFormatados?.quantidadeTotal || 0, + textoFiltro: formataTextFilterCidade(cidadeNome), + showCoord: showCoord === 'true', + }); + 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 (e) { + next(e); + } +}; + /// ////// Relatório de Famílias e Gêneros ////////// export const obtemDadosDoRelatorioDeFamiliasEGeneros = async (req, res, next) => { const { paginacao } = req; diff --git a/src/helpers/formata-dados-relatorio.js b/src/helpers/formata-dados-relatorio.js index ed9e7e8..1c71458 100644 --- a/src/helpers/formata-dados-relatorio.js +++ b/src/helpers/formata-dados-relatorio.js @@ -361,3 +361,85 @@ export function agruparPorGenero(dados) { }; }); } + +export function agruparPorCidade(dados) { + const agrupado = {}; + let quantidadeTotal = 0; + + dados.sort((a, b) => { + const familiaA = a?.familia_id ?? a?.familia?.id ?? 0; + const familiaB = b?.familia_id ?? b?.familia?.id ?? 0; + if (familiaA !== familiaB) return familiaA - familiaB; + + const generoA = a?.genero_id ?? a?.genero?.id ?? 0; + const generoB = b?.genero_id ?? b?.genero?.id ?? 0; + if (generoA !== generoB) return generoA - generoB; + + const especieA = a?.especie_id ?? a?.especy?.id ?? 0; + const especieB = b?.especie_id ?? b?.especy?.id ?? 0; + if (especieA !== especieB) return especieA - especieB; + + const familiaNomeA = a?.familia?.nome || ''; + const familiaNomeB = b?.familia?.nome || ''; + const familiaNome = familiaNomeA.localeCompare(familiaNomeB); + if (familiaNome !== 0) return familiaNome; + + const generoNomeA = a?.genero?.nome || ''; + const generoNomeB = b?.genero?.nome || ''; + const generoNome = generoNomeA.localeCompare(generoNomeB); + if (generoNome !== 0) return generoNome; + + const especieNomeA = a?.especy?.nome || ''; + const especieNomeB = b?.especy?.nome || ''; + return especieNomeA.localeCompare(especieNomeB); + }).forEach(entradaOriginal => { + const cidade = entradaOriginal.cidade; + const estado = cidade?.estado?.nome || 'Desconhecido'; + const estadoSigla = cidade?.estado?.sigla || '-'; + const municipio = cidade?.nome || 'Desconhecido'; + + const chave = `${estado} > ${municipio}`; + + const entrada = { + ...entradaOriginal, + latitude: entradaOriginal?.latitude || null, + longitude: entradaOriginal?.longitude || null, + autor: entradaOriginal.especy?.autor?.nome || '', + }; + + if (!agrupado[chave]) { + agrupado[chave] = { + estado, + estadoSigla, + municipio, + latitude: entradaOriginal?.latitude || null, + longitude: entradaOriginal?.longitude || null, + quantidadeRegistros: 0, + registros: [], + }; + } + + agrupado[chave].registros.push(entrada); + agrupado[chave].quantidadeRegistros += 1; + quantidadeTotal += 1; + }); + + const locais = Object.values(agrupado); + const locaisComResumo = adicionarResumoTaxonomicoPorLocal(locais); + + return { + locais: locaisComResumo, + quantidadeTotal, + }; +} + +export const formataTextFilterCidade = (cidade, inicio, fim) => { + let filtro = 'Coletados'; + if (inicio && fim) { + filtro += ` no período ${format(new Date(inicio), 'dd/MM/yyyy')} à ${format(new Date(fim), 'dd/MM/yyyy')}`; + } + if (cidade) { + filtro += ` na cidade ${cidade}`; + } + return filtro; +}; diff --git a/src/reports/templates/TombosPorCidade.tsx b/src/reports/templates/TombosPorCidade.tsx new file mode 100644 index 0000000..09a9939 --- /dev/null +++ b/src/reports/templates/TombosPorCidade.tsx @@ -0,0 +1,161 @@ +import React from "react"; +import { Page } from "../components/Page"; + +interface Registro { + hcf: number; + data_coleta_ano: number; + data_coleta_mes: number; + data_coleta_dia: number; + especy: { + nome: string; + genero: { + nome: string; + } + familia: { + nome: string; + } + } + familia: { + nome: string; + } + genero: { + nome: string; + } + latitude: number | null; + longitude: number | null; + autor?: string; +} + +interface CidadeGroup { + estado: string; + estadoSigla: string; + municipio: string; + registros: Registro[]; + quantidadeRegistros: number; + quantidadeEspecies?: number; + quantidadeGeneros?: number; + quantidadeFamilias?: number; +} + +interface RelacaoTombosPorCidadeProps { + dados: CidadeGroup[]; + total?: number; + textoFiltro?: string; + showCoord?: boolean; +} + +function RelacaoTombosPorCidade({ dados, total, textoFiltro, showCoord = false }: RelacaoTombosPorCidadeProps) { + const renderTotalizador = (geral: boolean, qtd?: number, qtdEspecies?: number, qtdGeneros?: number, qtdFamilias?: number) => { + return ( +
+ Total {geral ? 'geral' : 'da cidade'}: {geral ? total : qtd} {!geral ? `(Famílias: ${qtdFamilias || 0}, Gêneros: ${qtdGeneros || 0}, Espécies: ${qtdEspecies || 0})`: ''} +
+ ) + } + + const criaData = (registro: Registro) => { + const { data_coleta_ano, data_coleta_mes, data_coleta_dia } = registro; + const romanoMeses = [ + 'I', 'II', 'III', 'IV', 'V', 'VI', + 'VII', 'VIII', 'IX', 'X', 'XI', 'XII' + ]; + let data = ''; + if (data_coleta_dia !== null && data_coleta_dia !== undefined) { + data = String(data_coleta_dia).padStart(2, '0'); + } + if (data_coleta_mes !== null && data_coleta_mes !== undefined) { + if (data) data += '/'; + data += romanoMeses[data_coleta_mes - 1]; + } + if (data_coleta_ano !== null && data_coleta_ano !== undefined) { + if (data) data += '/'; + data += data_coleta_ano; + } + return data; + } + + const converteDecimalParaDMS = (decimal: number | null, isLatitude = true) => { + 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(2); + + let hemisferio; + if (isLatitude) { + hemisferio = decimal >= 0 ? 'N' : 'S'; + } else { + hemisferio = decimal >= 0 ? 'E' : 'W'; + } + + return `${graus}° ${minutos}' ${segundos}" ${hemisferio}`; + }; + + const obtemCordenadas = (registro: Registro) => { + return { + latitude: registro.latitude ? converteDecimalParaDMS(registro.latitude, true) : '', + longitude: registro.longitude ? converteDecimalParaDMS(registro.longitude, false) : '' + }; + } + + const renderTable = (registros: Registro[]) => { + return ( + + + + + + + {showCoord && } + {showCoord && } + + + + + {registros.map((item, i) => { + const { especy, familia, genero } = item; + const cordenadas = obtemCordenadas(item); + return ( + + + + + {showCoord && } + {showCoord && } + + + ) + })} + +
Data ColetaFamíliaEspécieLatitudeLongitudeNº do Tombo
{criaData(item)}{familia?.nome}
{genero?.nome} {especy?.nome}
{item.autor}
{cordenadas.latitude}{cordenadas.longitude}{item.hcf}
+ ) + } + + const renderItem = (item: CidadeGroup) => { + return ( +
+
+
+

UF.: {item.estadoSigla}

+

Município: {item.municipio}

+
+
+ {renderTable(item.registros)} + {renderTotalizador(false, item.quantidadeRegistros, item.quantidadeEspecies, item.quantidadeGeneros, item.quantidadeFamilias)} +
+ ) + } + + return ( + + {dados.map(renderItem)} + {renderTotalizador(true)} + + ) +} + +export default RelacaoTombosPorCidade diff --git a/src/routes/relatorio.js b/src/routes/relatorio.js index a0782bc..bb209b7 100644 --- a/src/routes/relatorio.js +++ b/src/routes/relatorio.js @@ -343,6 +343,28 @@ export default app => { controller.obtemDadosDoRelatorioDeLocalDeColeta, ]); + app.route('/relatorio/tombos-por-cidade') + .get([ + tokensMiddleware([ + TIPOS_USUARIOS.CURADOR, + TIPOS_USUARIOS.OPERADOR, + TIPOS_USUARIOS.IDENTIFICADOR, + ]), + listagensMiddleware, + controller.obtemDadosDoRelatorioDeTombosPorCidade, + ]); + + app.route('/relatorio/tombos-por-cidade') + .post([ + tokensMiddleware([ + TIPOS_USUARIOS.CURADOR, + TIPOS_USUARIOS.OPERADOR, + TIPOS_USUARIOS.IDENTIFICADOR, + ]), + listagensMiddleware, + controller.obtemDadosDoRelatorioDeTombosPorCidade, + ]); + app.route('/relatorio/familias-generos') .get([ tokensMiddleware([ @@ -397,4 +419,5 @@ export default app => { listagensMiddleware, controller.obtemDadosDoRelatorioDeQuantidade, ]); + };