Dashboard — Automações WhatsApp
by Daniel Gualberto
HTML
<html lang="pt-BR" data-bs-theme="dark">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css" rel="stylesheet">
<nav class="navbar navbar-expand bg-body border-bottom sticky-top" style="height:56px">
<div class="container-fluid gap-2">
<!-- Toggle do sidebar (desktop e mobile) -->
<button class="btn btn-outline-secondary d-inline-flex align-items-center" id="sidebarToggleTop" type="button" aria-label="Alternar menu">
<i class="bi bi-list"></i>
</button>
<!-- Breadcrumb (desktop) -->
<ol class="breadcrumb mb-0 d-none d-md-flex">
<li class="breadcrumb-item"><a href="#">Dashboard</a></li>
<li class="breadcrumb-item active" aria-current="page">Visão geral</li>
</ol>
<!-- Busca global (desktop) -->
<form class="d-none d-lg-flex flex-grow-1" role="search" aria-label="Busca global">
<div class="input-group input-group-sm mx-2">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input id="globalSearch" class="form-control" type="search" placeholder="Buscar ( / ) — campanhas, contatos, fluxos..." aria-label="Buscar">
</div>
</form>
<div class="d-flex align-items-center gap-2 ms-auto">
<!-- Botão busca (mobile) -->
<button class="btn btn-outline-secondary btn-sm d-lg-none" type="button" data-bs-toggle="modal" data-bs-target="#searchModal" aria-label="Buscar">
<i class="bi bi-search"></i>
</button>
<!-- Status (API / Fila) -->
<a href="#" class="badge rounded-pill text-success-emphasis bg-success-subtle text-decoration-none d-none d-md-inline-flex" data-bs-toggle="tooltip" title="Status do sistema">
<span class="me-2"><span class="badge bg-success rounded-circle me-1" style="width:.5rem;height:.5rem;"></span>API OK • 320 ms</span>
</a>
...
CSS
:root{
--radius: 12px;
}
.icon-shape{
width: 44px; height: 44px; border-radius: 10px;
display: grid; place-items: center; font-size: 1.25rem;
}
.kpi-value{ font-size: clamp(1.25rem, 2.8vw, 1.75rem); font-weight: 700; letter-spacing: -0.02em; }
.trend-up{ color: var(--bs-success); }
.trend-down{ color: var(--bs-danger); }
.card{ border-radius: var(--radius); }
.heatmap{
display: grid; grid-template-columns: repeat(7, 1fr); gap: .25rem;
}
.cell{
width: 100%; aspect-ratio: 1/1; border-radius: .4rem; background-color: var(--bs-gray-200);
}
.lvl-1{ background-color: #dbeafe; } /* azul claro */
.lvl-2{ background-color: #bfdbfe; }
.lvl-3{ background-color: #93c5fd; }
.lvl-4{ background-color: #60a5fa; }
.lvl-5{ background-color: #3b82f6; }
.status-dot{
width: .6rem; height: .6rem; border-radius: 50%;
display: inline-block; margin-right: .5rem;
}
.dot-ok{ background: var(--bs-success); }
.dot-warn{ background: var(--bs-warning); }
.dot-bad{ background: var(--bs-danger); }
.text-muted-compact{ font-size: .825rem; color: var(--bs-secondary-color); }
.list-compact .list-group-item{ padding: .6rem .75rem; }
@media (max-width: 575.98px){
.kpi-value{ font-size: 1.35rem; }
}.layout{ display:flex; min-height:100vh; }
.sidebar{
width: 350px; transition: width .2s ease;
position: sticky; top:0; height:100vh; overflow-y:auto;
background: var(--bs-body-bg); border-right: 1px solid var(--bs-border-color);
}
.sidebar.collapsed{ width: 74px; }
.sidebar .brand{ height:56px; }
.sidebar .nav-link{
display:flex; align-items:center; gap:.5rem;
padding:.5rem .75rem; color: var(--bs-body-color);
border-radius:.5rem; margin: .125rem .5rem;
}
.sidebar .nav-link:hover{ background: var(--bs-secondary-bg); }
.sidebar .nav-link.active{...
JavaScript
// Tooltips
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => new bootstrap.Tooltip(el));// Tooltips (úteis no modo colapsado)
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => new bootstrap.Tooltip(el));
// Colapsar/expandir sidebar
const sidebar = document.getElementById('sidebar');
document.getElementById('sidebarToggle').addEventListener('click', () => {
sidebar.classList.toggle('collapsed');
// Fecha submenus abertos ao colapsar para evitar overflow
if (sidebar.classList.contains('collapsed')) {
document.querySelectorAll('#sidebar .collapse.show').forEach(c => new bootstrap.Collapse(c, {toggle:false}).hide());
}
});
(() => {
const $ = (id) => document.getElementById(id);
const debug = new URLSearchParams(location.search).get('debug') === '1';
const logList = $('logList');
const show = (el, v) => el?.classList.toggle('d-none', !v);
// Estado global
const state = {
method: null,
qr: { active:false, connected:false, expiresAt:null, timer:null },
code: {
active:false, connected:false, expiresAt:null, timer:null,
phone:'', channel:'sms', txnId:null, value:null,
callUnlockAt:null, callUnlockSeconds:60
},
devices: [] // começa vazio; será preenchido ao conectar
};
// Utils UI
function toast(msg, variant='primary'){
const id = 't'+Date.now();
const html = `
<div id="${id}" class="toast align-items-center text-bg-${variant} border-0" role="status" aria-live="polite" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">${msg}</div>
<button class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Fechar"></button>
</div>
</div>`;
$('toastArea').insertAdjacentHTML('beforeend', html);
new bootstrap.Toast($(id), {delay:2500}).show();
}
function addLog(text, icon='bi-dot',...