Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
371 changes: 371 additions & 0 deletions src/controllers/rfids-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,371 @@
import { Op, Sequelize } from 'sequelize';

Check warning on line 1 in src/controllers/rfids-controller.js

View workflow job for this annotation

GitHub Actions / lint

There should be at least one empty line between import groups
import models from '../models/index.js';

const { Rfid, TomboFoto, Tombo, Especie, Coletor } = models;

const getIncludedTomboFoto = rfidJson => (
rfidJson.TomboFoto || rfidJson.tombos_foto || rfidJson.tombo_foto || rfidJson.TomboFotos
);

const getIncludedTombo = tomboFotoJson => (
tomboFotoJson?.Tombo || tomboFotoJson?.tombo
);

const getNomeCientifico = tombo => (
tombo?.especie?.nome || tombo?.Especie?.nome || tombo?.nome_cientifico || 'N/A'
);

const getColetorPrincipal = tombo => (
tombo?.coletor?.nome || tombo?.Coletor?.nome || 'N/A'
);

const codificarParaEpcHex = (tomboHcf, codigoBarra) => {
const tomboId = parseInt(tomboHcf, 10);
const match = codigoBarra.match(/([a-zA-Z]+)(\d+)/);

Check failure on line 25 in src/controllers/rfids-controller.js

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
if (!match) {
throw new Error('Formato de código de barras inválido para gravação RFID.');
}

Check failure on line 29 in src/controllers/rfids-controller.js

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
const herbario = match[1];

Check failure on line 30 in src/controllers/rfids-controller.js

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
const numero = parseInt(match[2], 10);

Check failure on line 31 in src/controllers/rfids-controller.js

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed

// ID Tombo (4 Bytes = 8 chars hex)
const bloco1 = tomboId.toString(16).padStart(8, '0').toUpperCase();

// Sigla herbario (4 Bytes = 8 chars hex)
const prefixo = `_${herbario}`.substring(0, 4).padEnd(4, ' ');

Check failure on line 37 in src/controllers/rfids-controller.js

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
let bloco2 = '';

for(let i = 0; i < 4; i++) {

Check failure on line 40 in src/controllers/rfids-controller.js

View workflow job for this annotation

GitHub Actions / lint

Expected space(s) after "for"
bloco2 += prefixo.charCodeAt(i).toString(16).padStart(2, '0').toUpperCase();
}

// Numero cog de barras (4 Bytes = 8 chars hex)
const bloco3 = numero.toString(16).padStart(8, '0').toUpperCase();

return bloco1 + bloco2 + bloco3;

Check failure on line 47 in src/controllers/rfids-controller.js

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
};

const decodificarEpcHex = (epcHex) => {

Check failure on line 50 in src/controllers/rfids-controller.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected parentheses around single function argument
if (!epcHex || epcHex.length !== 24) return epcHex;

Check failure on line 51 in src/controllers/rfids-controller.js

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed

try {
const hexBloco1 = epcHex.substring(0, 8);
const hexBloco2 = epcHex.substring(8, 16);
const hexBloco3 = epcHex.substring(16, 24);
const tomboId = parseInt(hexBloco1, 16);
let prefixo = '';

for(let i = 0; i < 8; i += 2) {

Check failure on line 60 in src/controllers/rfids-controller.js

View workflow job for this annotation

GitHub Actions / lint

Expected space(s) after "for"
prefixo += String.fromCharCode(parseInt(hexBloco2.substring(i, i + 2), 16));
}

prefixo = prefixo.trim();

const numero = parseInt(hexBloco3, 16);
const numeroFormatado = String(numero).padStart(9, '0');

return `${tomboId}${prefixo}${numeroFormatado}`;
} catch (e) {
return epcHex;
}
};

export const iniciarGravacao = async (request, response, next) => {
const { tombo_foto_id } = request.body;

try {
const foto = await TomboFoto.findByPk(tombo_foto_id);
if (!foto) return response.status(404).json({ erro: 'Foto não encontrada.' });

let epcHexParaGravar;

try {
epcHexParaGravar = codificarParaEpcHex(foto.tombo_hcf, foto.codigo_barra);
} catch (err) {
return response.status(400).json({ erro: err.message });
}

let rfid = await Rfid.findOne({ where: { tombo_foto_id } });

if (rfid) {
if (rfid.status === 'CONCLUIDO') {
return response.status(409).json({
erro: 'Tombo já possui uma etiqueta RFID vinculada e concluída.'
});
}

rfid.epc = epcHexParaGravar;
rfid.status = 'PENDENTE';
await rfid.save();

} else {
rfid = await Rfid.create({
tombo_foto_id,
epc: epcHexParaGravar,
status: 'PENDENTE'
});
}

return response.status(201).json({
mensagem: 'Operação iniciada.',
rfid: {
id: rfid.id,
epc: rfid.epc,
status: rfid.status
}
});

} catch (error) {
next(error);
}
};

export const finalizarGravacao = async (request, response, next) => {
const { id } = request.params;
const { status, tid } = request.body;

try {
const rfid = await Rfid.findByPk(id);
if (!rfid) return response.status(404).json({ erro: 'Tag não encontrada.' });

if (rfid.status === 'CONCLUIDO') {
return response.status(409).json({
erro: 'Ttag já registrada como CONCLUIDO e não pode ser modificada.'
});
}

const statusFinal = status || '';

switch (statusFinal) {
case 'FALHA':
rfid.tid = null;
break;

case 'CONCLUIDO':
if (tid) {
const tagExistente = await Rfid.findOne({ where: { tid } });

if (tagExistente && tagExistente.id !== Number(id)) {
return response.status(409).json({
erro: 'TID já se encontra vinculado a outro tombo no sistema.'
});
}

rfid.tid = tid;
}
break;

default:
return response.status(400).json({
erro: `Operação negada. Status inválido: '${statusFinal}'.`
});
}

rfid.status = statusFinal;
await rfid.save();

return response.status(200).json({
mensagem: 'Operação finalizada.',
rfid
});

} catch (error) {
next(error);
}
};

export const listagem = async (request, response, next) => {
try {
const limite = parseInt(request.query.limite) || 20;
const pagina = parseInt(request.query.pagina) || 1;
const offset = (pagina - 1) * limite;

const { tombo_hcf, epc, codigo_barra, status } = request.query;
const whereRfid = {};

if (epc) {
whereRfid.epc = { [Op.iLike]: `%${epc}%` };
}

if (status) {
whereRfid.status = status;
}

const whereFoto = {};

if (tombo_hcf) {
whereFoto.tombo_hcf = tombo_hcf;
}

if (codigo_barra) {
whereFoto.codigo_barra = codigo_barra;
}

const rfids = await Rfid.findAndCountAll({
limit: limite,
offset,
where: whereRfid,
include: [
{
model: TomboFoto,
attributes: ['id', 'tombo_hcf', 'codigo_barra', 'caminho_foto'],
where: Object.keys(whereFoto).length > 0 ? whereFoto : undefined,
required: Object.keys(whereFoto).length > 0
}
],
order: [['created_at', 'DESC']]
});

return response.status(200).json({
dados: rfids.rows,
meta: {
total: rfids.count,
pagina,
limite
},
});

} catch (error) {
next(error);
}
};

export const listarPendentesRfid = async (request, response, next) => {

try {
const limite = parseInt(request.query.limite) || 10;
const pagina = parseInt(request.query.pagina) || 1;
const offset = (pagina - 1) * limite;
const { q } = request.query;

const whereCondicao = {
id: {
[Op.notIn]: Sequelize.literal(`(SELECT tombo_foto_id FROM rfids WHERE status = 'CONCLUIDO' AND tombo_foto_id IS NOT NULL)`)
}
};

if (q) {
whereCondicao[Op.or] = [
Sequelize.where(Sequelize.cast(Sequelize.col('tombo_hcf'), 'varchar'), { [Op.iLike]: `%${q}%` }),
Sequelize.where(Sequelize.cast(Sequelize.col('codigo_barra'), 'varchar'), { [Op.iLike]: `%${q}%` })
];
}

const tombosPendentes = await TomboFoto.findAndCountAll({
attributes: ['id', 'tombo_hcf', 'codigo_barra', 'caminho_foto'],
where: whereCondicao,
include: [
{
model: Tombo,
attributes: ['hcf', 'nome_cientifico'],
required: false,
include: [
{
model: Especie,
as: 'especie',
attributes: ['nome'],
required: false,
},
{
model: Coletor,
as: 'coletor',
attributes: ['nome'],
required: false,
},
],
},
],
limit: limite,
offset: offset,
order: [['id', 'DESC']]
});

const dados = tombosPendentes.rows.map(tomboFoto => {
const item = tomboFoto.toJSON();
const tombo = getIncludedTombo(item);

return {
id: item.id,
tombo_hcf: item.tombo_hcf,
codigo_barra: item.codigo_barra,
caminho_foto: item.caminho_foto,
nome_cientifico: getNomeCientifico(tombo),
coletor_principal: getColetorPrincipal(tombo),
};
});

return response.status(200).json({
dados,
meta: {
total: tombosPendentes.count,
pagina,
limite
}
});

} catch (error) {
next(error);
}
};

export const validarEpc = async (request, response, next) => {
const { epc } = request.params;

try {
const rfid = await Rfid.findOne({
where: { epc: epc },
include: [
{
model: TomboFoto,
include: [
{
model: Tombo,
include: [
{
model: Especie,
as: 'especie',
attributes: ['nome']
},
{
model: Coletor,
as: 'coletor',
attributes: ['nome']
}
]
}
]
}
]
});

if (!rfid) {
return response.status(404).json({
valido: false,
mensagem: 'EPC não encontrado.'
});
}

const rfidJson = rfid.toJSON();
const tomboFoto = getIncludedTomboFoto(rfidJson);
const tombo = getIncludedTombo(tomboFoto);

return response.status(200).json({
valido: true,
mensagem: 'EPC validado com sucesso.',
dados: {
id_rfid: rfidJson.id,
epc_formatado: rfidJson.epc,
epc: decodificarEpcHex(rfidJson.epc),
status_rfid: rfidJson.status,
tombo_hcf: tombo?.hcf || tomboFoto?.tombo_hcf || 'N/A',
nome_cientifico: getNomeCientifico(tombo),
coletor_principal: getColetorPrincipal(tombo)
}
});

} catch (error) {
next(error);
}
};
26 changes: 26 additions & 0 deletions src/database/migration/20260510165542_criacao_tabela_rfids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Knex } from 'knex'

export async function run(knex: Knex): Promise<void> {
await knex.transaction(async trx => {

await trx.schema.dropTableIfExists('rfids')

await trx.schema.createTable('rfids', table => {
table.increments('id').primary()

table.bigInteger('tombo_foto_id').notNullable()
table.foreign('tombo_foto_id')
.references('id')
.inTable('tombos_fotos')
.onDelete('CASCADE')
.onUpdate('CASCADE')

table.string('epc', 96).notNullable().unique()
table.string('tid', 96).nullable().unique()

table.string('status', 20).notNullable().defaultTo('PENDENTE')
table.timestamp('created_at').defaultTo(trx.fn.now())
table.timestamp('updated_at').defaultTo(trx.fn.now())
})
})
}
Loading
Loading