feat: integrate barcode scanning functionality in 'Almoxarifado' for improved product search and registration, along with image upload support for enhanced inventory management
This commit is contained in:
118
packages/backend/convex/actions/buscarInfoProduto.ts
Normal file
118
packages/backend/convex/actions/buscarInfoProduto.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { action } from '../_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
|
||||
interface OpenFoodFactsProduct {
|
||||
product?: {
|
||||
product_name?: string;
|
||||
product_name_pt?: string;
|
||||
generic_name?: string;
|
||||
generic_name_pt?: string;
|
||||
categories?: string;
|
||||
categories_tags?: string[];
|
||||
image_url?: string;
|
||||
image_front_url?: string;
|
||||
image_front_small_url?: string;
|
||||
brands?: string;
|
||||
quantity?: string;
|
||||
packaging?: string;
|
||||
};
|
||||
status?: number;
|
||||
status_verbose?: string;
|
||||
}
|
||||
|
||||
interface ProductInfo {
|
||||
nome?: string;
|
||||
descricao?: string;
|
||||
categoria?: string;
|
||||
imagemUrl?: string;
|
||||
marca?: string;
|
||||
quantidade?: string;
|
||||
embalagem?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca informações de produto via API externa (Open Food Facts)
|
||||
* Esta é uma funcionalidade opcional que pode ser usada para preencher
|
||||
* automaticamente informações de produtos quando disponível.
|
||||
*/
|
||||
export const buscarInfoProdutoPorCodigoBarras = action({
|
||||
args: {
|
||||
codigoBarras: v.string()
|
||||
},
|
||||
handler: async (ctx, args): Promise<ProductInfo | null> => {
|
||||
const { codigoBarras } = args;
|
||||
|
||||
// Validar formato básico de código de barras (EAN-13, UPC, etc.)
|
||||
if (!codigoBarras || codigoBarras.length < 8 || codigoBarras.length > 14) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Tentar buscar na API Open Food Facts (gratuita, sem autenticação)
|
||||
const response = await fetch(
|
||||
`https://world.openfoodfacts.org/api/v0/product/${codigoBarras}.json`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'SGSE-App/1.0 (Almoxarifado)'
|
||||
},
|
||||
signal: AbortSignal.timeout(5000) // Timeout de 5 segundos
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as OpenFoodFactsProduct;
|
||||
|
||||
if (data.status !== 1 || !data.product) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const product = data.product;
|
||||
|
||||
// Extrair categoria (primeira categoria disponível)
|
||||
let categoria: string | undefined;
|
||||
if (product.categories_tags && product.categories_tags.length > 0) {
|
||||
// Pegar a primeira categoria e limpar tags
|
||||
const primeiraCategoria = product.categories_tags[0];
|
||||
categoria = primeiraCategoria
|
||||
.replace(/^pt:/, '')
|
||||
.replace(/^en:/, '')
|
||||
.replace(/-/g, ' ')
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
} else if (product.categories) {
|
||||
categoria = product.categories.split(',')[0].trim();
|
||||
}
|
||||
|
||||
const info: ProductInfo = {
|
||||
nome: product.product_name_pt || product.product_name || undefined,
|
||||
descricao: product.generic_name_pt || product.generic_name || undefined,
|
||||
categoria,
|
||||
imagemUrl:
|
||||
product.image_front_url ||
|
||||
product.image_url ||
|
||||
product.image_front_small_url ||
|
||||
undefined,
|
||||
marca: product.brands || undefined,
|
||||
quantidade: product.quantity || undefined,
|
||||
embalagem: product.packaging || undefined
|
||||
};
|
||||
|
||||
// Retornar apenas se tiver pelo menos nome ou descrição
|
||||
if (info.nome || info.descricao) {
|
||||
return info;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
// Log do erro mas não falhar a operação
|
||||
console.error('Erro ao buscar informações do produto:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -55,7 +55,8 @@ export const listarMateriais = query({
|
||||
materiais = materiais.filter(
|
||||
(m) =>
|
||||
m.codigo.toLowerCase().includes(buscaLower) ||
|
||||
m.nome.toLowerCase().includes(buscaLower)
|
||||
m.nome.toLowerCase().includes(buscaLower) ||
|
||||
(m.codigoBarras && m.codigoBarras.toLowerCase().includes(buscaLower))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,6 +82,30 @@ export const obterMaterial = query({
|
||||
}
|
||||
});
|
||||
|
||||
export const buscarMaterialPorCodigoBarras = query({
|
||||
args: { codigoBarras: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) return null;
|
||||
|
||||
try {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: 'almoxarifado',
|
||||
acao: 'listar'
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const material = await ctx.db
|
||||
.query('materiais')
|
||||
.withIndex('by_codigoBarras', (q) => q.eq('codigoBarras', args.codigoBarras))
|
||||
.first();
|
||||
|
||||
return material ?? null;
|
||||
}
|
||||
});
|
||||
|
||||
export const listarMovimentacoes = query({
|
||||
args: {
|
||||
materialId: v.optional(v.id('materiais')),
|
||||
@@ -595,7 +620,10 @@ export const criarMaterial = mutation({
|
||||
estoqueMaximo: v.optional(v.number()),
|
||||
estoqueAtual: v.optional(v.number()),
|
||||
localizacao: v.optional(v.string()),
|
||||
fornecedor: v.optional(v.string())
|
||||
fornecedor: v.optional(v.string()),
|
||||
codigoBarras: v.optional(v.string()),
|
||||
imagemUrl: v.optional(v.string()),
|
||||
imagemBase64: v.optional(v.string())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
@@ -613,6 +641,18 @@ export const criarMaterial = mutation({
|
||||
throw new Error('Código do material já existe');
|
||||
}
|
||||
|
||||
// Verificar se código de barras já existe (se fornecido)
|
||||
if (args.codigoBarras) {
|
||||
const codigoBarrasExistente = await ctx.db
|
||||
.query('materiais')
|
||||
.withIndex('by_codigoBarras', (q) => q.eq('codigoBarras', args.codigoBarras))
|
||||
.first();
|
||||
|
||||
if (codigoBarrasExistente) {
|
||||
throw new Error('Código de barras já está cadastrado para outro material');
|
||||
}
|
||||
}
|
||||
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) throw new Error('Usuário não autenticado');
|
||||
|
||||
@@ -650,6 +690,9 @@ export const editarMaterial = mutation({
|
||||
estoqueMaximo: v.optional(v.number()),
|
||||
localizacao: v.optional(v.string()),
|
||||
fornecedor: v.optional(v.string()),
|
||||
codigoBarras: v.optional(v.string()),
|
||||
imagemUrl: v.optional(v.string()),
|
||||
imagemBase64: v.optional(v.string()),
|
||||
ativo: v.optional(v.boolean())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
@@ -673,6 +716,18 @@ export const editarMaterial = mutation({
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar se código de barras já existe (se foi alterado)
|
||||
if (args.codigoBarras && args.codigoBarras !== material.codigoBarras) {
|
||||
const codigoBarrasExistente = await ctx.db
|
||||
.query('materiais')
|
||||
.withIndex('by_codigoBarras', (q) => q.eq('codigoBarras', args.codigoBarras))
|
||||
.first();
|
||||
|
||||
if (codigoBarrasExistente) {
|
||||
throw new Error('Código de barras já está cadastrado para outro material');
|
||||
}
|
||||
}
|
||||
|
||||
const dadosAnteriores = { ...material };
|
||||
const dadosNovos: Partial<Doc<'materiais'>> & { atualizadoEm: number } = {
|
||||
atualizadoEm: Date.now()
|
||||
@@ -688,6 +743,9 @@ export const editarMaterial = mutation({
|
||||
if (args.estoqueMaximo !== undefined) dadosNovos.estoqueMaximo = args.estoqueMaximo;
|
||||
if (args.localizacao !== undefined) dadosNovos.localizacao = args.localizacao;
|
||||
if (args.fornecedor !== undefined) dadosNovos.fornecedor = args.fornecedor;
|
||||
if (args.codigoBarras !== undefined) dadosNovos.codigoBarras = args.codigoBarras;
|
||||
if (args.imagemUrl !== undefined) dadosNovos.imagemUrl = args.imagemUrl;
|
||||
if (args.imagemBase64 !== undefined) dadosNovos.imagemBase64 = args.imagemBase64;
|
||||
if (args.ativo !== undefined) dadosNovos.ativo = args.ativo;
|
||||
|
||||
await ctx.db.patch(args.id, dadosNovos);
|
||||
|
||||
@@ -44,6 +44,9 @@ export const almoxarifadoTables = {
|
||||
estoqueAtual: v.number(),
|
||||
localizacao: v.optional(v.string()),
|
||||
fornecedor: v.optional(v.string()),
|
||||
codigoBarras: v.optional(v.string()),
|
||||
imagemUrl: v.optional(v.string()),
|
||||
imagemBase64: v.optional(v.string()),
|
||||
ativo: v.boolean(),
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
@@ -52,7 +55,8 @@ export const almoxarifadoTables = {
|
||||
.index('by_codigo', ['codigo'])
|
||||
.index('by_categoria', ['categoria'])
|
||||
.index('by_ativo', ['ativo'])
|
||||
.index('by_estoqueAtual', ['estoqueAtual']),
|
||||
.index('by_estoqueAtual', ['estoqueAtual'])
|
||||
.index('by_codigoBarras', ['codigoBarras']),
|
||||
|
||||
movimentacoesEstoque: defineTable({
|
||||
materialId: v.id('materiais'),
|
||||
|
||||
Reference in New Issue
Block a user