Last active
August 28, 2026 12:04
-
-
Save atomjoy/435f6749000f3232d92ddaca0dc13fb7 to your computer and use it in GitHub Desktop.
Lead pipeline panel in html, js.
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="pl"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>CRM Pipeline</title> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| font-family: -apple-system, sans-serif; | |
| } | |
| body { | |
| background: #f4f6f9; | |
| padding: 40px 20px; | |
| } | |
| .wrapper { | |
| max-width: 1300px; | |
| margin: 0 auto; | |
| background: #fff; | |
| border-radius: 12px; | |
| padding: 24px; | |
| box-shadow: 0 2px 6px rgba(0, 0, 0, 0.01); | |
| } | |
| .top-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 24px; | |
| } | |
| h2 { | |
| font-size: 1.4rem; | |
| color: #1a1a1a; | |
| font-weight: 700; | |
| } | |
| .add-lead-btn { | |
| background: #84cc16; | |
| color: #fff; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 6px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| } | |
| .add-lead-btn:hover { | |
| background: #91e415; | |
| } | |
| .container { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 12px; | |
| } | |
| .col { | |
| flex: 1; | |
| background: #fff; | |
| border: 1px solid #e2e8f0; | |
| border-radius: 10px; | |
| padding: 16px; | |
| min-height: 500px; | |
| } | |
| .col:first-child { | |
| background: #f7f9f2; | |
| border: 1px solid #dbe2cb; | |
| } | |
| .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 16px; | |
| padding-bottom: 8px; | |
| } | |
| .col-title { | |
| font-size: 0.78rem; | |
| text-transform: uppercase; | |
| font-weight: 700; | |
| color: #718096; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: #cbd5e0; | |
| display: inline-block; | |
| } | |
| .col:first-child .dot { | |
| background: #84cc16; | |
| } | |
| .count { | |
| font-size: 0.75rem; | |
| background: #edf2f7; | |
| padding: 2px 6px; | |
| border-radius: 10px; | |
| color: #4a5568; | |
| font-weight: bold; | |
| } | |
| .col:first-child .count { | |
| background: #e2f0d3; | |
| color: #4d7c0f; | |
| } | |
| .col-value { | |
| font-size: 0.75rem; | |
| color: #718096; | |
| margin-right: 6px; | |
| font-weight: 600; | |
| } | |
| .col:first-child .col-value { | |
| color: #4d7c0f; | |
| } | |
| .cards-dropzone { | |
| min-height: 450px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .card { | |
| background: #fff; | |
| border: 1px solid #e5e5e6; | |
| border-radius: 8px; | |
| padding: 12px; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); | |
| display: flex; | |
| flex-direction: column; | |
| cursor: pointer; | |
| } | |
| .card.active-card { | |
| border: 1px solid #84cc16; | |
| box-shadow: 0 0 0 3px #84cc1633; | |
| } | |
| .card.faded { | |
| opacity: 0.7; | |
| } | |
| .card-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .avatar { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 50%; | |
| background: #edf2f7; | |
| color: #4a5568; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 0.8rem; | |
| font-weight: 700; | |
| flex-shrink: 0; | |
| } | |
| .card.active-card .avatar { | |
| background: #d9f99d; | |
| color: #3f6212; | |
| } | |
| .name { | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| color: #1a202c; | |
| } | |
| .source { | |
| font-size: 0.75rem; | |
| color: #718096; | |
| margin-bottom: 2px; | |
| } | |
| .assignee-select-inline { | |
| font-size: 0.72rem; | |
| color: #000; | |
| border: 1px solid #84cc1633; | |
| background: #84cc1633; | |
| border-radius: 4px; | |
| padding: 4px; | |
| outline: none; | |
| max-width: 140px; | |
| cursor: pointer; | |
| font-weight: 400; | |
| } | |
| .card-details { | |
| border-top: 1px solid #f1f5f9; | |
| padding-top: 10px; | |
| margin-top: 10px; | |
| } | |
| .value { | |
| font-size: 0.82rem; | |
| font-weight: 700; | |
| color: #1a202c; | |
| } | |
| .tasks { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| margin-bottom: 10px; | |
| } | |
| .task-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 0.72rem; | |
| color: #4a5568; | |
| cursor: pointer; | |
| } | |
| .task-item input { | |
| accent-color: #84cc16; | |
| cursor: pointer; | |
| } | |
| .next-btn { | |
| align-self: stretch; | |
| background: transparent; | |
| border: 1px solid #dcdde1; | |
| color: #7f8c8d; | |
| padding: 5px; | |
| font-size: 0.7rem; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| text-align: center; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| transition: all 0.2s; | |
| } | |
| .next-btn:hover { | |
| background: #34495e; | |
| color: #fff; | |
| border-color: #34495e; | |
| } | |
| .col:last-child .next-btn { | |
| display: none; | |
| } | |
| .footer { | |
| margin-top: 24px; | |
| padding-top: 20px; | |
| border-top: 1px solid #edf2f7; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| font-size: 0.95rem; | |
| color: #4a5568; | |
| } | |
| .f-amount { | |
| font-size: 1.6rem; | |
| font-weight: 800; | |
| color: #1a1a1a; | |
| } | |
| .modal-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.5); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| } | |
| .modal-overlay.active { | |
| display: flex; | |
| } | |
| .modal-box { | |
| background: #fff; | |
| padding: 24px; | |
| border-radius: 8px; | |
| width: 100%; | |
| max-width: 450px; | |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); | |
| animation: fadeIn 0.2s ease-out; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: scale(0.95); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| } | |
| .modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| padding-bottom: 10px; | |
| border-bottom: 1px solid #eee; | |
| } | |
| .modal-title { | |
| font-size: 1.2rem; | |
| color: #2c3e50; | |
| font-weight: bold; | |
| } | |
| .close-btn { | |
| background: none; | |
| border: none; | |
| font-size: 1.5rem; | |
| color: #7f8c8d; | |
| cursor: pointer; | |
| line-height: 1; | |
| } | |
| .close-btn:hover { | |
| color: #333; | |
| } | |
| .form-group { | |
| margin-bottom: 14px; | |
| } | |
| .form-group label { | |
| display: block; | |
| font-size: 0.8rem; | |
| font-weight: bold; | |
| color: #34495e; | |
| margin-bottom: 5px; | |
| } | |
| .form-group input, | |
| .form-group select, | |
| .form-group textarea { | |
| width: 100%; | |
| padding: 10px; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| font-size: 0.9rem; | |
| color: #2c3e50; | |
| outline: none; | |
| transition: border-color 0.2s; | |
| background: #fff; | |
| } | |
| .form-group input:focus, | |
| .form-group select:focus, | |
| .form-group textarea:focus { | |
| border-color: #84cc16; | |
| } | |
| .form-row { | |
| display: flex; | |
| gap: 12px; | |
| } | |
| .form-row .form-group { | |
| flex: 1; | |
| } | |
| .form-actions { | |
| display: flex; | |
| justify-content: flex-end; | |
| gap: 10px; | |
| margin-top: 20px; | |
| padding-top: 10px; | |
| border-top: 1px solid #eee; | |
| } | |
| .btn-cancel { | |
| background: #e74c3c; | |
| color: #fff; | |
| border: none; | |
| padding: 10px 16px; | |
| border-radius: 4px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| } | |
| .btn-save { | |
| background: #84cc16; | |
| color: #fff; | |
| border: none; | |
| padding: 10px 16px; | |
| border-radius: 4px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| } | |
| .btn-cancel:hover { | |
| background: #c0392b; | |
| } | |
| .btn-save:hover { | |
| background: #90e216; | |
| } | |
| </style> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const employees = [ | |
| { id: 101, name: 'Marcin Kowalski' }, | |
| { id: 102, name: 'Anna Nowak' }, | |
| { id: 103, name: 'Piotr Zieliński' }, | |
| ] | |
| const defaultLeads = [ | |
| { id: 1, company: 'ACME Corp', name: 'Jan Kowalski', source: 'Formularz WWW', value: 30000, stage: 0, task1: false, task2: false, assigneeId: 101 }, | |
| { id: 2, company: 'XYZ Logistics', name: 'Anna Nowak', source: 'Meta Ads', value: 12500, stage: 0, task1: false, task2: false, assigneeId: 102 }, | |
| { id: 3, company: 'SoftTech SA', name: 'Marta Wilk', source: 'Polecenie', value: 19900, stage: 1, task1: false, task2: false, assigneeId: 103 }, | |
| ] | |
| const state = { | |
| leads: JSON.parse(localStorage.getItem('crm_leads')) || defaultLeads, | |
| activeCardId: parseInt(localStorage.getItem('crm_active_card_id')) || 1, | |
| history: JSON.parse(localStorage.getItem('crm_history')) || { won: 0, lost: 0 }, | |
| } | |
| const modal = document.getElementById('modal') | |
| const openModalBtn = document.getElementById('openModal') | |
| const closeX = document.getElementById('closeX') | |
| const closeBtn = document.getElementById('closeBtn') | |
| const leadForm = document.getElementById('leadForm') | |
| const fAssignee = document.getElementById('f-assignee') | |
| function initAssigneeOptions() { | |
| if (fAssignee) { | |
| fAssignee.innerHTML = employees.map((emp) => `<option value="${emp.id}">${emp.name}</option>`).join('') | |
| } | |
| } | |
| const toggleModal = () => modal.classList.toggle('active') | |
| openModalBtn.addEventListener('click', toggleModal) | |
| closeX.addEventListener('click', toggleModal) | |
| closeBtn.addEventListener('click', toggleModal) | |
| modal.addEventListener('click', (e) => { | |
| if (e.target === modal) toggleModal() | |
| }) | |
| function saveToStorage() { | |
| localStorage.setItem('crm_leads', JSON.stringify(state.leads)) | |
| localStorage.setItem('crm_active_card_id', state.activeCardId) | |
| localStorage.setItem('crm_history', JSON.stringify(state.history)) | |
| } | |
| function getInitials(company, name) { | |
| if (company) return company.substring(0, 2).toUpperCase() | |
| return name.substring(0, 2).toUpperCase() | |
| } | |
| window.moveNext = function (id) { | |
| const lead = state.leads.find((l) => l.id === id) | |
| if (lead) { | |
| if (lead.stage < 3) { | |
| lead.stage++ | |
| if (lead.stage === 3) { | |
| state.history.won++ | |
| } | |
| saveToStorage() | |
| renderPipeline() | |
| } | |
| } | |
| } | |
| window.deleteLead = function (id) { | |
| if (confirm('Czy chcesz oznaczyć ten lead jako przegrany i usunąć?')) { | |
| const lead = state.leads.find((l) => l.id === id) | |
| if (lead && lead.stage !== 3) { | |
| state.history.lost++ | |
| } | |
| state.leads = state.leads.filter((l) => l.id !== id) | |
| if (state.activeCardId === id && state.leads.length > 0) { | |
| state.activeCardId = state.leads[0].id | |
| } else if (state.leads.length === 0) { | |
| state.activeCardId = null | |
| } | |
| saveToStorage() | |
| renderPipeline() | |
| } | |
| } | |
| window.toggleTask = function (id, taskNumber, checked) { | |
| const lead = state.leads.find((l) => l.id === id) | |
| if (lead) { | |
| if (taskNumber === 1) lead.task1 = checked | |
| if (taskNumber === 2) lead.task2 = checked | |
| saveToStorage() | |
| } | |
| } | |
| window.changeAssignee = function (leadId, newEmployeeId) { | |
| const lead = state.leads.find((l) => l.id === leadId) | |
| if (lead) { | |
| lead.assigneeId = parseInt(newEmployeeId) | |
| saveToStorage() | |
| renderPipeline() | |
| } | |
| } | |
| function formatMoney(amount) { | |
| if (amount) { | |
| return amount.toLocaleString('pl-PL') + ' zł' | |
| } | |
| return 0 + ' zł' | |
| } | |
| function selectCard(id) { | |
| state.activeCardId = id | |
| saveToStorage() | |
| renderPipeline() | |
| } | |
| function renderPipeline() { | |
| const dropzones = [document.getElementById('col-0'), document.getElementById('col-1'), document.getElementById('col-2'), document.getElementById('col-3')] | |
| dropzones.forEach((dz) => { | |
| if (dz) dz.innerHTML = '' | |
| }) | |
| let totalPipelineValue = 0 | |
| let colValues = [0, 0, 0, 0] | |
| state.leads.forEach((lead) => { | |
| totalPipelineValue += lead.value | |
| colValues[lead.stage] = colValues[lead.stage] + lead.value | |
| const isActive = lead.id === state.activeCardId | |
| const cardEl = document.createElement('div') | |
| cardEl.className = `card ${isActive ? 'active-card' : 'faded'}` | |
| cardEl.onclick = (e) => { | |
| if (!e.target.closest('input') && !e.target.closest('button') && !e.target.closest('select')) { | |
| selectCard(lead.id) | |
| } | |
| } | |
| const optionsHtml = employees.map((emp) => `<option value="${emp.id}" ${emp.id === lead.assigneeId ? 'selected' : ''}>${emp.name}</option>`).join('') | |
| cardEl.innerHTML = ` | |
| <div class="card-row"> | |
| <div class="avatar">${getInitials(lead.company, lead.name)}</div> | |
| <div> | |
| <div class="name">${lead.company || lead.name}</div> | |
| <div class="source">${lead.source}</div> | |
| </div> | |
| </div> | |
| <div class="card-details"> | |
| <div style="display: flex; margin-bottom: 5px; align-items: center; justify-content: space-between"> | |
| <div class="value">${formatMoney(lead.value)}</div> | |
| <select class="assignee-select-inline" onchange="changeAssignee(${lead.id}, this.value)"> | |
| ${optionsHtml} | |
| </select> | |
| </div> | |
| <div class="tasks"> | |
| <label class="task-item"> | |
| <input type="checkbox" ${lead.task1 ? 'checked' : ''} onchange="toggleTask(${lead.id}, 1, this.checked)"> | |
| Przygotować ofertę | |
| </label> | |
| <label class="task-item"> | |
| <input type="checkbox" ${lead.task2 ? 'checked' : ''} onchange="toggleTask(${lead.id}, 2, this.checked)"> | |
| Wysłać e-mail | |
| </label> | |
| </div> | |
| <div style="display:flex;gap:6px;"> | |
| <button class="next-btn" onclick="moveNext(${lead.id})">${lead.stage === 2 ? 'Wygrana 🎉' : 'Dalej'}</button> | |
| <button class="next-btn" style="border-color:#e74c3c;color:#e74c3c;" onclick="deleteLead(${lead.id})">Usuń</button> | |
| </div> | |
| </div> | |
| ` | |
| if (dropzones[lead.stage]) { | |
| dropzones[lead.stage].appendChild(cardEl) | |
| } | |
| }) | |
| ;[0, 1, 2, 3].forEach((i) => { | |
| const colLeads = state.leads.filter((l) => l.stage === i) | |
| const dz = document.getElementById(`col-${i}`) | |
| if (dz) { | |
| const colElement = dz.parentElement | |
| if (colElement) { | |
| const countEl = colElement.querySelector('.count') | |
| if (countEl) countEl.innerText = colLeads.length | |
| } | |
| const valEl = document.getElementById(`val-${i}`) | |
| if (valEl) { | |
| valEl.innerText = formatMoney(colValues[i]) | |
| } | |
| } | |
| }) | |
| document.getElementById('totalAmount').innerText = formatMoney(totalPipelineValue) | |
| const totalClosed = state.history.won + state.history.lost | |
| const rate = totalClosed > 0 ? Math.round((state.history.won / totalClosed) * 100) : 0 | |
| document.getElementById('conversionRate').innerText = `${rate}%` | |
| document.getElementById('wonCount').innerText = state.history.won | |
| document.getElementById('lostCount').innerText = state.history.lost | |
| } | |
| leadForm.addEventListener('submit', (e) => { | |
| e.preventDefault() | |
| const newLead = { | |
| id: Date.now(), | |
| company: document.getElementById('f-company').value, | |
| name: document.getElementById('f-name').value, | |
| source: document.getElementById('f-source').value, | |
| value: parseFloat(document.getElementById('f-value').value) || 0, | |
| stage: 0, | |
| task1: false, | |
| task2: false, | |
| assigneeId: parseInt(document.getElementById('f-assignee').value), | |
| } | |
| state.leads.push(newLead) | |
| state.activeCardId = newLead.id | |
| toggleModal() | |
| leadForm.reset() | |
| saveToStorage() | |
| renderPipeline() | |
| }) | |
| initAssigneeOptions() | |
| renderPipeline() | |
| }) | |
| </script> | |
| </head> | |
| <body> | |
| <div class="wrapper"> | |
| <div class="top-bar"> | |
| <h2>Pipeline sprzedaży</h2> | |
| <button class="add-lead-btn" id="openModal">Nowy lead</button> | |
| </div> | |
| <div class="container"> | |
| <div class="col"> | |
| <div class="header"> | |
| <div class="col-title"><span class="dot"></span>Nowy lead</div> | |
| <div><span class="col-value" id="val-0">0 zł</span> <span class="count">0</span></div> | |
| </div> | |
| <div class="cards-dropzone" id="col-0"></div> | |
| </div> | |
| <div class="col"> | |
| <div class="header"> | |
| <div class="col-title"><span class="dot"></span>Zakwalifikowany</div> | |
| <div><span class="col-value" id="val-1">0 zł</span> <span class="count">0</span></div> | |
| </div> | |
| <div class="cards-dropzone" id="col-1"></div> | |
| </div> | |
| <div class="col"> | |
| <div class="header"> | |
| <div class="col-title"><span class="dot"></span>Oferta wysłana</div> | |
| <div><span class="col-value" id="val-2">0 zł</span> <span class="count">0</span></div> | |
| </div> | |
| <div class="cards-dropzone" id="col-2"></div> | |
| </div> | |
| <div class="col"> | |
| <div class="header"> | |
| <div class="col-title"><span class="dot"></span>Umowa podpisana</div> | |
| <div><span class="col-value" id="val-3">0 zł</span> <span class="count">0</span></div> | |
| </div> | |
| <div class="cards-dropzone" id="col-3"></div> | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| <div>Pula wszystkich umów w pipeline: <span class="f-amount" id="totalAmount">0 zł</span></div> | |
| <div style="font-size: 0.9rem; color: #718096">Skuteczność (Konwersja): <span id="conversionRate" style="font-weight: bold; color: #2ecc71">0%</span> (Wygrane: <span id="wonCount">0</span> | Przegrane: <span id="lostCount">0</span>)</div> | |
| </div> | |
| </div> | |
| <div class="modal-overlay" id="modal"> | |
| <div class="modal-box"> | |
| <div class="modal-header"> | |
| <div class="modal-title">Dodaj nowy lead</div> | |
| <button class="close-btn" id="closeX">×</button> | |
| </div> | |
| <form id="leadForm"> | |
| <div class="form-group"> | |
| <label>NAZWA FIRMY / KLIENTA</label> | |
| <input type="text" id="f-company" placeholder="np. ACME Corp" required /> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>OSOBA DECYZYJNA</label> | |
| <input type="text" id="f-name" placeholder="Jan Kowalski" required /> | |
| </div> | |
| <div class="form-group"> | |
| <label>ŹRÓDŁO POZYSKANIA</label> | |
| <select id="f-source" required> | |
| <option value="">Wybierz...</option> | |
| <option value="Formularz WWW">Formularz WWW</option> | |
| <option value="Meta Ads">Meta Ads</option> | |
| <option value="Google Ads">Google Ads</option> | |
| <option value="Polecenie">Polecenie</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>SZACOWANA WARTOŚĆ (ZŁ)</label> | |
| <input type="number" id="f-value" placeholder="0.00" min="0" required /> | |
| </div> | |
| <div class="form-group"> | |
| <label>OPIEKUN LEADA</label> | |
| <select id="f-assignee" required> | |
| <!-- Opcje ładowane dynamicznie z JS --> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="form-actions"> | |
| <button type="button" class="btn-cancel" id="closeBtn">Anuluj</button> | |
| <button type="submit" class="btn-save">Zapisz lead</button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </body> | |
| </html> |
atomjoy
commented
Aug 28, 2026
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment