diff --git a/src/controllers/relatorios-controller.js b/src/controllers/relatorios-controller.js index ee4cc3b6..d582f09f 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 ed9e7e80..1c71458f 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 00000000..09a99394 --- /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 ( +
| Data Coleta | +Família | +Espécie | + {showCoord &&Latitude | } + {showCoord &&Longitude | } +Nº do Tombo | +
|---|---|---|---|---|---|
| {criaData(item)} | +{familia?.nome} | +{genero?.nome} {especy?.nome} {item.autor} |
+ {showCoord && {cordenadas.latitude} | } + {showCoord &&{cordenadas.longitude} | } +{item.hcf} | +