Skip to content

Instantly share code, notes, and snippets.

@atomjoy
Last active August 28, 2026 12:04
Show Gist options
  • Select an option

  • Save atomjoy/435f6749000f3232d92ddaca0dc13fb7 to your computer and use it in GitHub Desktop.

Select an option

Save atomjoy/435f6749000f3232d92ddaca0dc13fb7 to your computer and use it in GitHub Desktop.
Lead pipeline panel in html, js.
<!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">&times;</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

atomjoy commented Aug 28, 2026

Copy link
Copy Markdown
Author
lead-pipeline

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment