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',...