A Pen by Adriel Santos on CodePen.
Created
November 25, 2025 11:30
-
-
Save adrielcruz9966-a11y/75c8dde50ff40efd0e5020bff72e158e to your computer and use it in GitHub Desktop.
Untitled
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="pt-BR"> | |
| <head> | |
| <meta charset="utf-8"/> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"/> | |
| <title>Sistema Safra — Correspondente Bancário</title> | |
| <style> | |
| :root{ | |
| --bg:#0f1724; | |
| --card:#0b1220; | |
| --accent:#06b6d4; | |
| --accent-2:#06d6a0; | |
| --muted:#94a3b8; | |
| } | |
| *{box-sizing:border-box;font-family:Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial} | |
| body{margin:0;background:linear-gradient(180deg,#071024 0%, #071029 100%);color:#e6eef8;min-height:100vh;padding:28px} | |
| .container{max-width:980px;margin:0 auto} | |
| header{display:flex;gap:16px;align-items:center;justify-content:space-between;margin-bottom:18px} | |
| header div:first-child > div:first-child{font-size:28px;font-weight:bold;color:#06b6d4;animation:slideFadeIn 1s ease-out;} | |
| h1{font-size:20px;margin:0} | |
| p.lead{margin:0;color:var(--muted);font-size:13px} | |
| .card{background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));padding:16px;border-radius:12px;box-shadow:0 6px 24px rgba(2,6,23,0.6);animation:cardFadeIn 0.6s ease-out;} | |
| .grid{display:grid;grid-template-columns:1fr 360px;gap:16px} | |
| form{display:flex;gap:8px;flex-wrap:wrap} | |
| input,select,button{padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.06);background:transparent;color:inherit} | |
| input::placeholder{color:rgba(230,238,248,0.5)} | |
| .small{font-size:13px;color:var(--muted)} | |
| .list{margin-top:12px} | |
| .lead-row{display:flex;align-items:center;justify-content:space-between;padding:10px;border-radius:8px;margin-bottom:8px;background:linear-gradient(90deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));animation:cardFadeIn 0.5s ease-out;} | |
| .lead-main{display:flex;gap:12px;align-items:center} | |
| .badge{font-size:12px;padding:6px 8px;border-radius:999px;background:rgba(255,255,255,0.03)} | |
| .status{font-weight:600} | |
| .actions{display:flex;gap:8px} | |
| .btn{cursor:pointer;border:none} | |
| .btn.primary{background:var(--accent);color:#042028} | |
| .btn.ghost{background:transparent;border:1px solid rgba(255,255,255,0.04)} | |
| .progress-wrap{margin-top:12px} | |
| .progress{height:18px;background:rgba(255,255,255,0.04);border-radius:999px;overflow:hidden} | |
| .progress > i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--accent-2));width:0%} | |
| .meta{display:flex;gap:12px;align-items:center;flex-wrap:wrap} | |
| .meta .box{flex:1;padding:8px;border-radius:8px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));} | |
| footer.small{margin-top:14px;color:var(--muted);font-size:12px} | |
| .status-badge{font-size:11px;padding:4px 8px;border-radius:6px;font-weight:600} | |
| .status-prospectar{background:rgba(6,182,212,0.15);color:#06b6d4} | |
| .status-contato{background:rgba(251,191,36,0.15);color:#fbbf24} | |
| .status-proposta{background:rgba(168,85,247,0.15);color:#a855f7} | |
| .status-fechado{background:rgba(6,214,160,0.15);color:#06d6a0} | |
| @keyframes slideFadeIn {0%{opacity:0;transform:translateX(-30px);}100%{opacity:1;transform:translateX(0);}} | |
| @keyframes cardFadeIn {0%{opacity:0;transform:translateY(10px);}100%{opacity:1;transform:translateY(0);}} | |
| @media(max-width:900px){.grid{grid-template-columns:1fr;}.container{padding:12px}} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <div style="display:flex;align-items:center;gap:12px;"> | |
| <div>Safra</div> | |
| <div> | |
| <h1>Sistema de Metas — Correspondente Bancário</h1> | |
| <p class="lead">Rastreie clientes para Categoria Ouro e desbloqueio do INSS — meta atual: <strong id="meta-total">4</strong></p> | |
| </div> | |
| </div> | |
| <div class="small">Usuário: <strong>Adriel</strong></div> | |
| </header> | |
| <div class="grid"> | |
| <div> | |
| <div class="card"> | |
| <form id="addForm"> | |
| <input id="name" placeholder="Nome do cliente" required /> | |
| <input id="cpf" placeholder="CPF (somente números)" required /> | |
| <input id="phone" placeholder="Telefone (opcional)" /> | |
| <select id="status"> | |
| <option value="Prospectar">Prospectar</option> | |
| <option value="Contato">Contato</option> | |
| <option value="Proposta">Proposta</option> | |
| <option value="Fechado">Fechado</option> | |
| </select> | |
| <button class="btn primary" type="submit">Adicionar</button> | |
| <button id="clearBtn" class="btn ghost" type="button">Limpar tudo</button> | |
| </form> | |
| <div style="margin-top:12px;display:flex;gap:10px;align-items:center"> | |
| <div class="small">Filtro:</div> | |
| <select id="filter"> | |
| <option value="all">Todos</option> | |
| <option>Prospectar</option> | |
| <option>Contato</option> | |
| <option>Proposta</option> | |
| <option>Fechado</option> | |
| </select> | |
| <button id="exportCsv" class="btn ghost">Exportar CSV</button> | |
| </div> | |
| <div class="list" id="leadsList"></div> | |
| </div> | |
| <div class="card" style="margin-top:12px"> | |
| <h3 style="margin:0 0 8px 0">Resumo / Progresso</h3> | |
| <div class="meta"> | |
| <div class="box">Meta total: <strong id="metaDisplay">4</strong></div> | |
| <div class="box">Fechados: <strong id="closedCount">0</strong></div> | |
| <div class="box">Propostas: <strong id="proposalCount">0</strong></div> | |
| </div> | |
| <div class="progress-wrap"> | |
| <div class="small">Progresso até a meta</div> | |
| <div class="progress" aria-hidden> | |
| <i id="progressBar"></i> | |
| </div> | |
| <div style="display:flex;justify-content:space-between;margin-top:6px"> | |
| <div class="small">0</div> | |
| <div class="small" id="progressText">0 / 4</div> | |
| </div> | |
| </div> | |
| <div style="margin-top:10px;display:flex;gap:8px;align-items:center"> | |
| <input id="metaInput" type="number" min="1" placeholder="Definir meta (ex: 4)" /> | |
| <button id="setMeta" class="btn primary">Definir</button> | |
| </div> | |
| <div style="margin-top:10px" class="small">Dica: registre cada contato e atualize o status. A meta é salva no navegador.</div> | |
| </div> | |
| </div> | |
| <aside> | |
| <div class="card"> | |
| <h3 style="margin-top:0">Resumo rápido</h3> | |
| <div style="display:flex;flex-direction:column;gap:8px;margin-top:10px"> | |
| <div class="small">Meta total atual: <strong id="sideMeta">4</strong></div> | |
| <div class="small">Atingido: <strong id="sideAchieved">0</strong></div> | |
| <div class="small">Restam: <strong id="sideRemaining">4</strong></div> | |
| <div style="height:8px"></div> | |
| <button id="resetStorage" class="btn ghost">Resetar armazenamento</button> | |
| <button id="sampleData" class="btn primary">Inserir dados de exemplo</button> | |
| </div> | |
| </div> | |
| <div class="card" style="margin-top:12px"> | |
| <h4 style="margin:0">Como usar</h4> | |
| <ol style="padding-left:18px;margin-top:8px;color:var(--muted);font-size:13px"> | |
| <li>Adicione clientes e atualize o <em>status</em>.</li> | |
| <li>Defina sua meta total (ex: 4).</li> | |
| <li>Acompanhe o progresso e exporte CSV para enviar à análise.</li> | |
| </ol> | |
| </div> | |
| <footer class="small">Feito para você — pronto para colar no CodePen.</footer> | |
| </aside> | |
| </div> | |
| </div> | |
| <script> | |
| (function(){ | |
| const KEY = 'safra_system_v1' | |
| const metaDefault = 4 | |
| const addForm = document.getElementById('addForm') | |
| const nameIn = document.getElementById('name') | |
| const cpfIn = document.getElementById('cpf') | |
| const phoneIn = document.getElementById('phone') | |
| const statusIn = document.getElementById('status') | |
| const leadsList = document.getElementById('leadsList') | |
| const progressBar = document.getElementById('progressBar') | |
| const progressText = document.getElementById('progressText') | |
| const metaDisplay = document.getElementById('metaDisplay') | |
| const metaInput = document.getElementById('metaInput') | |
| const setMeta = document.getElementById('setMeta') | |
| const closedCount = document.getElementById('closedCount') | |
| const proposalCount = document.getElementById('proposalCount') | |
| const metaTotalLabel = document.getElementById('meta-total') | |
| const sideMeta = document.getElementById('sideMeta') | |
| const sideAchieved = document.getElementById('sideAchieved') | |
| const sideRemaining = document.getElementById('sideRemaining') | |
| const filter = document.getElementById('filter') | |
| const clearBtn = document.getElementById('clearBtn') | |
| const exportCsv = document.getElementById('exportCsv') | |
| const resetStorage = document.getElementById('resetStorage') | |
| const sampleData = document.getElementById('sampleData') | |
| let state = { meta: metaDefault, leads: [] } | |
| function save(){ localStorage.setItem(KEY, JSON.stringify(state)) } | |
| function load(){ | |
| try{ | |
| const raw = localStorage.getItem(KEY); | |
| if(raw) { | |
| const parsed = JSON.parse(raw); | |
| state = parsed; | |
| } | |
| }catch(e){ | |
| console.warn('load failed', e); | |
| state = { meta: metaDefault, leads: [] }; | |
| } | |
| } | |
| function uid(){ return Date.now().toString(36) + Math.random().toString(36).slice(2,6) } | |
| function getStatusClass(status) { | |
| const statusMap = { | |
| 'Prospectar': 'status-prospectar', | |
| 'Contato': 'status-contato', | |
| 'Proposta': 'status-proposta', | |
| 'Fechado': 'status-fechado' | |
| }; | |
| return statusMap[status] || 'status-prospectar'; | |
| } | |
| function formatCPF(cpf) { | |
| if (cpf.length === 11) { | |
| return cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4'); | |
| } | |
| return cpf; | |
| } | |
| function formatPhone(phone) { | |
| const cleaned = phone.replace(/\D/g, ''); | |
| if (cleaned.length === 11) { | |
| return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3'); | |
| } else if (cleaned.length === 10) { | |
| return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '($1) $2-$3'); | |
| } | |
| return phone; | |
| } | |
| function render(){ | |
| leadsList.innerHTML = '' | |
| const f = filter.value | |
| const filtered = state.leads.filter(l=> f==='all'?true:l.status===f) | |
| if (filtered.length === 0) { | |
| leadsList.innerHTML = '<div class="small" style="text-align:center;padding:20px;color:var(--muted)">Nenhum cliente encontrado</div>' | |
| } else { | |
| filtered.forEach(l=>{ | |
| const el = document.createElement('div') | |
| el.className = 'lead-row' | |
| el.innerHTML = ` | |
| <div class="lead-main"> | |
| <div style="min-width:180px"> | |
| <div><strong>${escape(l.name)}</strong></div> | |
| <div class="small" style="margin-top:6px">CPF: ${formatCPF(l.cpf)}</div> | |
| <div class="small">${l.phone ? formatPhone(l.phone) : ''}</div> | |
| </div> | |
| <div class="status-badge ${getStatusClass(l.status)}">${l.status}</div> | |
| </div> | |
| <div class="actions"> | |
| <select data-id="${l.id}"> | |
| <option ${l.status==='Prospectar'?'selected':''}>Prospectar</option> | |
| <option ${l.status==='Contato'?'selected':''}>Contato</option> | |
| <option ${l.status==='Proposta'?'selected':''}>Proposta</option> | |
| <option ${l.status==='Fechado'?'selected':''}>Fechado</option> | |
| </select> | |
| <button class="btn ghost" data-del="${l.id}">Excluir</button> | |
| </div> | |
| ` | |
| leadsList.appendChild(el) | |
| }) | |
| } | |
| const closed = state.leads.filter(x=>x.status==='Fechado').length | |
| const proposals = state.leads.filter(x=>x.status==='Proposta').length | |
| closedCount.textContent = closed | |
| proposalCount.textContent = proposals | |
| metaDisplay.textContent = state.meta | |
| metaTotalLabel.textContent = state.meta | |
| sideMeta.textContent = state.meta | |
| sideAchieved.textContent = closed | |
| sideRemaining.textContent = Math.max(0,state.meta-closed) | |
| const pct = Math.min(100,Math.round((closed/state.meta)*100)) | |
| progressBar.style.width = pct+'%' | |
| progressText.textContent = closed+' / '+state.meta | |
| save() | |
| } | |
| function escape(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') } | |
| addForm.addEventListener('submit', e=>{ | |
| e.preventDefault() | |
| const name = nameIn.value.trim() | |
| const cpf = cpfIn.value.trim().replace(/\D/g, '') | |
| if(!name) return alert('Digite o nome do cliente') | |
| if(!/^[0-9]{11}$/.test(cpf)) return alert('CPF inválido, digite 11 números.') | |
| // Verificar se CPF já existe | |
| if(state.leads.some(lead => lead.cpf === cpf)) { | |
| return alert('Já existe um cliente cadastrado com este CPF.') | |
| } | |
| const lead = { | |
| id: uid(), | |
| name, | |
| cpf, | |
| phone: phoneIn.value.trim().replace(/\D/g, ''), | |
| status: statusIn.value, | |
| createdAt: new Date().toISOString() | |
| } | |
| state.leads.unshift(lead) | |
| nameIn.value=''; cpfIn.value=''; phoneIn.value=''; statusIn.value='Prospectar' | |
| render() | |
| }) | |
| leadsList.addEventListener('change', e=>{ | |
| const sel = e.target | |
| const id = sel.dataset.id | |
| if(!id) return | |
| const lead = state.leads.find(x=>x.id===id) | |
| if(lead) { | |
| lead.status = sel.value | |
| lead.updatedAt = new Date().toISOString() | |
| } | |
| render() | |
| }) | |
| leadsList.addEventListener('click', e=>{ | |
| const del = e.target.closest('[data-del]') | |
| if(!del) return | |
| const id = del.dataset.del | |
| if(confirm('Tem certeza que deseja excluir este cliente?')) { | |
| state.leads = state.leads.filter(x=>x.id!==id) | |
| render() | |
| } | |
| }) | |
| filter.addEventListener('change', render) | |
| setMeta.addEventListener('click', ()=>{ | |
| const v = parseInt(metaInput.value,10) | |
| if(!v || v<1) return alert('Defina uma meta válida (número inteiro >=1).') | |
| state.meta = v | |
| metaInput.value = '' | |
| render() | |
| }) | |
| clearBtn.addEventListener('click', ()=>{ | |
| if(confirm('Tem certeza que deseja remover todos os clientes?')) { | |
| state.leads = [] | |
| render() | |
| } | |
| }) | |
| exportCsv.addEventListener('click', ()=>{ | |
| if(state.leads.length === 0) { | |
| alert('Não há dados para exportar.') | |
| return | |
| } | |
| const headers = ['Nome', 'CPF', 'Telefone', 'Status', 'Data de Criação'] | |
| const csvData = state.leads.map(lead => [ | |
| lead.name, | |
| formatCPF(lead.cpf), | |
| lead.phone ? formatPhone(lead.phone) : '', | |
| lead.status, | |
| new Date(lead.createdAt).toLocaleDateString('pt-BR') | |
| ]) | |
| const csvContent = [headers, ...csvData] | |
| .map(row => row.map(field => `"${field}"`).join(',')) | |
| .join('\n') | |
| const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }) | |
| const url = URL.createObjectURL(blob) | |
| const link = document.createElement('a') | |
| link.setAttribute('href', url) | |
| link.setAttribute('download', `clientes_safra_${new Date().toISOString().slice(0,10)}.csv`) | |
| link.style.visibility = 'hidden' | |
| document.body.appendChild(link) | |
| link.click() | |
| document.body.removeChild(link) | |
| }) | |
| resetStorage.addEventListener('click', ()=>{ | |
| if(confirm('Isso irá resetar todos os dados. Tem certeza?')) { | |
| localStorage.removeItem(KEY) | |
| state = { meta: metaDefault, leads: [] } | |
| render() | |
| } | |
| }) | |
| sampleData.addEventListener('click', ()=>{ | |
| const sampleLeads = [ | |
| { | |
| id: uid(), | |
| name: 'João Silva', | |
| cpf: '12345678901', | |
| phone: '11999999999', | |
| status: 'Prospectar', | |
| createdAt: new Date().toISOString() | |
| }, | |
| { | |
| id: uid(), | |
| name: 'Maria Santos', | |
| cpf: '98765432100', | |
| phone: '11888888888', | |
| status: 'Contato', | |
| createdAt: new Date(Date.now() - 86400000).toISOString() | |
| }, | |
| { | |
| id: uid(), | |
| name: 'Pedro Oliveira', | |
| cpf: '45678912345', | |
| phone: '11777777777', | |
| status: 'Proposta', | |
| createdAt: new Date(Date.now() - 172800000).toISOString() | |
| }, | |
| { | |
| id: uid(), | |
| name: 'Ana Costa', | |
| cpf: '78912345678', | |
| phone: '11666666666', | |
| status: 'Fechado', | |
| createdAt: new Date(Date.now() - 259200000).toISOString() | |
| } | |
| ] | |
| state.leads.push(...sampleLeads) | |
| render() | |
| }) | |
| // Inicialização | |
| load() | |
| render() | |
| })() | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment