From 8f5958b5eb6463524b69e98474b13e19c98fa64e Mon Sep 17 00:00:00 2001 From: Edvaldo Szymonek Date: Fri, 24 Apr 2026 22:53:42 -0300 Subject: [PATCH 1/6] =?UTF-8?q?hcf-web#93=20adiciona=20verifica=C3=A7?= =?UTF-8?q?=C3=A3o=20da=20issue=20no=20t=C3=ADtulo=20do=20pull=20request?= =?UTF-8?q?=20(#414)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pull_request.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 296f9cd..a578879 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -4,6 +4,20 @@ on: - pull_request jobs: + check-issue-reference: + runs-on: ubuntu-latest + if: github.base_ref == 'development' + steps: + - name: Check if the issue is linked + env: + PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }} + run: | + if ! echo "$PULL_REQUEST_TITLE" | grep -qP '^[a-zA-Z0-9-]+#\d+'; then + echo "Pull request title must start with '#' (e.g. 'hcf-web#42 my changes')" + echo "Pull request title: $PULL_REQUEST_TITLE" + exit 1 + fi + lint: runs-on: ubuntu-latest steps: From e26869a34dc9919649818b32ddc140c2c36fe20d Mon Sep 17 00:00:00 2001 From: Edvaldo Szymonek Date: Fri, 24 Apr 2026 23:04:53 -0300 Subject: [PATCH 2/6] =?UTF-8?q?remove=20checagem=20do=20t=C3=ADtulo=20do?= =?UTF-8?q?=20pull=20request=20temporariamente=20(#417)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pull_request.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index a578879..296f9cd 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -4,20 +4,6 @@ on: - pull_request jobs: - check-issue-reference: - runs-on: ubuntu-latest - if: github.base_ref == 'development' - steps: - - name: Check if the issue is linked - env: - PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }} - run: | - if ! echo "$PULL_REQUEST_TITLE" | grep -qP '^[a-zA-Z0-9-]+#\d+'; then - echo "Pull request title must start with '#' (e.g. 'hcf-web#42 my changes')" - echo "Pull request title: $PULL_REQUEST_TITLE" - exit 1 - fi - lint: runs-on: ubuntu-latest steps: From 47f210f5db9e9e095eb032ff37eda7ce4abdd4dc Mon Sep 17 00:00:00 2001 From: Lucas Dos Santos Vaz <52181258+luscas18@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:02:23 -0300 Subject: [PATCH 3/6] =?UTF-8?q?Migration=20de=20restrutura=C3=A7=C3=A3o=20?= =?UTF-8?q?da=20tabela=20de=20variedades=20(#380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...311154039_corrige-duplicatas-variedades.ts | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/database/migration/20260311154039_corrige-duplicatas-variedades.ts diff --git a/src/database/migration/20260311154039_corrige-duplicatas-variedades.ts b/src/database/migration/20260311154039_corrige-duplicatas-variedades.ts new file mode 100644 index 0000000..35f27c5 --- /dev/null +++ b/src/database/migration/20260311154039_corrige-duplicatas-variedades.ts @@ -0,0 +1,143 @@ +import { Knex } from 'knex' + +function normalize(value: unknown): string { + const safeValue = typeof value === 'string' || typeof value === 'number' ? String(value) : '' + + return safeValue + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .replace(/\s+/g, ' ') + .toLowerCase() +} + +type SnapshotItem = { + tomboId: number + key: string +} + +type NovaVariedade = { + key: string + nome: string + familia_id: number + genero_id: number + especie_id: number + autor_id: number | null +} + +export async function run(knex: Knex): Promise { + await knex.transaction(async trx => { + const tombos = await trx('tombos as t') + .join('familias as f', 't.familia_id', 'f.id') + .join('generos as g', 't.genero_id', 'g.id') + .join('especies as e', 't.especie_id', 'e.id') + .leftJoin('variedades as v', 't.variedade_id', 'v.id') + .select( + 't.hcf as tombo_id', + 't.familia_id', + 't.genero_id', + 't.especie_id', + 'v.nome as variedade_nome', + 'v.autor_id as variedade_autor_id', + 'f.nome as familia_nome', + 'g.nome as genero_nome', + 'e.nome as especie_nome' + ) + .whereNotNull('t.familia_id') + .whereNotNull('t.genero_id') + .whereNotNull('t.especie_id') + .whereNotNull('v.nome') + + const snapshot: SnapshotItem[] = [] + const variedadesMap = new Map() + + for (const row of tombos) { + const key = [ + normalize(row.variedade_nome), + normalize(row.familia_nome), + normalize(row.genero_nome), + normalize(row.especie_nome), + row.variedade_autor_id + ].join('|') + + snapshot.push({ + tomboId: Number(row.tombo_id), + key + }) + + if (!variedadesMap.has(key)) { + variedadesMap.set(key, { + key, + nome: String(row.variedade_nome).trim(), + familia_id: Number(row.familia_id), + genero_id: Number(row.genero_id), + especie_id: Number(row.especie_id), + autor_id: row.variedade_autor_id ? Number(row.variedade_autor_id) : null + }) + } + } + + const novasVariedades = Array.from(variedadesMap.values()) + const tomboIds = snapshot.map(item => item.tomboId) + + if (tomboIds.length > 0) { + await trx('tombos') + .whereIn('hcf', tomboIds) + .update({ variedade_id: null }) + } + + await trx('variedades').del() + + const inserted = await trx('variedades') + .insert( + novasVariedades.map(item => ({ + nome: item.nome, + familia_id: item.familia_id, + genero_id: item.genero_id, + especie_id: item.especie_id, + autor_id: item.autor_id + })) + ) + .returning([ + 'id', + 'nome', + 'familia_id', + 'genero_id', + 'especie_id', + 'autor_id' + ]) + + const newIdMap = new Map() + + for (const row of inserted) { + const matchingTombo = tombos.find( + t => + Number(t.familia_id) === Number(row.familia_id) + && Number(t.genero_id) === Number(row.genero_id) + && Number(t.especie_id) === Number(row.especie_id) + && normalize(t.variedade_nome) === normalize(row.nome) + && (t.variedade_autor_id ? Number(t.variedade_autor_id) : null) === (row.autor_id ? Number(row.autor_id) : null) + ) + + const key = [ + normalize(row.nome), + normalize(matchingTombo?.familia_nome), + normalize(matchingTombo?.genero_nome), + normalize(matchingTombo?.especie_nome), + row.autor_id ? Number(row.autor_id) : null + ].join('|') + + newIdMap.set(key, Number(row.id)) + } + + for (const item of snapshot) { + const novaVariedadeId = newIdMap.get(item.key) + + if (novaVariedadeId) { + await trx('tombos') + .where('hcf', item.tomboId) + .update({ variedade_id: novaVariedadeId }) + } + } + }) +} From 0eab0eaeef131712692f829a9c490a184d9a2b13 Mon Sep 17 00:00:00 2001 From: Lucas Dos Santos Vaz <52181258+luscas18@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:03:26 -0300 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20adiciona=20campos=20faltantes=20no?= =?UTF-8?q?=20relat=C3=B3rio=20de=20locais=20de=20coleta=20(#419)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/relatorios-controller.js | 36 ++++++++++++++++++--- src/helpers/formata-dados-relatorio.js | 9 +++--- src/reports/templates/LocaisColeta.tsx | 41 +++++++++++++++++++++--- 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/controllers/relatorios-controller.js b/src/controllers/relatorios-controller.js index ee4cc3b..d252717 100644 --- a/src/controllers/relatorios-controller.js +++ b/src/controllers/relatorios-controller.js @@ -40,6 +40,8 @@ const { Estado, Pais, TomboFoto, + Variedade, + Subespecie, } = models; /// ////// Relatório de Inventário de Espécies ////////// @@ -291,7 +293,7 @@ export const obtemDadosDoRelatorioDeColetaIntervaloDeData = async (req, res, nex { [Op.between]: [ Sequelize.literal(`TO_DATE('${dataInicio.slice(0, 10)}', 'YYYY-MM-DD')`), - Sequelize.literal(`TO_DATE('${(dataFim || new Date().toISOString().slice(0, 10))}', 'YYYY-MM-DD')`), + Sequelize.literal(`TO_DATE('${(dataFim ? dataFim.slice(0, 10) : new Date().toISOString().slice(0, 10))}', 'YYYY-MM-DD')`), ], }, ), @@ -403,7 +405,7 @@ export const obtemDadosDoRelatorioDeColetaPorColetorEIntervaloDeData = async (re { [Op.between]: [ Sequelize.literal(`TO_DATE('${dataInicio.slice(0, 10)}', 'YYYY-MM-DD')`), - Sequelize.literal(`TO_DATE('${(dataFim || new Date().toISOString().slice(0, 10))}', 'YYYY-MM-DD')`), + Sequelize.literal(`TO_DATE('${(dataFim ? dataFim.slice(0, 10) : new Date().toISOString().slice(0, 10))}', 'YYYY-MM-DD')`), ], }, ), @@ -530,7 +532,7 @@ export const obtemDadosDoRelatorioDeLocalDeColeta = async (req, res, next) => { { [Op.between]: [ Sequelize.literal(`TO_DATE('${dataInicio.slice(0, 10)}', 'YYYY-MM-DD')`), - Sequelize.literal(`TO_DATE('${(dataFim || new Date().toISOString().slice(0, 10))}', 'YYYY-MM-DD')`), + Sequelize.literal(`TO_DATE('${(dataFim ? dataFim.slice(0, 10) : new Date().toISOString().slice(0, 10))}', 'YYYY-MM-DD')`), ], }, ), @@ -546,6 +548,8 @@ export const obtemDadosDoRelatorioDeLocalDeColeta = async (req, res, next) => { 'familia_id', 'especie_id', 'genero_id', + 'variedade_id', + 'sub_especie_id', 'nome_cientifico', 'data_coleta_ano', 'data_coleta_mes', @@ -576,6 +580,28 @@ export const obtemDadosDoRelatorioDeLocalDeColeta = async (req, res, next) => { }, ], }, + { + model: Variedade, + attributes: ['id', 'nome'], + include: [ + { + model: Autor, + attributes: ['id', 'nome'], + as: 'autor', + }, + ], + }, + { + model: Subespecie, + attributes: ['id', 'nome'], + include: [ + { + model: Autor, + attributes: ['id', 'nome'], + as: 'autor', + }, + ], + }, { model: LocalColeta, attributes: ['id', 'descricao'], @@ -748,7 +774,7 @@ export const obtemDadosDoRelatorioDeCodigoDeBarras = async (req, res, next) => { { [Op.between]: [ Sequelize.literal(`TO_DATE('${dataInicio.slice(0, 10)}', 'YYYY-MM-DD')`), - Sequelize.literal(`TO_DATE('${(dataFim || new Date().toISOString().slice(0, 10))}', 'YYYY-MM-DD')`), + Sequelize.literal(`TO_DATE('${(dataFim ? dataFim.slice(0, 10) : new Date().toISOString().slice(0, 10))}', 'YYYY-MM-DD')`), ], }, ), @@ -815,7 +841,7 @@ export const obtemDadosDoRelatorioDeQuantidade = async (req, res, next) => { { [Op.between]: [ Sequelize.literal(`TO_DATE('${dataInicio.slice(0, 10)}', 'YYYY-MM-DD')`), - Sequelize.literal(`TO_DATE('${(dataFim || new Date().toISOString().slice(0, 10))}', 'YYYY-MM-DD')`), + Sequelize.literal(`TO_DATE('${(dataFim ? dataFim.slice(0, 10) : new Date().toISOString().slice(0, 10))}', 'YYYY-MM-DD')`), ], }, ), diff --git a/src/helpers/formata-dados-relatorio.js b/src/helpers/formata-dados-relatorio.js index ed9e7e8..a43e04d 100644 --- a/src/helpers/formata-dados-relatorio.js +++ b/src/helpers/formata-dados-relatorio.js @@ -246,13 +246,13 @@ export function agruparPorLocal(dados) { let quantidadeTotal = 0; dados.sort((a, b) => { - const familia = a?.familia?.nome.localeCompare(b?.familia?.nome); + const familia = (a?.familia?.nome || '').localeCompare(b?.familia?.nome || ''); if (familia !== 0) return familia; - const genero = a?.genero?.nome.localeCompare(b?.genero?.nome); + const genero = (a?.genero?.nome || '').localeCompare(b?.genero?.nome || ''); if (genero !== 0) return genero; - return a?.especy?.nome.localeCompare(b?.especy?.nome); + return (a?.especy?.nome || '').localeCompare(b?.especy?.nome || ''); }).forEach(entradaOriginal => { const locaisColetum = entradaOriginal.locais_coletum; const cidade = locaisColetum?.cidade; @@ -267,7 +267,8 @@ export function agruparPorLocal(dados) { ...entradaOriginal, latitude: entradaOriginal?.latitude || null, longitude: entradaOriginal?.longitude || null, - autor: entradaOriginal.especy?.autor?.nome || '', + variedade: entradaOriginal.variedade || null, + sub_especie: entradaOriginal.sub_especy || null, }; if (!agrupado[chave]) { diff --git a/src/reports/templates/LocaisColeta.tsx b/src/reports/templates/LocaisColeta.tsx index 7ed88c5..2f73ed8 100644 --- a/src/reports/templates/LocaisColeta.tsx +++ b/src/reports/templates/LocaisColeta.tsx @@ -8,6 +8,7 @@ interface Registro { data_coleta_dia: number; especy: { nome: string; + autor?: { nome: string } | null; genero: { nome: string; } @@ -21,9 +22,16 @@ interface Registro { genero: { nome: string; } + variedade?: { + nome: string; + autor?: { nome: string } | null; + } | null; + sub_especie?: { + nome: string; + autor?: { nome: string } | null; + } | null; latitude: number | null; longitude: number | null; - autor?: string; } interface LocaisColeta { @@ -103,6 +111,30 @@ function RelacaoLocaisColeta({ dados, total, textoFiltro, showCoord = false }: R }; } + const renderNomeCientifico = (item: Registro) => { + const { especy, genero } = item; + return ( + <> + {genero?.nome} {especy?.nome} + {especy?.autor?.nome && ` ${especy.autor.nome}`} + {item.sub_especie && ( + <> + {' subsp. '} + {item.sub_especie.nome} + {item.sub_especie.autor?.nome && ` ${item.sub_especie.autor.nome}`} + + )} + {item.variedade && ( + <> + {' var. '} + {item.variedade.nome} + {item.variedade.autor?.nome && ` ${item.variedade.autor.nome}`} + + )} + + ); + } + const renderTable = (registros: Registro[]) => { return ( @@ -110,8 +142,7 @@ function RelacaoLocaisColeta({ dados, total, textoFiltro, showCoord = false }: R - - {/* */} + {showCoord && } {showCoord && } @@ -119,13 +150,13 @@ function RelacaoLocaisColeta({ dados, total, textoFiltro, showCoord = false }: R {registros.map((item, i) => { - const { especy, familia, genero } = item; + const { familia } = item; const cordenadas = obtemCordenadas(item); return ( - + {showCoord && } {showCoord && } From 56ad375e1eb15da3b66590a37111e58a6594f362 Mon Sep 17 00:00:00 2001 From: Lucas Dos Santos Vaz <52181258+luscas18@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:09:24 -0300 Subject: [PATCH 5/6] 401 coletores duplicados (#420) --- .../20260429003533_coletores-duplicados.ts | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/database/migration/20260429003533_coletores-duplicados.ts diff --git a/src/database/migration/20260429003533_coletores-duplicados.ts b/src/database/migration/20260429003533_coletores-duplicados.ts new file mode 100644 index 0000000..c431451 --- /dev/null +++ b/src/database/migration/20260429003533_coletores-duplicados.ts @@ -0,0 +1,92 @@ +import { Knex } from 'knex' + +type DupGroupRow = { + nome_norm: string + keep_id: number + ids: number[] + qtde: number +} + +export async function run(knex: Knex): Promise { + await knex.transaction(async trx => { + // Agrupa por nome normalizado; prefere o registro com e-mail, depois o menor id + const dupGroups = (await trx('coletores') + .select([ + trx.raw('TRIM(LOWER(nome)) as nome_norm'), + trx.raw(`COALESCE( + MIN(id) FILTER (WHERE email IS NOT NULL AND email <> ''), + MIN(id) + )::int as keep_id`), + trx.raw('ARRAY_AGG(id ORDER BY id) as ids'), + trx.raw('COUNT(*)::int as qtde') + ]) + .groupByRaw('TRIM(LOWER(nome))') + .havingRaw('COUNT(*) > 1')) as unknown as DupGroupRow[] + + if (!dupGroups.length) return + + const pairs: Array<{ keep_id: number; drop_id: number }> = [] + + for (const g of dupGroups) { + const keepId = Number(g.keep_id) + const ids = (g.ids ?? []).map(n => Number(n)).filter(Number.isFinite) + + for (const id of ids) { + if (id !== keepId) pairs.push({ keep_id: keepId, drop_id: id }) + } + } + + if (!pairs.length) return + + await trx.raw('DROP TABLE IF EXISTS coletor_merge') + + await trx.schema.createTable('coletor_merge', table => { + table.integer('keep_id').notNullable() + table.integer('drop_id').notNullable().primary() + }) + + await trx('coletor_merge').insert(pairs) + + // Atualiza tombos.coletor_id + await trx.raw(` + UPDATE tombos t + SET coletor_id = m.keep_id + FROM coletor_merge m + WHERE t.coletor_id = m.drop_id + `) + + // tombos_coletores tem PK composta (tombo_hcf, coletor_id) — só opera se a tabela existir + const hasTombosColetores = await trx.schema.hasTable('tombos_coletores') + + if (hasTombosColetores) { + // Remove linhas que já existem com keep_id para o mesmo tombo (evita conflito de PK) + await trx.raw(` + DELETE FROM tombos_coletores tc + USING coletor_merge m + WHERE tc.coletor_id = m.drop_id + AND EXISTS ( + SELECT 1 FROM tombos_coletores tc2 + WHERE tc2.tombo_hcf = tc.tombo_hcf + AND tc2.coletor_id = m.keep_id + ) + `) + + // Atualiza as linhas restantes em tombos_coletores + await trx.raw(` + UPDATE tombos_coletores tc + SET coletor_id = m.keep_id + FROM coletor_merge m + WHERE tc.coletor_id = m.drop_id + `) + } + + // Remove os coletores duplicados + await trx.raw(` + DELETE FROM coletores c + USING coletor_merge m + WHERE c.id = m.drop_id + `) + + await trx.schema.dropTable('coletor_merge') + }) +} From 9bd0828ff517eefb3db946748fe0c99193fda0b2 Mon Sep 17 00:00:00 2001 From: Lucas Dos Santos Vaz <52181258+luscas18@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:11:53 -0300 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20migra=C3=A7=C3=A3o=20para=20normaliz?= =?UTF-8?q?a=C3=A7=C3=A3o=20da=20tabela=20de=20subespecies=20(#399)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...06235328_corrije-duplicatas-subespecies.ts | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/database/migration/20260406235328_corrije-duplicatas-subespecies.ts diff --git a/src/database/migration/20260406235328_corrije-duplicatas-subespecies.ts b/src/database/migration/20260406235328_corrije-duplicatas-subespecies.ts new file mode 100644 index 0000000..1e82acf --- /dev/null +++ b/src/database/migration/20260406235328_corrije-duplicatas-subespecies.ts @@ -0,0 +1,136 @@ +import { Knex } from 'knex' + +function normalize(str: string | null | undefined) { + if (!str) return '' + return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim() +} + +export async function run(knex: Knex): Promise { + await knex.transaction(async trx => { + // 1. BUSCA OS DADOS CONFORME ESTÃO NO TOMBO + const tombos = await trx('tombos as t') + .join('familias as f', 't.familia_id', 'f.id') + .join('generos as g', 't.genero_id', 'g.id') + .join('especies as e', 't.especie_id', 'e.id') + .join('sub_especies as sub', 't.sub_especie_id', 'sub.id') + .select( + 't.hcf as tombo_id', + 't.familia_id', + 't.genero_id', + 't.especie_id', // Esta é a espécie que manda na reconstrução + 'sub.nome as sub_nome', + 'sub.autor_id as sub_autor_id', + 'f.nome as familia_nome', + 'g.nome as genero_nome', + 'e.nome as especie_nome' + ) + .whereNotNull('t.familia_id') + .whereNotNull('t.genero_id') + .whereNotNull('t.especie_id') + .whereNotNull('sub.nome') + .where('sub.nome', '<>', '') + .whereRaw('TRIM(sub.nome) <> \'\'') + + type NovaSubespecie = { + key: string + nome: string + familia_id: number + genero_id: number + especie_id: number + autor_id: number | null + } + + const uniqueSubespecies = new Map() + const tombosToUpdate = new Map() + + for (const row of tombos) { + const key = [ + normalize(row.sub_nome), + normalize(row.familia_nome), + normalize(row.genero_nome), + normalize(row.especie_nome), + row.sub_autor_id + ].join('|') + + if (!uniqueSubespecies.has(key)) { + uniqueSubespecies.set(key, { + key, + nome: row.sub_nome, + familia_id: row.familia_id, + genero_id: row.genero_id, + especie_id: row.especie_id, // Vincula à espécie definida no tombo + autor_id: row.sub_autor_id + }) + } + + if (!tombosToUpdate.has(key)) { + tombosToUpdate.set(key, []) + } + tombosToUpdate.get(key)!.push(row.tombo_id) + } + + const chunkSize = 1000 + + // 2. RESET E RECONSTRUÇÃO TOTAL + // Desconecta todos os tombos antes de apagar a tabela antiga + await trx('tombos') + .whereNotNull('sub_especie_id') + .update({ sub_especie_id: null }) + + // Limpa a tabela antiga + await trx('sub_especies').del() + + if (uniqueSubespecies.size > 0) { + const itemsToInsert = Array.from(uniqueSubespecies.values()).map(sub => ({ + nome: sub.nome, + familia_id: sub.familia_id, + genero_id: sub.genero_id, + especie_id: sub.especie_id, + autor_id: sub.autor_id + })) + + for (let i = 0; i < itemsToInsert.length; i += chunkSize) { + const chunk = itemsToInsert.slice(i, i + chunkSize) + await trx('sub_especies').insert(chunk) + } + + // 3. REATRIBUIÇÃO DOS NOVOS IDS + const newSubEspecies = await trx('sub_especies as sub') + .join('familias as f', 'sub.familia_id', 'f.id') + .join('generos as g', 'sub.genero_id', 'g.id') + .join('especies as e', 'sub.especie_id', 'e.id') + .select( + 'sub.id', + 'sub.nome as sub_nome', + 'sub.autor_id as sub_autor_id', + 'f.nome as familia_nome', + 'g.nome as genero_nome', + 'e.nome as especie_nome' + ) + + const newIdMap = new Map() + for (const row of newSubEspecies) { + const key = [ + normalize(row.sub_nome), + normalize(row.familia_nome), + normalize(row.genero_nome), + normalize(row.especie_nome), + row.sub_autor_id + ].join('|') + newIdMap.set(key, row.id) + } + + for (const [key, tomboIds] of tombosToUpdate.entries()) { + const newId = newIdMap.get(key) + if (newId !== undefined && tomboIds.length > 0) { + for (let i = 0; i < tomboIds.length; i += chunkSize) { + const chunk = tomboIds.slice(i, i + chunkSize) + await trx('tombos') + .whereIn('hcf', chunk) + .update({ sub_especie_id: newId }) + } + } + } + } + }) +}
Data Coleta FamíliaEspécieAutorNome CientíficoLatitudeLongitudeNº do Tombo
{criaData(item)} {familia?.nome}
{genero?.nome} {especy?.nome}
{item.autor}
{renderNomeCientifico(item)}{cordenadas.latitude}{cordenadas.longitude}{item.hcf}