<title>Consulta de Produtos - Código de Barras</title>
<style>
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
background: #f0f2f5;
margin: 0;
padding: 16px;
color: #1e293b;
}
.container {
max-width: 700px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 28px;
box-shadow: 0 8px 20px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.02);
padding: 20px;
margin-bottom: 20px;
transition: all 0.2s;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header h1 {
font-size: 1.7rem;
font-weight: 600;
margin: 0 0 4px 0;
background: linear-gradient(135deg, #2b6e3c, #1e8e3e);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
.header p {
font-size: 0.85rem;
color: #5b6e8c;
margin: 0;
}
.import-area {
background: #f8fafc;
border: 1.5px dashed #94a3b8;
border-radius: 20px;
padding: 16px;
text-align: center;
margin-bottom: 24px;
transition: background 0.2s;
}
.import-label {
display: inline-flex;
align-items: center;
gap: 8px;
background: #1e8e3e;
color: white;
padding: 10px 20px;
border-radius: 40px;
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.2s;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.import-label:active {
background: #166b2e;
transform: scale(0.97);
}
.import-label input {
display: none;
}
.file-status {
font-size: 0.8rem;
margin-top: 10px;
color: #2c6b2f;
font-weight: 500;
}
.stats {
display: flex;
justify-content: space-between;
background: #eef2ff;
padding: 12px 16px;
border-radius: 60px;
margin-bottom: 24px;
font-size: 0.85rem;
font-weight: 500;
}
.search-box {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 24px;
}
.search-input-wrapper {
flex: 1;
position: relative;
}
.search-input-wrapper input {
width: 100%;
padding: 16px 18px;
font-size: 1rem;
border: 1.5px solid #e2e8f0;
border-radius: 48px;
background: white;
transition: all 0.2s;
font-family: monospace;
letter-spacing: 0.3px;
}
.search-input-wrapper input:focus {
outline: none;
border-color: #2b6e3c;
box-shadow: 0 0 0 3px rgba(30,142,62,0.2);
}
.scan-btn {
background: #1e293b;
color: white;
border: none;
border-radius: 48px;
padding: 0 22px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: 0.1s linear;
height: 54px;
}
.scan-btn:active {
background: #0f172a;
transform: scale(0.96);
}
.result-card {
background: white;
border-radius: 28px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.product-found {
border-left: 6px solid #1e8e3e;
}
.product-notfound {
border-left: 6px solid #dc2626;
text-align: center;
color: #7f8c8d;
}
.product-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 8px;
word-break: break-word;
}
.barcode {
font-family: monospace;
background: #f1f5f9;
padding: 6px 12px;
border-radius: 40px;
display: inline-block;
font-size: 0.85rem;
margin-bottom: 16px;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #edf2f7;
}
.info-label {
font-weight: 600;
color: #4a5b7a;
}
.info-value {
font-weight: 500;
text-align: right;
word-break: break-word;
max-width: 60%;
}
.price {
font-size: 1.4rem;
font-weight: 800;
color: #2b6e3c;
}
.help-text {
font-size: 0.75rem;
text-align: center;
margin-top: 24px;
color: #6c7a91;
}
/* Timer bar */
.timer-container {
margin-top: 16px;
padding: 8px 12px;
background: #f8fafc;
border-radius: 60px;
display: flex;
align-items: center;
gap: 12px;
font-size: 0.8rem;
}
.timer-bar {
flex: 1;
height: 6px;
background: #e2e8f0;
border-radius: 10px;
overflow: hidden;
}
.timer-progress {
width: 0%;
height: 100%;
background: #1e8e3e;
border-radius: 10px;
transition: width 0.1s linear;
}
.timer-text {
font-weight: 500;
color: #475569;
min-width: 65px;
text-align: right;
}
button, .import-label {
cursor: pointer;
user-select: none;
}
@media (max-width: 480px) {
body {
padding: 12px;
}
.search-box {
flex-direction: column;
gap: 8px;
}
.scan-btn {
width: 100%;
justify-content: center;
}
.info-row {
flex-direction: column;
gap: 4px;
}
.info-value {
text-align: left;
max-width: 100%;
}
.timer-container {
font-size: 0.7rem;
}
}
</style>
📦 Leitor de Estoque
consulta por código de barras (EAN-13 / GTIN)
<div class="import-area" id="importArea">
<label class="import-label">
📂 IMPORTAR CADASTRO
<input type="file" id="fileInput" accept=".html,.htm,.txt,.csv,text/html,text/plain">
</label>
<div id="fileStatus" class="file-status">Nenhum arquivo carregado. Clique para importar o relatório.</div>
<div id="recordCount" style="font-size:12px; margin-top:6px; color:#475569;"></div>
</div>
<div class="stats" id="statsPanel">
<span>📋 Produtos: <strong id="totalProducts">0</strong></span>
<span>✅ Consultas: <strong id="queryCount">0</strong></span>
</div>
<div class="search-box">
<div class="search-input-wrapper">
<input type="text" id="barcodeInput" placeholder="🔍 Digite ou cole o código de barras" autocomplete="off" enterkeyhint="search">
</div>
<button class="scan-btn" id="searchBtn">
🔎 Consultar
</button>
</div>
<!-- Timer de limpeza automática -->
<div class="timer-container" id="timerContainer" style="display: none;">
<span>🕐 Limpeza automática:</span>
<div class="timer-bar">
<div class="timer-progress" id="timerProgress"></div>
</div>
<span class="timer-text" id="timerText">5.0s</span>
</div>
<div class="result-card" id="resultArea">
<div style="text-align: center; color: #94a3b8; padding: 16px;">
⚡ Aguardando consulta<br>
<span style="font-size: 13px;">Carregue o arquivo e pesquise por código EAN</span>
</div>
</div>
<div class="help-text">
💡 Dica: Use o arquivo "RELATORIO FISCAL + VALORES.html". <br>
Após cada consulta, o resultado e o campo são limpos automaticamente em 5 segundos.
</div>
</div>
<script>
// --------------------------------------------------------------
// Estrutura: produtos em Map (chave = EAN-13)
// --------------------------------------------------------------
let productMap = new Map();
let totalImported = 0;
let queryCounter = 0;
// Controle do timer
let clearTimer = null;
let timerInterval = null;
let remainingTime = 0;
let isTimerRunning = false;
// Elementos DOM
const fileInput = document.getElementById('fileInput');
const fileStatusDiv = document.getElementById('fileStatus');
const recordCountSpan = document.getElementById('recordCount');
const totalProductsSpan = document.getElementById('totalProducts');
const queryCountSpan = document.getElementById('queryCount');
const barcodeInput = document.getElementById('barcodeInput');
const searchBtn = document.getElementById('searchBtn');
const resultArea = document.getElementById('resultArea');
const timerContainer = document.getElementById('timerContainer');
const timerProgress = document.getElementById('timerProgress');
const timerText = document.getElementById('timerText');
// Atualiza contadores na tela
function updateStatsUI() {
totalProductsSpan.innerText = productMap.size;
queryCountSpan.innerText = queryCounter;
recordCountSpan.innerText = productMap.size > 0 ? `✅ ${productMap.size} produtos indexados` : '⚡ Nenhum produto carregado';
if (productMap.size === 0) {
fileStatusDiv.innerText = 'Nenhum arquivo carregado. Clique para importar o relatório.';
fileStatusDiv.style.color = '#b91c1c';
} else {
fileStatusDiv.innerText = `📁 Arquivo carregado com sucesso (${productMap.size} registros)`;
fileStatusDiv.style.color = '#2c6b2f';
}
}
// Para o timer de limpeza
function stopClearTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
if (clearTimer) {
clearTimeout(clearTimer);
clearTimer = null;
}
isTimerRunning = false;
timerContainer.style.display = 'none';
}
// Inicia timer de 5 segundos para limpar o campo e resultado
function startClearTimer() {
// Para qualquer timer anterior
stopClearTimer();
remainingTime = 5.0;
timerContainer.style.display = 'flex';
timerProgress.style.width = '100%';
timerText.innerText = '5.0s';
isTimerRunning = true;
// Atualiza barra a cada 100ms
timerInterval = setInterval(() => {
if (remainingTime > 0) {
remainingTime -= 0.1;
const percent = (remainingTime / 5.0) * 100;
timerProgress.style.width = `${percent}%`;
timerText.innerText = `${remainingTime.toFixed(1)}s`;
}
}, 100);
// Timer final para limpar
clearTimer = setTimeout(() => {
// Limpa o campo de busca
barcodeInput.value = '';
// Restaura o resultado para estado inicial
if (productMap.size > 0) {
resultArea.innerHTML = `
🔄 Pesquisa expirou
Digite um novo código de barras
`;
} else {
resultArea.innerHTML = `
⚡ Aguardando consulta
Carregue o arquivo e pesquise por código EAN
`;
}
// Foca no campo para nova consulta
barcodeInput.focus();
stopClearTimer();
}, 5000);
}
// Extrai preço de custo (converte vírgula para ponto)
function parsePrice(priceStr) {
if (!priceStr) return 0;
let cleaned = priceStr.toString().trim().replace(',', '.');
let num = parseFloat(cleaned);
return isNaN(num) ? 0 : num;
}
function formatMoney(value) {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
}
function normalizeBarcode(raw) {
if (!raw) return '';
return raw.toString().trim().replace(/\s+/g, '').replace(/-/g, '');
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
function showResult(product, searchedCode) {
if (!product) {
resultArea.innerHTML = `
🔍❌
Código não encontrado
🔢 ${escapeHtml(searchedCode)}
Verifique o código ou recarregue o arquivo.
`;
return;
}
const custoFormatado = formatMoney(product.precoCusto);
resultArea.innerHTML = `
${escapeHtml(product.descricao)}
📌 Código: ${escapeHtml(product.ean)}
💰 PREÇO DE CUSTO
${custoFormatado}
🆔 ID do produto
${product.id}
📦 Unidade
${escapeHtml(product.medida || '—')}
🏷️ NCM
${escapeHtml(product.ncm || '—')}
⚖️ Situação Tributária
${escapeHtml(product.situacaoTributaria || '—')}
`;
}
// PARSER DO ARQUIVO HTML
function parseHtmlFileAndIndex(htmlString) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
const tabela = doc.querySelector('table.tabela');
if (!tabela) {
throw new Error('Arquivo inválido: tabela com classe "tabela" não encontrada.');
}
const rows = tabela.querySelectorAll('tr');
if (rows.length < 2) {
throw new Error('Arquivo sem dados (sem linhas de produtos).');
}
let columnMap = {
id: -1, ean: -1, descricao: -1, precoCusto: -1,
medida: -1, ncm: -1, situacaoTributaria: -1
};
for (let i = 0; i < rows.length; i++) {
const ths = rows[i].querySelectorAll('th');
if (ths.length > 0) {
for (let idx = 0; idx < ths.length; idx++) {
const text = ths[idx].innerText.trim().toUpperCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
if (text === 'ID') columnMap.id = idx;
else if (text === 'EAN-13') columnMap.ean = idx;
else if (text === 'DESCRICAO') columnMap.descricao = idx;
else if (text === 'PRECO CUSTO') columnMap.precoCusto = idx;
else if (text === 'MEDIDA') columnMap.medida = idx;
else if (text === 'NCM') columnMap.ncm = idx;
else if (text === 'SITUACAO TRIBUTARIA') columnMap.situacaoTributaria = idx;
}
break;
}
}
if (columnMap.id === -1 || columnMap.ean === -1 || columnMap.descricao === -1 || columnMap.precoCusto === -1) {
throw new Error('Colunas obrigatórias (ID, EAN-13, DESCRICAO, PRECO CUSTO) não encontradas.');
}
const newMap = new Map();
for (let i = 0; i < rows.length; i++) {
const tds = rows[i].querySelectorAll('td');
if (tds.length === 0) continue;
const idCell = tds[columnMap.id]?.innerText.trim();
if (!idCell || isNaN(parseInt(idCell))) continue;
const eanRaw = tds[columnMap.ean]?.innerText.trim() || '';
let eanCode = normalizeBarcode(eanRaw);
if (eanCode === '') continue;
const descricao = tds[columnMap.descricao]?.innerText.trim() || 'SEM DESCRIÇÃO';
const precoRaw = tds[columnMap.precoCusto]?.innerText.trim().replace('R$', '').replace(',', '.').trim();
let precoCusto = parsePrice(precoRaw);
const medida = tds[columnMap.medida]?.innerText.trim() || '';
const ncm = tds[columnMap.ncm]?.innerText.trim() || '';
const situacaoTributaria = tds[columnMap.situacaoTributaria]?.innerText.trim() || '';
newMap.set(eanCode, {
id: idCell, ean: eanCode, descricao: descricao,
precoCusto: precoCusto, medida: medida, ncm: ncm,
situacaoTributaria: situacaoTributaria
});
}
if (newMap.size === 0) {
throw new Error('Nenhum produto válido com código de barras encontrado.');
}
return newMap;
}
function loadFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(e) {
try {
const map = parseHtmlFileAndIndex(e.target.result);
resolve(map);
} catch (err) {
reject(err);
}
};
reader.onerror = () => reject(new Error('Erro ao ler o arquivo.'));
reader.readAsText(file, 'UTF-8');
});
}
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
if (!file) return;
fileStatusDiv.innerText = '⏳ Processando arquivo... Aguarde.';
fileStatusDiv.style.color = '#a16207';
try {
const newMap = await loadFile(file);
productMap = newMap;
updateStatsUI();
// Para qualquer timer pendente
stopClearTimer();
resultArea.innerHTML = `
✅ Arquivo carregado! ${productMap.size} produtos prontos.
🔎 Digite um código de barras para consultar.
`;
barcodeInput.value = '';
barcodeInput.focus();
} catch (error) {
fileStatusDiv.innerText = `❌ Erro: ${error.message}`;
fileStatusDiv.style.color = '#b91c1c';
resultArea.innerHTML = `
Falha ao processar arquivo.
${escapeHtml(error.message)}
`;
productMap.clear();
updateStatsUI();
} finally {
fileInput.value = '';
}
});
function performSearch() {
let rawCode = barcodeInput.value.trim();
if (rawCode === "") {
resultArea.innerHTML = `
⚠️ Digite ou cole um código de barras.
`;
stopClearTimer();
return;
}
// Para timer anterior e inicia novo
stopClearTimer();
queryCounter++;
updateStatsUI();
const normalized = normalizeBarcode(rawCode);
if (normalized === "") {
showResult(null, rawCode);
startClearTimer();
return;
}
const found = productMap.get(normalized);
if (found) {
showResult(found, normalized);
} else {
let altKey = normalized.replace(/^0+/, '');
let altFound = altKey !== normalized ? productMap.get(altKey) : null;
if (altFound) {
showResult(altFound, normalized);
} else {
showResult(null, rawCode);
}
}
// Inicia timer para limpar após 5 segundos
startClearTimer();
}
searchBtn.addEventListener('click', performSearch);
barcodeInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
performSearch();
}
});
// Scanner com câmera
const cameraBtn = document.createElement('button');
cameraBtn.innerText = '📷 Câmera';
cameraBtn.className = 'scan-btn';
cameraBtn.style.background = '#3b5249';
cameraBtn.style.padding = '0 16px';
cameraBtn.style.fontSize = '0.9rem';
cameraBtn.id = 'customCameraBtn';
const searchWrapper = document.querySelector('.search-box');
if (searchWrapper && !document.getElementById('customCameraBtn')) {
searchWrapper.appendChild(cameraBtn);
cameraBtn.addEventListener('click', async () => {
if ('BarcodeDetector' in window) {
try {
const detector = new BarcodeDetector({ formats: ['ean_13', 'ean_8', 'code_128', 'code_39', 'upc_a', 'upc_e'] });
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
const video = document.createElement('video');
video.srcObject = stream;
video.setAttribute('playsinline', true);
video.style.position = 'fixed';
video.style.top = '0';
video.style.left = '0';
video.style.width = '100%';
video.style.height = '100%';
video.style.objectFit = 'cover';
video.style.zIndex = '9999';
video.style.background = 'black';
document.body.appendChild(video);
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
video.play();
const closeBtn = document.createElement('button');
closeBtn.innerText = '✖ Fechar Scanner';
closeBtn.style.position = 'fixed';
closeBtn.style.bottom = '30px';
closeBtn.style.left = '50%';
closeBtn.style.transform = 'translateX(-50%)';
closeBtn.style.zIndex = '10000';
closeBtn.style.padding = '12px 24px';
closeBtn.style.backgroundColor = '#dc2626';
closeBtn.style.color = 'white';
closeBtn.style.border = 'none';
closeBtn.style.borderRadius = '60px';
closeBtn.style.fontWeight = 'bold';
document.body.appendChild(closeBtn);
let scanning = true;
const scanInterval = setInterval(async () => {
if (!scanning) return;
if (video.videoWidth === 0) return;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
context.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
try {
const barcodes = await detector.detect(imageData);
if (barcodes.length > 0) {
const code = barcodes[0].rawValue;
scanning = false;
clearInterval(scanInterval);
stream.getTracks().forEach(track => track.stop());
video.remove();
closeBtn.remove();
barcodeInput.value = code;
performSearch();
}
} catch (err) {}
}, 300);
closeBtn.addEventListener('click', () => {
scanning = false;
clearInterval(scanInterval);
stream.getTracks().forEach(track => track.stop());
video.remove();
closeBtn.remove();
});
} catch (err) {
alert('Erro ao acessar câmera: ' + err.message);
}
} else {
alert('Seu navegador não suporta leitura por câmera. Digite o código manualmente.');
}
});
}
updateStatsUI();
if (productMap.size === 0) {
resultArea.innerHTML = `
📂
Clique em "IMPORTAR CADASTRO" e selecione o arquivo
RELATORIO FISCAL + VALORES.html
Após carregar, consulte produtos por código de barras.
`;
}
</script>
📦 Leitor de Estoque
consulta por código de barras (EAN-13 / GTIN)
Digite um novo código de barras
Carregue o arquivo e pesquise por código EAN
🔎 Digite um código de barras para consultar.
${escapeHtml(error.message)}
Clique em "IMPORTAR CADASTRO" e selecione o arquivo
RELATORIO FISCAL + VALORES.html
Após carregar, consulte produtos por código de barras.