Skip to content

Consulta produtos #1491

Description

@pedidosbarataomataapp
<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>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions