From 2e52a8b0ee62438eae5726154d0e99ccd25268cb Mon Sep 17 00:00:00 2001 From: Yuri Baza Date: Wed, 7 Jan 2026 11:27:38 -0300 Subject: [PATCH 1/8] fix: ajustes na ficha tombo (#322) --- src/controllers/fichas-tombos-controller.js | 25 +++++++-------------- src/views/ficha-tombo.ejs | 19 +++++++++++----- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/controllers/fichas-tombos-controller.js b/src/controllers/fichas-tombos-controller.js index 8fbdd3f3..9e9a961d 100644 --- a/src/controllers/fichas-tombos-controller.js +++ b/src/controllers/fichas-tombos-controller.js @@ -218,25 +218,16 @@ export default function fichaTomboController(request, response, next) { }); }) .then(async resultado => { - const { tombo, identificacao, fotos } = resultado; + // IMPORTANTE: Ao mexerem na ficha tombo, deixem os + // campos como opcionais, pois a ficha pode ser gerada + // mesmo sem possuirem todos os dados preenchidos no tombo - // await Subfamilia.findAll({ - // where: { - // familia_id: dadosTombo.familia?.id, - // }, - // include: [ - // { - // model: Autor, - // attributes: ['id', 'nome'], - // as: 'autor', - // }, - // ], - // }); + const { tombo, identificacao, fotos } = resultado; const coletores = `${!!tombo?.coletore?.nome !== false ? tombo?.coletore?.nome?.concat(' ') : ''}${tombo?.coletor_complementar ? tombo.coletor_complementar?.complementares : ''}`; - const localColeta = tombo.local_coleta; - const cidade = localColeta.cidade || ''; + const localColeta = tombo?.local_coleta; + const cidade = localColeta?.cidade || ''; const estado = cidade?.estado || ''; const pais = estado?.pais || ''; @@ -265,8 +256,8 @@ export default function fichaTomboController(request, response, next) { tombo: { ...tombo, coletores, - latitude: converteDecimalParaGMSSinal(tombo.latitude, true), - longitude: converteDecimalParaGMSSinal(tombo.longitude, true), + latitude: tombo.latitude && converteDecimalParaGMSSinal(tombo.latitude, true), + longitude: tombo.longitude && converteDecimalParaGMSSinal(tombo.longitude, true), data_tombo: formataDataSaida(tombo.data_tombo), data_coleta: formataColunasSeparadas(tombo.data_coleta_dia, tombo.data_coleta_mes, tombo.data_coleta_ano), }, diff --git a/src/views/ficha-tombo.ejs b/src/views/ficha-tombo.ejs index 38f68973..28615bcd 100644 --- a/src/views/ficha-tombo.ejs +++ b/src/views/ficha-tombo.ejs @@ -200,9 +200,16 @@
Identificador: <%- identificador %>
-
- Data: <%- romano_data_identificacao %> -
+ <% if (romano_data_identificacao) { %> +
+ Data: <%- romano_data_identificacao %> +
+ <% } %> + <% if (!romano_data_identificacao) { %> +
+ Data: +
+ <% } %>
Local de Coleta: @@ -210,7 +217,7 @@ <%- localColeta.complemento %> <% } %> <% if (localColeta && localColeta.descricao) { %> - - <%- localColeta.descricao %> + <%- localColeta.descricao %> <% } %> <% if (cidade && cidade.nome) { %> - <%- cidade.nome %> @@ -236,8 +243,8 @@ <% if (tombo.relevo) { %> - Relevo: <%- tombo.relevo.nome %> <% } %> - <% if (tombo.vegetacao) { %> - - Vegetação: <%- tombo.vegetacao.nome %> + <% if (tombo.vegetaco) { %> + - Vegetação: <%- tombo.vegetaco.nome %> <% } %> <% if (tombo && tombo.latitude) { %> - Latitude: <%- tombo.latitude %> From 1e4575c443692c1c931acc4f637fdea6d6baccb8 Mon Sep 17 00:00:00 2001 From: Lucas Dos Santos Vaz <52181258+luscas18@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:28:16 -0300 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20Alterando=20tipagem=20das=20datas=20?= =?UTF-8?q?da=20tabela=20de=20configura=C3=A7=C3=A3o=20(#317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/reflora-controller.js | 123 ++++++++++------ src/controllers/specieslink-controller.js | 136 ++++++++++++------ ...15003403_altera-tipos-data-configuracao.ts | 10 ++ src/herbarium/herbariumdatabase.js | 65 +++++---- src/herbarium/main.js | 6 +- src/models/Configuracao.js | 22 +-- 6 files changed, 232 insertions(+), 130 deletions(-) create mode 100644 src/database/migration/20251215003403_altera-tipos-data-configuracao.ts diff --git a/src/controllers/reflora-controller.js b/src/controllers/reflora-controller.js index c0490658..bcf56498 100644 --- a/src/controllers/reflora-controller.js +++ b/src/controllers/reflora-controller.js @@ -14,45 +14,71 @@ import { * A função preparaRequisicao, faz um select no banco verificando se tem registros * onde o horário de fim é nulo e o serviço é Reflora. Se o resultado dessa consulta * é maior que zero significa que foi retornado algum registro. Se existe algum registro no BD, - * onde a data de fim é nula e o serviço é Reflora eu verifico a periodicidade que é. Se a - * periodicidade for manual, ele não pode nem agendar nem pedir novamente. Agora se a periodicidade - * for semanal, mensal ou a cada dois meses, verificamos se a data atual é diferente dá data de - * próxima atualização se for eu atualizo com o novo valor, independentemente se é manual ou periódica. - * Caso seja a mesma data não poderá ser feito a troca. - * @param {*} request, é a requisição vinda do front end, às vezes pode - * conter alguns parâmetros nesse cabeçalhos para conseguir informações - * específicas. - * @param {*} response, é a resposta que será enviada ao back end. - * @param {*} next, é utilizado para chamar a próxima função da pilha. + * onde a data de fim é nula e o serviço é Reflora eu verifico a periodicidade que é. */ export const preparaRequisicao = (request, response) => { const { periodicidade } = request.query; const proximaAtualizacao = request.query.data_proxima_atualizacao; + selectEstaExecutandoServico(1).then(listaExecucaoReflora => { if (listaExecucaoReflora.length > 0) { - const periodicidadeBD = listaExecucaoReflora[0].dataValues.periodicidade; + const execucao = listaExecucaoReflora[0].dataValues; + const { periodicidade: periodicidadeBD } = execucao; + if (periodicidadeBD === 'MANUAL') { - response.status(200).json(JSON.parse(' { "result": "failed" } ')); - } else if ((periodicidadeBD === 'SEMANAL') || (periodicidadeBD === '1MES') || (periodicidadeBD === '2MESES')) { - if (moment().format('DD/MM/YYYY') !== listaExecucaoReflora[0].dataValues.data_proxima_atualizacao) { - const { id } = listaExecucaoReflora[0].dataValues; - atualizaTabelaConfiguracao(1, id, getHoraAtual(), null, periodicidade, proximaAtualizacao).then(() => { - response.status(200).json(JSON.parse(' { "result": "success" } ')); + response.status(200).json({ result: 'failed' }); + return; + } + + if ( + periodicidadeBD === 'SEMANAL' + || periodicidadeBD === '1MES' + || periodicidadeBD === '2MESES' + ) { + const { data_proxima_atualizacao: dataProximaAtualizacao } = execucao; + + const podeExecutar = !dataProximaAtualizacao + || moment().isAfter(moment(dataProximaAtualizacao), 'day'); + + if (podeExecutar) { + atualizaTabelaConfiguracao( + 1, + execucao.id, + getHoraAtual(), + null, + periodicidade, + proximaAtualizacao, + ).then(() => { + response.status(200).json({ result: 'success' }); }); } else { - response.status(200).json(JSON.parse(' { "result": "failed" } ')); + response.status(200).json({ result: 'failed' }); } } } else { selectTemExecucaoServico(1).then(execucaoReflora => { if (execucaoReflora.length === 0) { - insereExecucao(getHoraAtual(), null, periodicidade, proximaAtualizacao, 1).then(() => { - response.status(200).json(JSON.parse(' { "result": "success" } ')); + insereExecucao( + getHoraAtual(), + null, + periodicidade, + proximaAtualizacao, + 1, + ).then(() => { + response.status(200).json({ result: 'success' }); }); } else { - const { id } = execucaoReflora[0].dataValues; - atualizaTabelaConfiguracao(1, id, getHoraAtual(), null, periodicidade, proximaAtualizacao).then(() => { - response.status(200).json(JSON.parse(' { "result": "success" } ')); + const execucao = execucaoReflora[0].dataValues; + + atualizaTabelaConfiguracao( + 1, + execucao.id, + getHoraAtual(), + null, + periodicidade, + proximaAtualizacao, + ).then(() => { + response.status(200).json({ result: 'success' }); }); } }); @@ -62,37 +88,48 @@ export const preparaRequisicao = (request, response) => { /** * A função estaExecutando, faz um select no banco verificando se tem registros - * de execução do Reflora. Se tem execução do reflora é verificado a periodicidade - * se a periodicidade é manual, eu envio um JSON com informações de que está executando - * e que a periodicidade é manual (MANUAL === execução imediata). O mesmo processo vale para - * uma periodicidade que é agendada. Caso não seja retornando nada é retornando ao front - * end que não está sendo executado. - * @param {*} request, é a requisição vinda do front end, às vezes pode - * conter alguns parâmetros nesse cabeçalhos para conseguir informações - * específicas. - * @param {*} response, é a resposta que será enviada ao back end. - * @param {*} next, é utilizado para chamar a próxima função da pilha. + * de execução do Reflora. */ export const estaExecutando = (_, response) => { selectEstaExecutandoServico(1).then(listaExecucaoReflora => { response.header('Access-Control-Allow-Origin', '*'); response.header('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type'); response.header('Access-Control-Allow-Methods', 'GET'); + if (listaExecucaoReflora.length > 0) { - const { periodicidade } = listaExecucaoReflora[0].dataValues; + const execucao = listaExecucaoReflora[0].dataValues; + const { periodicidade } = execucao; + if (periodicidade === 'MANUAL') { - response.status(200).json(JSON.parse(' { "executando": true, "periodicidade": " " } ')); - } else if ((periodicidade === 'SEMANAL') || (periodicidade === '1MES') || (periodicidade === '2MESES')) { - if (moment().format('DD/MM/YYYY') !== listaExecucaoReflora[0].dataValues.data_proxima_atualizacao) { - response.status(200).json(JSON.parse(` { "executando": false, "periodicidade": "${periodicidade}" } `)); - } else { - response.status(200).json(JSON.parse(` { "executando": true, "periodicidade": "${periodicidade}" } `)); - } + response.status(200).json({ + executando: true, + periodicidade: ' ', + }); + return; + } + + if ( + periodicidade === 'SEMANAL' + || periodicidade === '1MES' + || periodicidade === '2MESES' + ) { + const { data_proxima_atualizacao: dataProximaAtualizacao } = execucao; + + const executando = !!dataProximaAtualizacao + && !moment().isAfter(moment(dataProximaAtualizacao), 'day'); + + response.status(200).json({ + executando, + periodicidade, + }); } } else { - response.status(200).json(JSON.parse(' { "executando": false, "periodicidade": " " } ')); + response.status(200).json({ + executando: false, + periodicidade: ' ', + }); } }); }; -export default { }; +export default {}; diff --git a/src/controllers/specieslink-controller.js b/src/controllers/specieslink-controller.js index 5990fdd6..09ce86ac 100644 --- a/src/controllers/specieslink-controller.js +++ b/src/controllers/specieslink-controller.js @@ -12,47 +12,80 @@ import { /** * A função preparaRequisicao, faz um select no banco verificando se tem registros - * onde o horário de fim é nulo e o serviço é Specieslink. Se o resultado dessa consulta + * onde o horário de fim é nulo e o serviço é SpeciesLink. Se o resultado dessa consulta * é maior que zero significa que foi retornado algum registro. Se existe algum registro no BD, - * onde a data de fim é nula e o serviço é Specieslink eu verifico a periodicidade que é. Se a + * onde a data de fim é nula e o serviço é SpeciesLink eu verifico a periodicidade que é. Se a * periodicidade for manual, ele não pode nem agendar nem pedir novamente. Agora se a periodicidade - * for semanal, mensal ou a cada dois meses, verificamos se a data atual é diferente dá data de - * próxima atualização se for eu atualizo com o novo valor, independentemente se é manual ou periódica. - * Caso seja a mesma data não poderá ser feito a troca. - * @param {*} request, é a requisição vinda do front end, às vezes pode - * conter alguns parâmetros nesse cabeçalhos para conseguir informações - * específicas. - * @param {*} response, é a resposta que será enviada ao back end. - * @param {*} next, é utilizado para chamar a próxima função da pilha. + * for semanal, mensal ou a cada dois meses, verificamos se a data atual é diferente da data de + * próxima atualização; se for, eu atualizo com o novo valor. + * Caso seja a mesma data, não poderá ser feito a troca. + * + * @param {*} request requisição vinda do front end + * @param {*} response resposta enviada ao front end */ export const preparaRequisicao = (request, response) => { const { periodicidade } = request.query; const proximaAtualizacao = request.query.data_proxima_atualizacao; + selectEstaExecutandoServico(2).then(listaExecucaoSpecieslink => { if (listaExecucaoSpecieslink.length > 0) { - const periodicidadeBD = listaExecucaoSpecieslink[0].dataValues.periodicidade; + const execucao = listaExecucaoSpecieslink[0].dataValues; + const periodicidadeBD = execucao.periodicidade; + if (periodicidadeBD === 'MANUAL') { - response.status(200).json(JSON.parse(' { "result": "failed" } ')); - } else if ((periodicidadeBD === 'SEMANAL') || (periodicidadeBD === '1MES') || (periodicidadeBD === '2MESES')) { - if (moment().format('DD/MM/YYYY') !== listaExecucaoSpecieslink[0].dataValues.data_proxima_atualizacao) { - const { id } = listaExecucaoSpecieslink[0].dataValues; - atualizaTabelaConfiguracao(2, id, getHoraAtual(), null, periodicidade, proximaAtualizacao).then(() => { - response.status(200).json(JSON.parse(' { "result": "success" } ')); + response.status(200).json({ result: 'failed' }); + return; + } + + if ( + periodicidadeBD === 'SEMANAL' + || periodicidadeBD === '1MES' + || periodicidadeBD === '2MESES' + ) { + const dataProximaAtualizacao = execucao.data_proxima_atualizacao; + + const podeExecutar = !dataProximaAtualizacao + || moment().isAfter(moment(dataProximaAtualizacao), 'day'); + + if (podeExecutar) { + atualizaTabelaConfiguracao( + 2, + execucao.id, + getHoraAtual(), + null, + periodicidade, + proximaAtualizacao, + ).then(() => { + response.status(200).json({ result: 'success' }); }); } else { - response.status(200).json(JSON.parse(' { "result": "failed" } ')); + response.status(200).json({ result: 'failed' }); } } } else { selectTemExecucaoServico(2).then(execucaoSpecieslink => { if (execucaoSpecieslink.length === 0) { - insereExecucao(getHoraAtual(), null, periodicidade, proximaAtualizacao, 2).then(() => { - response.status(200).json(JSON.parse(' { "result": "success" } ')); + insereExecucao( + getHoraAtual(), + null, + periodicidade, + proximaAtualizacao, + 2, + ).then(() => { + response.status(200).json({ result: 'success' }); }); } else { - const { id } = execucaoSpecieslink[0].dataValues; - atualizaTabelaConfiguracao(2, id, getHoraAtual(), null, periodicidade, proximaAtualizacao).then(() => { - response.status(200).json(JSON.parse(' { "result": "success" } ')); + const execucao = execucaoSpecieslink[0].dataValues; + + atualizaTabelaConfiguracao( + 2, + execucao.id, + getHoraAtual(), + null, + periodicidade, + proximaAtualizacao, + ).then(() => { + response.status(200).json({ result: 'success' }); }); } }); @@ -62,37 +95,54 @@ export const preparaRequisicao = (request, response) => { /** * A função estaExecutando, faz um select no banco verificando se tem registros - * de execução do Specieslink. Se tem execução do Specieslink é verificado a periodicidade - * se a periodicidade é manual, eu envio um JSON com informações de que está executando - * e que a periodicidade é manual (MANUAL === execução imediata). O mesmo processo vale para - * uma periodicidade que é agendada. Caso não seja retornando nada é retornando ao front - * end que não está sendo executado. - * @param {*} request, é a requisição vinda do front end, às vezes pode - * conter alguns parâmetros nesse cabeçalhos para conseguir informações - * específicas. - * @param {*} response, é a resposta que será enviada ao back end. - * @param {*} next, é utilizado para chamar a próxima função da pilha. + * de execução do SpeciesLink. Se tem execução é verificado a periodicidade. + * Se a periodicidade é manual, eu envio um JSON informando que está executando. + * O mesmo processo vale para uma periodicidade agendada. + * Caso não exista execução, é retornado que não está executando. + * + * @param {*} request requisição vinda do front end + * @param {*} response resposta enviada ao front end */ export const estaExecutando = (_, response) => { selectEstaExecutandoServico(2).then(listaExecucaoSpecieslink => { response.header('Access-Control-Allow-Origin', '*'); response.header('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type'); response.header('Access-Control-Allow-Methods', 'GET'); + if (listaExecucaoSpecieslink.length > 0) { - const { periodicidade } = listaExecucaoSpecieslink[0].dataValues; + const execucao = listaExecucaoSpecieslink[0].dataValues; + const { periodicidade } = execucao; + if (periodicidade === 'MANUAL') { - response.status(200).json(JSON.parse(' { "executando": true, "periodicidade": " " } ')); - } else if ((periodicidade === 'SEMANAL') || (periodicidade === '1MES') || (periodicidade === '2MESES')) { - if (moment().format('DD/MM/YYYY') !== listaExecucaoSpecieslink[0].dataValues.data_proxima_atualizacao) { - response.status(200).json(JSON.parse(` { "executando": false, "periodicidade": "${periodicidade}" } `)); - } else { - response.status(200).json(JSON.parse(` { "executando": true, "periodicidade": "${periodicidade}" } `)); - } + response.status(200).json({ + executando: true, + periodicidade: ' ', + }); + return; + } + + if ( + periodicidade === 'SEMANAL' + || periodicidade === '1MES' + || periodicidade === '2MESES' + ) { + const dataProximaAtualizacao = execucao.data_proxima_atualizacao; + + const executando = !!dataProximaAtualizacao + && !moment().isAfter(moment(dataProximaAtualizacao), 'day'); + + response.status(200).json({ + executando, + periodicidade, + }); } } else { - response.status(200).json(JSON.parse(' { "executando": false, "periodicidade": " " } ')); + response.status(200).json({ + executando: false, + periodicidade: ' ', + }); } }); }; -export default { }; +export default {}; diff --git a/src/database/migration/20251215003403_altera-tipos-data-configuracao.ts b/src/database/migration/20251215003403_altera-tipos-data-configuracao.ts new file mode 100644 index 00000000..79332d52 --- /dev/null +++ b/src/database/migration/20251215003403_altera-tipos-data-configuracao.ts @@ -0,0 +1,10 @@ +import { Knex } from 'knex' + +export async function run(knex: Knex): Promise { + await knex.raw(` + ALTER TABLE configuracao + MODIFY hora_inicio TIME NOT NULL, + MODIFY hora_fim TIME NULL, + MODIFY data_proxima_atualizacao DATETIME NULL; + `) +} diff --git a/src/herbarium/herbariumdatabase.js b/src/herbarium/herbariumdatabase.js index 803335a8..2c630ac8 100644 --- a/src/herbarium/herbariumdatabase.js +++ b/src/herbarium/herbariumdatabase.js @@ -131,11 +131,19 @@ export function selectEstaExecutandoServico(idServico) { export function insereExecucao(horaAtual, horaFim, periodicidadeUsuario, proximaAtualizacao, servicoUsuario) { const tabelaConfiguracao = modeloConfiguracao(conexao, Sequelize); const promessa = Q.defer(); + const horaInicioFormatada = typeof horaAtual === 'string' + ? horaAtual + : horaAtual.toISOString().substring(11, 19); + + const horaFimFormatada = horaFim ? (typeof horaFim === 'string' ? horaFim : horaFim.toISOString().substring(11, 19)) : null; + + const proximaAtualizacaoISO = proximaAtualizacao ? new Date(proximaAtualizacao) : null; + tabelaConfiguracao.create({ - hora_inicio: horaAtual, - hora_fim: horaFim, + hora_inicio: horaInicioFormatada, + hora_fim: horaFimFormatada, periodicidade: periodicidadeUsuario, - data_proxima_atualizacao: proximaAtualizacao, + data_proxima_atualizacao: proximaAtualizacaoISO, nome_arquivo: null, servico: servicoUsuario, }).then(() => { @@ -161,38 +169,43 @@ export function insereExecucao(horaAtual, horaFim, periodicidadeUsuario, proxima * @return promessa.promise, como é assíncrono ele só retorna quando resolver, ou seja, * quando terminar de realizar a atualização. */ -export function atualizaTabelaConfiguracao(idServico, idExecucao, horaInicio, horaFim, periodicidadeUsuario, proximaAtualizacao) { - if (idServico === 1) { - const tabelaConfiguracaoReflora = modeloConfiguracao(conexao, Sequelize); - const promessa = Q.defer(); - tabelaConfiguracaoReflora.update( - { - hora_inicio: horaInicio, - hora_fim: horaFim, - periodicidade: periodicidadeUsuario, - data_proxima_atualizacao: proximaAtualizacao, - }, - { where: { id: idExecucao } }, - ).then(() => { - promessa.resolve(); - }); - return promessa.promise; - } - const tabelaConfiguracaoSpeciesLink = modeloConfiguracao(conexao, Sequelize); +export function atualizaTabelaConfiguracao( + idServico, + idExecucao, + horaInicio, + horaFim, + periodicidadeUsuario, + proximaAtualizacao, +) { + const tabelaConfiguracao = modeloConfiguracao(conexao, Sequelize); const promessa = Q.defer(); - tabelaConfiguracaoSpeciesLink.update( + + // Normaliza hora_inicio + const horaInicioFormatada = horaInicio instanceof Date + ? horaInicio.toISOString().substring(11, 19) + : horaInicio; + + // Normaliza hora_fim + const horaFimFormatada = horaFim ? (horaFim instanceof Date ? horaFim.toISOString().substring(11, 19) : horaFim) : null; + + // Normaliza data_proxima_atualizacao (ISO) + const proximaAtualizacaoISO = proximaAtualizacao + ? new Date(proximaAtualizacao) + : null; + + tabelaConfiguracao.update( { - hora_inicio: horaInicio, - hora_fim: horaFim, + hora_inicio: horaInicioFormatada, + hora_fim: horaFimFormatada, periodicidade: periodicidadeUsuario, - data_proxima_atualizacao: proximaAtualizacao, + data_proxima_atualizacao: proximaAtualizacaoISO, }, { where: { id: idExecucao } }, ).then(() => { promessa.resolve(); }); - return promessa.promise; + return promessa.promise; } /** diff --git a/src/herbarium/main.js b/src/herbarium/main.js index 9da5e197..89bba3dc 100644 --- a/src/herbarium/main.js +++ b/src/herbarium/main.js @@ -138,7 +138,11 @@ function ehPossivelFazerComparacao(nomeArquivo, idServico) { */ export function preparaExecucao(existeExecucao, idServico) { const promessa = Q.defer(); - const nomeArquivo = processaNomeLog(existeExecucao.dataValues.hora_inicio); + const horaInicio = existeExecucao.dataValues.hora_inicio; + const horaInicioFormatada = horaInicio instanceof Date + ? horaInicio.toISOString().substring(11, 19) // HH:mm:ss + : horaInicio; + const nomeArquivo = processaNomeLog(horaInicioFormatada); ehPossivelFazerComparacao(nomeArquivo, idServico).then(() => { const { id } = existeExecucao.dataValues; let conteudoLOG; diff --git a/src/models/Configuracao.js b/src/models/Configuracao.js index 61723144..9fa7be34 100644 --- a/src/models/Configuracao.js +++ b/src/models/Configuracao.js @@ -1,27 +1,16 @@ -function associate(/* modelos */) { +function associate(/* models */) { + // não há associações para este model } -/** -CREATE TABLE `configuracao` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `hora_inicio` varchar(19) NOT NULL, - `hora_fim` varchar(19) DEFAULT NULL, - `periodicidade` enum('MANUAL','SEMANAL','1MES','2MESES') DEFAULT NULL, - `data_proxima_atualizacao` varchar(10) DEFAULT NULL, - `nome_arquivo` varchar(50) DEFAULT NULL, - `servico` enum('REFLORA','SPECIESLINK') DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET='latin1'; - */ export default (Sequelize, DataTypes) => { const attributes = { hora_inicio: { - type: DataTypes.STRING(19), + type: DataTypes.TIME, allowNull: false, }, hora_fim: { - type: DataTypes.STRING(19), + type: DataTypes.TIME, allowNull: true, }, periodicidade: { @@ -30,7 +19,7 @@ export default (Sequelize, DataTypes) => { allowNull: true, }, data_proxima_atualizacao: { - type: DataTypes.STRING(10), + type: DataTypes.DATE, allowNull: true, }, nome_arquivo: { @@ -45,7 +34,6 @@ export default (Sequelize, DataTypes) => { const options = { freezeTableName: false, - // Disabilita o created_at e updated_at timestamps: false, tableName: 'configuracao', }; From 0c7b5f41120807fe8e13a58a2d53cca0378cbca8 Mon Sep 17 00:00:00 2001 From: Lucas Dos Santos Vaz <52181258+luscas18@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:17:23 -0300 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20Alterando=20as=20urls=20que=20s?= =?UTF-8?q?=C3=A3o=20enviadas=20na=20recupera=C3=A7=C3=A3o=20de=20senha=20?= =?UTF-8?q?(#283)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Edvaldo Szymonek --- .env.example | 2 +- src/controllers/usuarios-controller.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index ba352751..513c62f4 100644 --- a/.env.example +++ b/.env.example @@ -23,4 +23,4 @@ SMTP_USER="seu-email@gmail.com" SMTP_PASS="sua-senha-de-app-de-16-digitos" SMTP_FROM="Sistema HCF " -URL_PAINEL=http://localhost:5173/ +PAINEL_BASE_URL=http://localhost:5173 diff --git a/src/controllers/usuarios-controller.js b/src/controllers/usuarios-controller.js index adb4d034..1d318912 100644 --- a/src/controllers/usuarios-controller.js +++ b/src/controllers/usuarios-controller.js @@ -349,9 +349,9 @@ export const solicitarTrocaDeSenha = async (request, response, next) => { }, }); - const link = `${process.env.URL_PAINEL}reset-senha?token=${token}`; + const link = `${process.env.PAINEL_BASE_URL}/alterar-senha?token=${token}`; await transporter.sendMail({ - from: process.env.SMTP_FROM || '"Sistema HCF" ', + from: process.env.SMTP_FROM || '"Sistema Herbário" ', to: email, subject: 'Redefinição de senha', html: ` From b6df70457be4b51d0ad81774c929cbcb2cb2a7ab Mon Sep 17 00:00:00 2001 From: Moran <105233020+feliperm17@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:24:49 -0300 Subject: [PATCH 4/8] =?UTF-8?q?Implementa=20script=20para=20atualiza=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20pol=C3=ADgonos=20de=20cidades=20(#265)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + src/utils/scripts/update_polygons.ts | 202 +++++++++++++++++++++++++++ yarn.lock | 96 ++++++++++++- 3 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 src/utils/scripts/update_polygons.ts diff --git a/package.json b/package.json index e241385b..7ca7ef0c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "audit:fix": "npm audit fix" }, "dependencies": { + "@types/pg": "^8.15.6", "axios": "1.13.2", "bcrypt": "6.0.0", "body-parser": "2.2.0", @@ -47,6 +48,7 @@ "multer": "2.0.2", "mysql2": "3.15.3", "nodemailer": "7.0.10", + "pg": "^8.16.3", "puppeteer": "24.28.0", "q": "1.5.1", "react": "19.2.0", diff --git a/src/utils/scripts/update_polygons.ts b/src/utils/scripts/update_polygons.ts new file mode 100644 index 00000000..5c8d0e23 --- /dev/null +++ b/src/utils/scripts/update_polygons.ts @@ -0,0 +1,202 @@ +import axios from 'axios' +import dotenv from 'dotenv' +import pLimit from 'p-limit' +import path from 'path' +import { Client } from 'pg' +import { fileURLToPath } from 'url' +import wkx from 'wkx' + +interface MunicipioIBGE { + id: number + nome: string + nome_normalizado: string +} + +interface MunicipioIBGEResponse { + id: number + nome: string +} + +interface Cidade { + id: number + nome: string + pol_wkb: Buffer | null +} + +interface ResultadoProcessamento { + inserido: number + atualizado: number + erro: number +} + +const currentFilename = fileURLToPath(import.meta.url) +const currentDirname = path.dirname(currentFilename) +const projectRoot = path.resolve(currentDirname, '..', '..', '..') + +dotenv.config({ path: path.join(projectRoot, '.env') }) + +const { + PG_DATABASE, + PG_USERNAME, + PG_PASSWORD, + PG_HOST, + PG_PORT, + DATABASE_URL +} = process.env + +const connectionString + = DATABASE_URL + || `postgresql://${PG_USERNAME}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/${PG_DATABASE}` + +function isValidConnectionString(cs: string | undefined): boolean { + return typeof cs === 'string' && cs.length > 0 && !/undefined/.test(cs) +} + +if (!isValidConnectionString(connectionString)) { + process.exit(1) +} + +const API_IBGE_MUNICIPIOS = 'https://servicodados.ibge.gov.br/api/v1/localidades/municipios' +const API_IBGE_POLYGON + = 'https://servicodados.ibge.gov.br/api/v3/malhas/municipios/{codigo_ibge}?formato=application/vnd.geo+json&qualidade=minima' + +const MAX_CONCURRENCY = 10 +const REQUEST_DELAY_MS = 150 + +function removerAcentos(str: string): string { + return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '') +} + +function delay(ms: number): Promise { + return new Promise(resolve => { + setTimeout(resolve, ms) + }) +} + +async function carregarListaMunicipios(): Promise { + const resp = await axios.get(API_IBGE_MUNICIPIOS, { timeout: 30000 }) + return resp.data.map((m: MunicipioIBGEResponse) => ({ + id: m.id, + nome: m.nome, + nome_normalizado: removerAcentos(m.nome.toLowerCase()) + })) +} + +function obterCodigoIbge(nome: string, municipios: MunicipioIBGE[]): number | null { + const nomeNorm = removerAcentos(nome.toLowerCase()) + const m = municipios.find(x => x.nome_normalizado === nomeNorm) + return m ? parseInt(String(m.id), 10) : null +} + +async function obterPoligonoIbge(codigoIbge: number): Promise { + const url = API_IBGE_POLYGON.replace('{codigo_ibge}', String(codigoIbge)) + const resp = await axios.get(url, { + responseType: 'arraybuffer', + timeout: 30000 + }) + + const txt = Buffer.from(resp.data).toString('utf-8') + const geojson = JSON.parse(txt) as { features: Array<{ geometry: unknown }> } + + if (!geojson?.features?.length) { + throw new Error(`GeoJSON vazio para codigo ${codigoIbge}`) + } + + const { geometry } = geojson.features[0] + if (!geometry) { + throw new Error(`Sem geometria no GeoJSON para codigo ${codigoIbge}`) + } + + const geom = wkx.Geometry.parseGeoJSON(geometry) + return geom.toWkb() +} + +async function processarCidade( + client: Client, + municipios: MunicipioIBGE[], + cidade: Cidade +): Promise { + const { + id, nome, pol_wkb: polWkb + } = cidade + + if (nome === 'Não Informado') { + return { + inserido: 0, atualizado: 0, erro: 0 + } + } + + const codigoIbge = obterCodigoIbge(nome, municipios) + if (!codigoIbge) { + return { + inserido: 0, atualizado: 0, erro: 1 + } + } + + let polBytes: Buffer + try { + polBytes = await obterPoligonoIbge(codigoIbge) + } catch { + return { + inserido: 0, atualizado: 0, erro: 1 + } + } + + const sql = ` + WITH newgeom AS ( + SELECT ST_Multi(ST_SetSRID(ST_GeomFromWKB($1::bytea, 4674), 4674)) AS g + ) + UPDATE public.cidades + SET poligono = (SELECT g FROM newgeom), updated_at = NOW() + WHERE id = $2 + AND (poligono IS NULL OR NOT ST_Equals(poligono, (SELECT g FROM newgeom))); + ` + + try { + const result = await client.query(sql, [polBytes, id]) + if (result && result.rowCount && result.rowCount > 0) { + return polWkb + ? { + inserido: 0, atualizado: 1, erro: 0 + } + : { + inserido: 1, atualizado: 0, erro: 0 + } + } + return { + inserido: 0, atualizado: 0, erro: 0 + } + } catch { + return { + inserido: 0, atualizado: 0, erro: 1 + } + } +} + +async function main(): Promise { + const municipios = await carregarListaMunicipios() + const client = new Client({ connectionString }) + await client.connect() + + const limit = pLimit(MAX_CONCURRENCY) + + try { + const { rows: cidades } = await client.query( + 'SELECT c.id, c.nome, ST_AsBinary(c.poligono) AS pol_wkb FROM public.cidades c JOIN public.estados e ON c.estado_id = e.id WHERE e.pais_id = 76;' + ) + + const tarefas = cidades.map(c => + limit(async () => { + const res = await processarCidade(client, municipios, c) + await delay(REQUEST_DELAY_MS) + return res + }) + ) + + await Promise.all(tarefas) + } finally { + await client.end() + } +} + +main().catch(() => process.exit(1)) diff --git a/yarn.lock b/yarn.lock index f11cdbae..2a54bff1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1810,6 +1810,15 @@ dependencies: undici-types "~6.21.0" +"@types/pg@^8.15.6": + version "8.15.6" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.15.6.tgz#4df7590b9ac557cbe5479e0074ec1540cbddad9b" + integrity sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ== + dependencies: + "@types/node" "*" + pg-protocol "*" + pg-types "^2.2.0" + "@types/qs@*": version "6.9.18" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.18.tgz#877292caa91f7c1b213032b34626505b746624c2" @@ -5248,11 +5257,67 @@ performance-now@^2.1.0: resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== +pg-cloudflare@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz#a1f3d226bab2c45ae75ea54d65ec05ac6cfafbef" + integrity sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg== + pg-connection-string@2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz#713d82053de4e2bd166fab70cd4f26ad36aab475" integrity sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA== +pg-connection-string@^2.9.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.9.1.tgz#bb1fd0011e2eb76ac17360dc8fa183b2d3465238" + integrity sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.10.1.tgz#481047c720be2d624792100cac1816f8850d31b2" + integrity sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg== + +pg-protocol@*, pg-protocol@^1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.10.3.tgz#ac9e4778ad3f84d0c5670583bab976ea0a34f69f" + integrity sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ== + +pg-types@2.2.0, pg-types@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@^8.16.3: + version "8.16.3" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.16.3.tgz#160741d0b44fdf64680e45374b06d632e86c99fd" + integrity sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw== + dependencies: + pg-connection-string "^2.9.1" + pg-pool "^3.10.1" + pg-protocol "^1.10.3" + pg-types "2.2.0" + pgpass "1.0.5" + optionalDependencies: + pg-cloudflare "^1.2.7" + +pgpass@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + picocolors@^1.0.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" @@ -5309,13 +5374,35 @@ possible-typed-array-names@^1.0.0: postcss@^8.5.6: version "8.5.6" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== dependencies: nanoid "^3.3.11" picocolors "^1.1.1" source-map-js "^1.2.1" +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -6039,6 +6126,11 @@ spdx-license-ids@^3.0.0: resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz" integrity sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg== +split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + sprintf-js@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz" @@ -6774,7 +6866,7 @@ ws@^8.18.3: resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== -xtend@^4.0.2: +xtend@^4.0.0, xtend@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== From 14a63609724b31e4ac6853463943219cbbeceee8 Mon Sep 17 00:00:00 2001 From: Edvaldo Szymonek Date: Mon, 19 Jan 2026 21:38:04 -0300 Subject: [PATCH 5/8] =?UTF-8?q?corrige=20lint=20e=20indenta=C3=A7=C3=A3o?= =?UTF-8?q?=20do=20arquivo=20package.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 182 ++++++++++++------------- src/controllers/usuarios-controller.js | 1 + 2 files changed, 92 insertions(+), 91 deletions(-) diff --git a/package.json b/package.json index 7ca7ef0c..b9b9e3d2 100644 --- a/package.json +++ b/package.json @@ -1,93 +1,93 @@ { - "name": "hcf-api", - "version": "0.1.1", - "description": "Projeto para construção da API para o sistema do Herbário da UTFPR", - "main": "dist/app/index.js", - "author": "Elaine Sangali ", - "license": "MIT", - "private": true, - "engines": { - "node": "^22" - }, - "scripts": { - "migration:create": "tsx --env-file=.env src/database/cli.ts migration:create", - "migration:apply": "tsx --env-file=.env src/database/cli.ts migration:apply", - "tsc:check": "tsc --noEmit", - "lint:eslint": "eslint --cache", - "lint": "run-p tsc:check lint:eslint", - "clean": "rimraf dist", - "build:app": "babel -d ./dist -x '.js,.ts,.tsx' --copy-files --no-copy-ignored ./src", - "start": "nodemon --ext 'js,ts,tsx' --exec babel-node --extensions '.js,.ts,.tsx' ./src/index.js", - "build": "run-s clean build:app", - "test": "vitest --run", - "test:coverage": "vitest --run --coverage", - "prepare": "husky", - "audit": "npm audit --audit-level=moderate", - "audit:fix": "npm audit fix" - }, - "dependencies": { - "@types/pg": "^8.15.6", - "axios": "1.13.2", - "bcrypt": "6.0.0", - "body-parser": "2.2.0", - "commander": "14.0.2", - "cors": "2.8.5", - "date-fns": "4.1.0", - "dotenv": "17.2.3", - "ejs": "^2.6.1", - "express": "5.1.0", - "express-rate-limit": "8.2.1", - "express-validator": "7.3.0", - "fast-csv": "5.0.5", - "helmet": "8.1.0", - "jsonwebtoken": "9.0.2", - "knex": "3.1.0", - "moment": "^2.24.0", - "moment-timezone": "^0.5.21", - "morgan": "1.10.1", - "multer": "2.0.2", - "mysql2": "3.15.3", - "nodemailer": "7.0.10", - "pg": "^8.16.3", - "puppeteer": "24.28.0", - "q": "1.5.1", - "react": "19.2.0", - "react-dom": "19.2.0", - "request": "2.88.2", - "sequelize": "^4.38.0", - "swagger-jsdoc": "6.2.8", - "swagger-ui-express": "5.0.1", - "throttled-queue": "3.0.0", - "tsx": "4.20.6", - "uuid": "13.0.0" - }, - "devDependencies": { - "@babel/cli": "7.28.3", - "@babel/core": "7.28.5", - "@babel/node": "7.28.0", - "@babel/plugin-proposal-class-properties": "7.18.6", - "@babel/preset-env": "7.28.5", - "@babel/preset-react": "7.28.5", - "@babel/preset-typescript": "7.28.5", - "@eslint/js": "9.39.1", - "@stylistic/eslint-plugin": "5.5.0", - "@types/express": "5.0.5", - "@types/react": "19.2.2", - "@types/react-dom": "19.2.2", - "@vitest/coverage-v8": "4.0.7", - "babel-plugin-module-resolver": "5.0.2", - "chai": "6.2.0", - "chai-http": "^5.1.2", - "eslint": "9.39.1", - "eslint-plugin-import-helpers": "2.0.1", - "globals": "16.5.0", - "husky": "9.1.7", - "jiti": "2.6.1", - "nodemon": "3.1.10", - "npm-run-all": "4.1.5", - "rimraf": "6.1.2", - "typescript": "5.9.3", - "typescript-eslint": "8.46.3", - "vitest": "4.0.7" - } + "name": "hcf-api", + "version": "0.1.1", + "description": "Projeto para construção da API para o sistema do Herbário da UTFPR", + "main": "dist/app/index.js", + "author": "Elaine Sangali ", + "license": "MIT", + "private": true, + "engines": { + "node": "^22" + }, + "scripts": { + "migration:create": "tsx --env-file=.env src/database/cli.ts migration:create", + "migration:apply": "tsx --env-file=.env src/database/cli.ts migration:apply", + "typescript:check": "tsc --noEmit", + "lint:eslint": "eslint --cache", + "lint": "run-p typescript:check lint:eslint", + "clean": "rimraf dist", + "build:app": "babel -d ./dist -x '.js,.ts,.tsx' --copy-files --no-copy-ignored ./src", + "start": "nodemon --ext 'js,ts,tsx' --exec babel-node --extensions '.js,.ts,.tsx' ./src/index.js", + "build": "run-s clean build:app", + "test": "vitest --run", + "test:coverage": "vitest --run --coverage", + "prepare": "husky", + "audit": "npm audit --audit-level=moderate", + "audit:fix": "npm audit fix" + }, + "dependencies": { + "@types/pg": "^8.15.6", + "axios": "1.13.2", + "bcrypt": "6.0.0", + "body-parser": "2.2.0", + "commander": "14.0.2", + "cors": "2.8.5", + "date-fns": "4.1.0", + "dotenv": "17.2.3", + "ejs": "^2.6.1", + "express": "5.1.0", + "express-rate-limit": "8.2.1", + "express-validator": "7.3.0", + "fast-csv": "5.0.5", + "helmet": "8.1.0", + "jsonwebtoken": "9.0.2", + "knex": "3.1.0", + "moment": "^2.24.0", + "moment-timezone": "^0.5.21", + "morgan": "1.10.1", + "multer": "2.0.2", + "mysql2": "3.15.3", + "nodemailer": "7.0.10", + "pg": "^8.16.3", + "puppeteer": "24.28.0", + "q": "1.5.1", + "react": "19.2.0", + "react-dom": "19.2.0", + "request": "2.88.2", + "sequelize": "^4.38.0", + "swagger-jsdoc": "6.2.8", + "swagger-ui-express": "5.0.1", + "throttled-queue": "3.0.0", + "tsx": "4.20.6", + "uuid": "13.0.0" + }, + "devDependencies": { + "@babel/cli": "7.28.3", + "@babel/core": "7.28.5", + "@babel/node": "7.28.0", + "@babel/plugin-proposal-class-properties": "7.18.6", + "@babel/preset-env": "7.28.5", + "@babel/preset-react": "7.28.5", + "@babel/preset-typescript": "7.28.5", + "@eslint/js": "9.39.1", + "@stylistic/eslint-plugin": "5.5.0", + "@types/express": "5.0.5", + "@types/react": "19.2.2", + "@types/react-dom": "19.2.2", + "@vitest/coverage-v8": "4.0.7", + "babel-plugin-module-resolver": "5.0.2", + "chai": "6.2.0", + "chai-http": "^5.1.2", + "eslint": "9.39.1", + "eslint-plugin-import-helpers": "2.0.1", + "globals": "16.5.0", + "husky": "9.1.7", + "jiti": "2.6.1", + "nodemon": "3.1.10", + "npm-run-all": "4.1.5", + "rimraf": "6.1.2", + "typescript": "5.9.3", + "typescript-eslint": "8.46.3", + "vitest": "4.0.7" + } } diff --git a/src/controllers/usuarios-controller.js b/src/controllers/usuarios-controller.js index 1d318912..490251ee 100644 --- a/src/controllers/usuarios-controller.js +++ b/src/controllers/usuarios-controller.js @@ -1,4 +1,5 @@ import nodemailer from 'nodemailer'; + import { UserRegistrationDTO } from '../dtos/UserRegistrationDTO'; import BadRequestExeption from '../errors/bad-request-exception'; import limparEspacos from '../helpers/limpa-espaco'; From a2860b53faddef18c8295681a04e89cdeb5f5297 Mon Sep 17 00:00:00 2001 From: Yuri Baza Date: Wed, 28 Jan 2026 19:44:01 -0300 Subject: [PATCH 6/8] Fix/ficha tombo opcionais e ajustes (#324) --- src/controllers/fichas-tombos-controller.js | 6 ++-- src/controllers/relatorios-controller.js | 2 +- src/helpers/coordenadas.js | 37 +++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/controllers/fichas-tombos-controller.js b/src/controllers/fichas-tombos-controller.js index 9e9a961d..0addb97c 100644 --- a/src/controllers/fichas-tombos-controller.js +++ b/src/controllers/fichas-tombos-controller.js @@ -1,7 +1,7 @@ import moment from 'moment-timezone'; import path from 'path'; -import { converteDecimalParaGMSSinal } from '~/helpers/coordenadas'; +import { converteDecimalParaGrausMinutosSegundos } from '~/helpers/coordenadas'; // import identificador from '~/routes/identificador'; @@ -256,8 +256,8 @@ export default function fichaTomboController(request, response, next) { tombo: { ...tombo, coletores, - latitude: tombo.latitude && converteDecimalParaGMSSinal(tombo.latitude, true), - longitude: tombo.longitude && converteDecimalParaGMSSinal(tombo.longitude, true), + latitude: tombo.latitude && converteDecimalParaGrausMinutosSegundos(tombo.latitude, true, true), + longitude: tombo.longitude && converteDecimalParaGrausMinutosSegundos(tombo.longitude, false, true), data_tombo: formataDataSaida(tombo.data_tombo), data_coleta: formataColunasSeparadas(tombo.data_coleta_dia, tombo.data_coleta_mes, tombo.data_coleta_ano), }, diff --git a/src/controllers/relatorios-controller.js b/src/controllers/relatorios-controller.js index 68e391b7..3efa0d36 100644 --- a/src/controllers/relatorios-controller.js +++ b/src/controllers/relatorios-controller.js @@ -589,7 +589,7 @@ export const obtemDadosDoRelatorioDeLocalDeColeta = async (req, res, next) => { ReportLocalColeta, { dados: dadosFormatados.locais, total: dadosFormatados?.quantidadeTotal || 0, - textoFiltro: formataTextFilter(local, dataInicio, dataFim || new Date()), + textoFiltro: formataTextFilter(undefined, dataInicio, dataFim || new Date()), showCoord: showCoord === 'true', }); const readable = new Readable(); diff --git a/src/helpers/coordenadas.js b/src/helpers/coordenadas.js index ca6836c7..06f8f79e 100644 --- a/src/helpers/coordenadas.js +++ b/src/helpers/coordenadas.js @@ -144,3 +144,40 @@ export const converteDecimalParaGMSSinal = (decimal, isLat) => { return result; }; + +export const converteDecimalParaGrausMinutosSegundos = (gDec, ehLat, formatada) => { + let graus; + let minutos; + let aux; + let segundos; + let direcao; + + graus = parseInt(gDec); + aux = (graus - gDec) * 60; + minutos = parseInt(aux); + aux = (aux - minutos) * 60; + segundos = aux; + + if (ehLat) { + // Eixo X + if (graus < 0) direcao = 'W'; + else direcao = 'E'; + } else { + // Eixo Y + if (graus >= 0) direcao = 'N'; + else direcao = 'S'; + } + + if (formatada) { + return `${Math.abs(graus)}°${Math.abs(minutos)}'${Math.abs(segundos).toFixed(2) + .replace('.', ',')}" ${direcao}`; + } + + return { + graus: Math.abs(graus), + minutos: Math.abs(minutos), + segundos: Math.abs(segundos).toFixed(2) + .replace('.', ','), + direcao, + }; +}; From b341f1a456b783ad6d26aa6bbed4ad9fd39adb06 Mon Sep 17 00:00:00 2001 From: Yuri Baza Date: Tue, 3 Feb 2026 22:32:41 -0300 Subject: [PATCH 7/8] Ajustes solicitados no relatorio de coleta (#329) --- src/controllers/relatorios-controller.js | 2 +- src/reports/assets/styles/root.css | 13 ++++++++++--- src/reports/templates/LocaisColeta.tsx | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/controllers/relatorios-controller.js b/src/controllers/relatorios-controller.js index 3efa0d36..cc394d15 100644 --- a/src/controllers/relatorios-controller.js +++ b/src/controllers/relatorios-controller.js @@ -478,7 +478,7 @@ export const obtemDadosDoRelatorioDeLocalDeColeta = async (req, res, next) => { let whereData = {}; if (local) { whereLocal = { - descricao: { [Op.like]: `%${local}%` }, + id: local, }; } if (dataInicio) { diff --git a/src/reports/assets/styles/root.css b/src/reports/assets/styles/root.css index 6cdb7611..a1acd525 100644 --- a/src/reports/assets/styles/root.css +++ b/src/reports/assets/styles/root.css @@ -109,8 +109,8 @@ tfoot { .grupoLocalColeta { display: flex; - align-items: center; gap: 1rem; + width: 100%; } .grupoLocalColeta div:first-child { @@ -124,12 +124,19 @@ tfoot { } .grupoLocalColeta div h1 { - white-space: nowrap; /* impede quebra de linha */ - overflow: hidden; /* esconde o que ultrapassa */ + white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; text-align: left; } +.grupoLocalColeta div h1.longText { + text-align: left; + word-break: break-word; + max-width: 100%; + white-space: normal; +} + .grade-imagens { display: grid; grid-template-columns: repeat(3, 1fr); /* 3 colunas */ diff --git a/src/reports/templates/LocaisColeta.tsx b/src/reports/templates/LocaisColeta.tsx index 57870dfd..dcf0421a 100644 --- a/src/reports/templates/LocaisColeta.tsx +++ b/src/reports/templates/LocaisColeta.tsx @@ -134,7 +134,7 @@ function RelacaoLocaisColeta({ dados, total, textoFiltro, showCoord = false }: R

Município: {item.municipio}

-

Local: {item.local}

+

Local: {item.local}

{renderTable(item.registros)} From 6f468796593cebaed6846cacfa9de705a7105cdd Mon Sep 17 00:00:00 2001 From: Moran <105233020+feliperm17@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:33:11 -0300 Subject: [PATCH 8/8] feat/Script atualizar latitude e longitude da cidade (#328) --- src/utils/scripts/update_coordinates.ts | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/utils/scripts/update_coordinates.ts diff --git a/src/utils/scripts/update_coordinates.ts b/src/utils/scripts/update_coordinates.ts new file mode 100644 index 00000000..445ee2c7 --- /dev/null +++ b/src/utils/scripts/update_coordinates.ts @@ -0,0 +1,52 @@ +import dotenv from 'dotenv' +import path from 'path' +import { Client } from 'pg' +import { fileURLToPath } from 'url' + +const currentFilename = fileURLToPath(import.meta.url) +const currentDirname = path.dirname(currentFilename) +const projectRoot = path.resolve(currentDirname, '..', '..', '..') + +dotenv.config({ path: path.join(projectRoot, '.env') }) + +const { + PG_DATABASE, + PG_USERNAME, + PG_PASSWORD, + PG_HOST, + PG_PORT, + DATABASE_URL +} = process.env + +const connectionString + = DATABASE_URL + || `postgresql://${PG_USERNAME}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/${PG_DATABASE}` + +function isValidConnectionString(cs: string | undefined): boolean { + return typeof cs === 'string' && cs.length > 0 && !/undefined/.test(cs) +} + +if (!isValidConnectionString(connectionString)) { + process.exit(1) +} + +async function main(): Promise { + const client = new Client({ connectionString }) + await client.connect() + + try { + await client.query(` + UPDATE cidades + SET + latitude = ST_Y(ST_PointOnSurface(poligono)), + longitude = ST_X(ST_PointOnSurface(poligono)), + updated_at = NOW() + WHERE poligono IS NOT NULL + AND (latitude IS NULL OR longitude IS NULL); + `) + } finally { + await client.end() + } +} + +main().catch(() => process.exit(1))