feat: enhance 'Almoxarifado' functionality by integrating barcode scanning for material entry and exit, improving user experience with loading indicators and error handling for better inventory management

This commit is contained in:
2025-12-22 10:52:46 -03:00
parent e19c24b9ab
commit b1db926ab4
8 changed files with 1125 additions and 287 deletions

View File

@@ -10,6 +10,7 @@
import type * as acoes from "../acoes.js";
import type * as actions_buscarInfoProduto from "../actions/buscarInfoProduto.js";
import type * as actions_downloadImage from "../actions/downloadImage.js";
import type * as actions_email from "../actions/email.js";
import type * as actions_linkPreview from "../actions/linkPreview.js";
import type * as actions_pushNotifications from "../actions/pushNotifications.js";
@@ -109,6 +110,7 @@ import type {
declare const fullApi: ApiFromModules<{
acoes: typeof acoes;
"actions/buscarInfoProduto": typeof actions_buscarInfoProduto;
"actions/downloadImage": typeof actions_downloadImage;
"actions/email": typeof actions_email;
"actions/linkPreview": typeof actions_linkPreview;
"actions/pushNotifications": typeof actions_pushNotifications;

View File

@@ -0,0 +1,137 @@
'use node';
import { action } from '../_generated/server';
import { v } from 'convex/values';
/**
* Baixa uma imagem de uma URL externa e converte para base64.
*
* Esta action roda no servidor (Node.js), então não tem restrições de CORS
* do navegador. Pode baixar imagens de qualquer domínio.
*
* @param url - URL da imagem a ser baixada
* @returns String base64 da imagem (data URL) ou null se falhar
*/
export const downloadImageAsBase64 = action({
args: {
url: v.string()
},
returns: v.union(v.string(), v.null()),
handler: async (ctx, args): Promise<string | null> => {
const { url } = args;
try {
// Validar URL
let urlObj: URL;
try {
urlObj = new URL(url);
} catch {
console.error('URL inválida:', url);
return null;
}
// Verificar se é uma URL HTTP/HTTPS
if (!['http:', 'https:'].includes(urlObj.protocol)) {
console.error('Protocolo não suportado:', urlObj.protocol);
return null;
}
// Baixar a imagem (server-side não tem CORS)
// Tentar múltiplas estratégias para evitar bloqueios (403) de CDNs
type HeadersStrategy = Record<string, string>;
const estrategias: Array<{ name: string; headers: HeadersStrategy }> = [
// Estratégia 1: Headers completos de navegador moderno
{
name: 'headers-completos',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
Accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
'Accept-Encoding': 'gzip, deflate, br',
Referer: urlObj.origin + '/',
'Sec-Fetch-Dest': 'image',
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'cross-site',
'Cache-Control': 'no-cache'
} as HeadersStrategy
},
// Estratégia 2: Headers mínimos mas realistas
{
name: 'headers-minimos',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
Accept: 'image/*',
Referer: urlObj.origin + '/'
} as HeadersStrategy
},
// Estratégia 3: Apenas User-Agent básico
{
name: 'user-agent-basico',
headers: {
'User-Agent': 'Mozilla/5.0'
} as HeadersStrategy
},
// Estratégia 4: Sem headers (máximo compatibilidade)
{
name: 'sem-headers',
headers: {} as HeadersStrategy
}
];
let ultimoErro: { status?: number; statusText?: string; message?: string } | null = null;
for (const estrategia of estrategias) {
try {
const response = await fetch(url, {
headers: estrategia.headers,
signal: AbortSignal.timeout(10000) // Timeout de 10 segundos
});
if (response.ok) {
// Verificar Content-Type
const contentType = response.headers.get('content-type');
if (contentType && contentType.startsWith('image/')) {
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const base64 = buffer.toString('base64');
const dataUrl = `data:${contentType};base64,${base64}`;
console.log(
`✅ Imagem baixada usando estratégia "${estrategia.name}": ${url} (${buffer.length} bytes)`
);
return dataUrl;
} else {
console.warn(`Estratégia "${estrategia.name}": Content-Type inválido:`, contentType);
}
} else {
ultimoErro = {
status: response.status,
statusText: response.statusText
};
console.warn(
`Estratégia "${estrategia.name}" falhou: ${response.status} ${response.statusText}`
);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
ultimoErro = { message: errorMessage };
console.warn(`Estratégia "${estrategia.name}" lançou exceção:`, errorMessage);
}
}
// Se todas as estratégias falharam, logar erro detalhado
console.error(
'❌ Todas as estratégias falharam ao baixar imagem:',
url,
'Último erro:',
ultimoErro
);
return null;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('Erro ao baixar imagem de URL:', url, errorMessage);
return null;
}
}
});