Files

1586 lines
70 KiB
HTML

<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Sistema Nuvem</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<style>
:root {
--primary: #06b6d4;
--primary-dark: #0891b2;
--bg-dark: #0f172a;
--bg-card: #1e293b;
--bg-sidebar: #0d1324;
--text-light: #f1f5f9;
--text-muted: #94a3b8;
--glass-border: rgba(255, 255, 255, 0.08);
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #06b6d4;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg-dark);
color: var(--text-light);
}
.sidebar {
position: fixed;
left: 0;
top: 0;
width: 260px;
height: 100vh;
background: var(--bg-sidebar);
border-right: 1px solid var(--glass-border);
padding: 24px 16px;
z-index: 50;
display: flex;
flex-direction: column;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: 12px;
padding: 0 8px 24px;
border-bottom: 1px solid var(--glass-border);
margin-bottom: 24px;
}
.sidebar-brand .icon {
width: 42px;
height: 42px;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: white;
}
.sidebar-brand h3 {
font-size: 1rem;
font-weight: 700;
margin: 0;
}
.sidebar-brand span {
font-size: 0.7rem;
color: var(--text-muted);
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 12px;
color: var(--text-muted);
text-decoration: none;
transition: all 0.2s;
margin-bottom: 4px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
}
.nav-item:hover,
.nav-item.active {
background: rgba(6, 182, 212, 0.1);
color: var(--primary);
}
.nav-item.active {
color: var(--text-light);
background: rgba(6, 182, 212, 0.15);
}
.nav-item i {
font-size: 1.1rem;
width: 20px;
text-align: center;
}
.sidebar-footer {
margin-top: auto;
padding-top: 16px;
border-top: 1px solid var(--glass-border);
}
.main-content {
margin-left: 260px;
padding: 32px;
min-height: 100vh;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
flex-wrap: wrap;
gap: 16px;
}
.page-header h1 {
font-size: 1.6rem;
font-weight: 800;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.stat-card {
background: var(--bg-card);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 24px;
transition: all 0.3s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.stat-label {
color: var(--text-muted);
font-size: 0.8rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 8px;
}
.stat-value {
font-size: 1.8rem;
font-weight: 800;
}
.card-section {
background: var(--bg-card);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
}
.card-section h3 {
font-size: 1rem;
font-weight: 700;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.card-section h3 i {
color: var(--primary);
}
.table-custom {
color: var(--text-light);
font-size: 0.85rem;
}
.table-custom th {
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
border-color: var(--glass-border);
padding: 12px;
}
.table-custom td {
border-color: var(--glass-border);
padding: 12px;
vertical-align: middle;
}
.table-custom tbody tr:hover {
background: rgba(255, 255, 255, 0.03);
}
.form-custom .form-control,
.form-custom .form-select {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--glass-border);
border-radius: 10px;
color: var(--text-light);
padding: 10px 14px;
}
.form-custom .form-control:focus,
.form-custom .form-select:focus {
background: rgba(255, 255, 255, 0.08);
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.15);
color: var(--text-light);
}
.form-custom label {
color: var(--text-muted);
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 4px;
}
.btn-action {
border: none;
border-radius: 8px;
padding: 8px 16px;
font-weight: 600;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-add {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
}
.btn-add:hover {
transform: translateY(-1px);
box-shadow: 0 5px 15px rgba(6, 182, 212, 0.3);
color: white;
}
.btn-del {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
color: var(--danger);
}
.btn-del:hover {
background: rgba(239, 68, 68, 0.2);
color: white;
}
.tab-buttons {
display: flex;
gap: 8px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.tab-btn {
padding: 10px 20px;
border-radius: 10px;
border: 1px solid var(--glass-border);
background: transparent;
color: var(--text-muted);
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.tab-btn:hover {
border-color: var(--primary);
color: var(--primary);
}
.tab-btn.active {
background: rgba(6, 182, 212, 0.15);
border-color: var(--primary);
color: var(--text-light);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 200;
}
.badge-status {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 0.7rem;
font-weight: 600;
}
.badge-active {
background: rgba(16, 185, 129, 0.15);
color: var(--success);
}
.badge-inactive {
background: rgba(239, 68, 68, 0.15);
color: var(--danger);
}
@media (max-width: 768px) {
.sidebar {
display: none;
}
.main-content {
margin-left: 0;
}
}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-brand">
<div class="icon"><i class="bi bi-cloud-fill"></i></div>
<div>
<h3>Sistema Nuvem</h3>
<span>Promoções & Sorteio</span>
</div>
</div>
<a class="nav-item active" href="#dashboard" data-tab="dashboard"><i class="bi bi-speedometer2"></i> Dashboard</a>
<a class="nav-item" href="#settings" data-tab="settings" style="color:var(--cyan-400); border: 1px solid rgba(6,182,212,0.2); background: rgba(6,182,212,0.05);"><i class="bi bi-gear-fill"></i> Configurações (Asaas)</a>
<a class="nav-item" href="#promotions" data-tab="promotions"><i class="bi bi-tag"></i> Promoções</a>
<a class="nav-item" href="#raffle" data-tab="raffle"><i class="bi bi-ticket-perforated"></i> Sorteio</a>
<a class="nav-item" href="#leads" data-tab="leads"><i class="bi bi-people"></i> Leads WiFi</a>
<a class="nav-item" href="#admins" data-tab="admins"><i class="bi bi-person-gear"></i> Usuários Admin</a>
<a class="nav-item" href="#plugins" data-tab="plugins"><i class="bi bi-puzzle"></i> Plugins</a>
<a href="http://alemaoconveniencias.local:4001/" class="nav-item" target="_blank"><i class="bi bi-globe"></i> Ver Landing Page</a>
<div class="sidebar-footer">
<a href="/admin/logout" class="nav-item"><i class="bi bi-box-arrow-left"></i> Sair</a>
</div>
</div>
<div class="main-content">
<!-- Dashboard Tab -->
<div class="tab-content active" id="tab-dashboard">
<div class="page-header">
<h1>☁️ Dashboard Nuvem</h1>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Leads WiFi</div>
<div class="stat-value" id="statLeads" style="color:var(--primary)">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Promoções Ativas</div>
<div class="stat-value" id="statPromos" style="color:var(--warning)">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Números Vendidos</div>
<div class="stat-value" id="statTickets" style="color:var(--success)">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Receita Vendas</div>
<div class="stat-value" id="statRevenue" style="color:var(--warning)">R$ 0</div>
</div>
</div>
<!-- Gráficos de Analytics -->
<div class="row g-4 mb-4">
<div class="col-md-6">
<div class="card-section h-100">
<h3><i class="bi bi-graph-up"></i> Receita por Dia (Últimos 30 Dias)</h3>
<canvas id="salesByDayChart" height="250"></canvas>
</div>
</div>
<div class="col-md-6">
<div class="card-section h-100">
<h3><i class="bi bi-clock-history"></i> Vendas por Horário (Picos)</h3>
<canvas id="salesByHourChart" height="250"></canvas>
</div>
</div>
<div class="col-12">
<div class="card-section">
<h3><i class="bi bi-pie-chart"></i> Desempenho por Sorteio</h3>
<canvas id="salesByRaffleChart" height="200"></canvas>
</div>
</div>
</div>
</div>
<!-- Promotions Tab -->
<div class="tab-content" id="tab-promotions">
<div class="page-header">
<h1>🏷️ Promoções</h1>
<button class="btn btn-action btn-add" onclick="togglePromoForm()">
<i class="bi bi-plus-lg me-1"></i> Nova Promoção
</button>
</div>
<!-- Add Promotion Form -->
<div class="card-section form-custom" id="promoForm" style="display:none">
<h3><i class="bi bi-plus-circle"></i> Cadastrar Promoção</h3>
<div class="row g-3">
<div class="col-md-4">
<label for="promoNome">Nome do Produto *</label>
<input type="text" class="form-control" id="promoNome" placeholder="Ex: Coca-Cola 2L">
</div>
<div class="col-md-4">
<label for="promoDesc">Descrição</label>
<input type="text" class="form-control" id="promoDesc" placeholder="Descrição do produto">
</div>
<div class="col-md-2">
<label for="promoPrecoOrig">Preço Original</label>
<input type="number" class="form-control" id="promoPrecoOrig" step="0.01" placeholder="0.00">
</div>
<div class="col-md-2">
<label for="promoPrecoPromo">Preço Promo *</label>
<input type="number" class="form-control" id="promoPrecoPromo" step="0.01" placeholder="0.00">
</div>
<div class="col-12">
<button class="btn btn-action btn-add" onclick="createPromotion()">
<i class="bi bi-check-lg me-1"></i> Salvar Promoção
</button>
</div>
</div>
</div>
<!-- Promotions Table -->
<div class="card-section">
<div class="table-responsive">
<table class="table table-custom mb-0">
<thead>
<tr>
<th>#</th>
<th>Produto</th>
<th>Descrição</th>
<th>Original</th>
<th>Promocional</th>
<th>Status</th>
<th>Ações</th>
</tr>
</thead>
<tbody id="promosTable">
<tr>
<td colspan="7" class="text-center py-4" style="color:var(--text-muted)">Carregando...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Raffle Tab -->
<div class="tab-content" id="tab-raffle">
<div class="page-header">
<h1>🎟️ Sorteio</h1>
<button class="btn btn-action btn-add" onclick="toggleRaffleForm()">
<i class="bi bi-plus-lg me-1"></i> Novo Sorteio
</button>
</div>
<!-- New Raffle Form -->
<div class="card-section form-custom" id="raffleForm" style="display:none">
<h3><i class="bi bi-plus-circle"></i> Criar Sorteio</h3>
<div class="row g-3">
<div class="col-md-4">
<label for="raffleTitulo">Título *</label>
<input type="text" class="form-control" id="raffleTitulo">
</div>
<div class="col-md-4">
<label for="raffleDesc">Descrição</label>
<input type="text" class="form-control" id="raffleDesc">
</div>
<div class="col-md-2">
<label for="rafflePreco">Preço/Número</label>
<input type="number" class="form-control" id="rafflePreco" step="0.01" value="5.00">
</div>
<div class="col-md-2">
<label for="raffleTotal">Total Números</label>
<input type="number" class="form-control" id="raffleTotal" value="500">
</div>
<div class="col-12">
<button class="btn btn-action btn-add" onclick="createRaffle()">
<i class="bi bi-check-lg me-1"></i> Criar Sorteio
</button>
</div>
</div>
</div>
<!-- Sold Tickets -->
<div class="card-section">
<h3><i class="bi bi-ticket-perforated"></i> Números Vendidos</h3>
<div class="table-responsive">
<table class="table table-custom mb-0">
<thead>
<tr>
<th>Número</th>
<th>Sorteio</th>
<th>Comprador</th>
<th>Telefone</th>
<th>CPF</th>
<th>Data</th>
<th>Ações</th>
</tr>
</thead>
<tbody id="ticketsTable">
<tr>
<td colspan="7" class="text-center py-4" style="color:var(--text-muted)">Carregando...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Leads Tab -->
<div class="tab-content" id="tab-leads">
<div class="page-header">
<h1>📡 Leads WiFi</h1>
</div>
<div class="card-section">
<div class="table-responsive">
<table class="table table-custom mb-0">
<thead>
<tr>
<th>#</th>
<th>Nome</th>
<th>CPF</th>
<th>Telefone</th>
<th>IP</th>
<th>Data</th>
<th>Ações</th>
</tr>
</thead>
<tbody id="leadsTable">
<tr>
<td colspan="7" class="text-center py-4" style="color:var(--text-muted)">Carregando...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Admins Tab -->
<div class="tab-content" id="tab-admins">
<div class="page-header">
<h1>👤 Gerenciamento de Usuários Admin</h1>
<button class="btn btn-action btn-add" onclick="toggleAdminForm()">
<i class="bi bi-person-plus me-1"></i> Novo Administrador
</button>
</div>
<!-- Add Admin Form -->
<div class="card-section form-custom" id="adminForm" style="display:none">
<h3><i class="bi bi-person-plus"></i> Cadastrar Administrador</h3>
<div class="row g-3">
<div class="col-md-4">
<label for="adminUsername">Nome de Usuário *</label>
<input type="text" class="form-control" id="adminUsername" placeholder="Ex: joao123"
minlength="3">
</div>
<div class="col-md-4">
<label for="adminPassword">Senha *</label>
<div class="position-relative">
<input type="password" class="form-control" id="adminPassword"
placeholder="Mínimo 4 caracteres" minlength="4">
</div>
</div>
<div class="col-md-4">
<label for="adminPasswordConfirm">Confirmar Senha *</label>
<input type="password" class="form-control" id="adminPasswordConfirm"
placeholder="Repita a senha">
</div>
<div class="col-12">
<button class="btn btn-action btn-add" onclick="createAdmin()">
<i class="bi bi-check-lg me-1"></i> Criar Administrador
</button>
</div>
</div>
</div>
<!-- Admins Table -->
<div class="card-section">
<h3><i class="bi bi-shield-check"></i> Administradores Cadastrados</h3>
<div class="table-responsive">
<table class="table table-custom mb-0">
<thead>
<tr>
<th>ID</th>
<th>Usuário</th>
<th>E-mail</th>
<th>Telefone</th>
<th>Status</th>
<th>Ações</th>
</tr>
</thead>
<tbody id="adminsTable">
<tr>
<td colspan="4" class="text-center py-4" style="color:var(--text-muted)">Carregando...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- CONFIGURAÇÕES -->
<div id="tab-settings" class="tab-content" style="display: none;">
<div class="page-header">
<div>
<h1>⚙️ Configurações Globais</h1>
<p class="text-muted">Integrações de Gateways e Variáveis de Ambiente</p>
</div>
</div>
<div class="card-section">
<h3><i class="bi bi-bank"></i> Integração Asaas (PIX)</h3>
<p class="text-muted" style="font-size: 0.9rem; margin-bottom: 20px;">A chave de API é necessária para gerar cobranças Pix instantâneas.</p>
<form id="settingsForm">
<div class="mb-3">
<label class="form-label" for="settingAsaasKey">ASAAS API KEY</label>
<input type="password" class="form-control" id="settingAsaasKey" style="background: rgba(255,255,255,0.05); color:white; border: 1px solid rgba(255,255,255,0.1);" placeholder="Ex: $aact_YTU5YTE0M2M...">
</div>
<button type="submit" class="btn btn-primary-custom" id="btnSaveSettings">Salvar Configurações</button>
<span id="settingsAlert" class="ms-3 text-success d-none"><i class="bi bi-check-circle"></i> Salvo com sucesso!</span>
</form>
</div>
<div class="card-section mt-4">
<h3><i class="bi bi-whatsapp"></i> NewWhats (Para Notificações)</h3>
<p class="text-muted" style="font-size: 0.9rem; margin-bottom: 20px;">O backend do Alemão usa as variáveis de ambiente NEWWHATS_API_URL e NEWWHATS_API_TOKEN definidas no arquivo .env</p>
</div>
</div>
<!-- Plugins Tab -->
<div class="tab-content" id="tab-plugins">
<div class="page-header">
<h1><i class="bi bi-puzzle me-2" style="color:var(--primary)"></i>Plugins</h1>
</div>
<div id="plugin-list"></div>
</div>
</div><!-- /main-content -->
<!-- NewWhats Pairing Modal -->
<div class="modal fade" id="pairModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="background:var(--bg-card);border:1px solid var(--glass-border);border-radius:20px;color:var(--text-light)">
<div class="modal-header" style="border-bottom:1px solid var(--glass-border);padding:20px 24px">
<h5 class="modal-title" style="font-weight:700"><i class="bi bi-link-45deg me-2"></i>Parear com NewWhats</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" style="filter:invert(1)"></button>
</div>
<div class="modal-body form-custom" style="padding:24px">
<div id="pair-step1">
<p style="color:var(--text-muted);font-size:.875rem;margin-bottom:16px">
Informe o e-mail da sua conta NewWhats. Um código será enviado para confirmação.
</p>
<div class="mb-3">
<label class="form-label">URL do servidor NewWhats</label>
<input type="text" class="form-control" id="pair-nw-url" placeholder="http://newwhats.local:8008">
</div>
<div class="mb-3">
<label class="form-label">E-mail da conta NewWhats</label>
<input type="email" class="form-control" id="pair-email" placeholder="seu@email.com">
</div>
<div id="pair-error1" style="color:#f87171;font-size:.82rem;display:none;margin-top:8px"></div>
</div>
<div id="pair-step2" style="display:none">
<p id="pair-step2-desc" style="color:var(--text-muted);font-size:.875rem;margin-bottom:16px">
Código enviado para <strong id="pair-email-label"></strong>. Digite abaixo:
</p>
<div class="mb-3">
<label class="form-label">Código de confirmação</label>
<input type="text" class="form-control" id="pair-code" maxlength="6" placeholder="000000"
style="font-size:1.5rem;letter-spacing:.4em;text-align:center">
</div>
<div id="pair-error2" style="color:#f87171;font-size:.82rem;display:none;margin-top:8px"></div>
</div>
<div id="pair-step3" style="display:none;text-align:center">
<div style="font-size:3rem;margin-bottom:12px"></div>
<p style="font-weight:700;margin-bottom:4px">Pareamento concluído!</p>
<p style="color:var(--text-muted);font-size:.875rem">A chave de integração foi salva automaticamente.</p>
</div>
</div>
<div class="modal-footer" style="border-top:1px solid var(--glass-border);padding:16px 24px">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" style="border-radius:10px">Cancelar</button>
<button type="button" class="btn btn-action btn-add" id="pair-btn-next" onclick="pairNext()">
Enviar código <i class="bi bi-arrow-right ms-1"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Edit Admin Modal -->
<div class="modal fade" id="editAdminModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content"
style="background:var(--bg-card);border:1px solid var(--glass-border);border-radius:20px;color:var(--text-light)">
<div class="modal-header" style="border-bottom:1px solid var(--glass-border);padding:20px 24px">
<h5 class="modal-title" style="font-weight:700"><i class="bi bi-pencil-square me-2"></i>Editar
Administrador</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" style="filter:invert(1)"></button>
</div>
<div class="modal-body form-custom" style="padding:24px">
<input type="hidden" id="editAdminId">
<div class="row g-3">
<div class="col-12">
<label class="form-label">Nome completo</label>
<input type="text" class="form-control" id="editAdminNome" placeholder="Ex: João da Silva">
</div>
<div class="col-md-6">
<label class="form-label">E-mail</label>
<input type="email" class="form-control" id="editAdminEmail" placeholder="admin@email.com">
</div>
<div class="col-md-6">
<label class="form-label">Telefone</label>
<input type="text" class="form-control" id="editAdminTelefone" placeholder="(00) 00000-0000">
</div>
<div class="col-12">
<hr style="border-color:var(--glass-border);margin:4px 0">
</div>
<div class="col-md-6">
<label class="form-label">Nome de usuário</label>
<input type="text" class="form-control" id="editAdminUsername">
</div>
<div class="col-md-6"></div>
<div class="col-md-6">
<label class="form-label">Nova senha <small style="color:var(--text-muted)">(opcional)</small></label>
<input type="password" class="form-control" id="editAdminPassword" placeholder="Deixe vazio para manter">
</div>
<div class="col-md-6">
<label class="form-label">Confirmar nova senha</label>
<input type="password" class="form-control" id="editAdminPasswordConfirm" placeholder="Repita a senha">
</div>
</div>
</div>
<div class="modal-footer" style="border-top:1px solid var(--glass-border);padding:16px 24px">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
style="border-radius:10px">Cancelar</button>
<button type="button" class="btn btn-action btn-add" onclick="updateAdmin()">
<i class="bi bi-check-lg me-1"></i> Salvar Alterações
</button>
</div>
</div>
</div>
</div>
<div class="toast-container">
<div id="toast" class="toast" role="alert">
<div class="toast-body" id="toastBody"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
function showToast(msg, type = 'success') {
const t = document.getElementById('toast');
const tb = document.getElementById('toastBody');
tb.textContent = msg;
t.className = 'toast show ' + (type === 'success' ? 'bg-success' : 'bg-danger');
t.style.color = 'white';
setTimeout(() => t.className = 'toast', 3000);
}
function formatCPF(cpf) {
if (!cpf || cpf.length !== 11) return cpf || '-';
return cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
}
function formatPhone(p) {
if (!p) return '-';
p = p.replace(/\D/g, '');
if (p.length === 11) return p.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3');
return p;
}
function formatDate(d) {
if (!d) return '-';
const dt = new Date(d);
return dt.toLocaleDateString('pt-BR') + ' ' + dt.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
}
// Tabs — hash-based routing
const VALID_TABS = ['dashboard', 'promotions', 'raffle', 'leads', 'admins', 'settings', 'plugins'];
function showTab(tab) {
if (!VALID_TABS.includes(tab)) tab = 'dashboard';
document.querySelectorAll('.tab-content').forEach(t => t.style.display = 'none');
document.querySelectorAll('.nav-item[data-tab]').forEach(n => n.classList.remove('active'));
const targetEl = document.getElementById('tab-' + tab);
if (targetEl) {
targetEl.style.display = 'block';
// Need a tiny delay for structural browser repaints specifically when using display block initially before adding animation. We just toggle block immediately.
}
const navLink = document.querySelector(`.nav-item[data-tab="${tab}"]`);
if (navLink) navLink.classList.add('active');
if (tab === 'promotions') loadPromotions();
if (tab === 'raffle') loadTickets();
if (tab === 'leads') loadLeads();
if (tab === 'dashboard') loadDashboard();
if (tab === 'admins') loadAdmins();
if (tab === 'settings') loadSettings();
if (tab === 'plugins') loadPlugins();
}
// Hash routing: read tab from URL hash
function getTabFromHash() {
const hash = window.location.hash.replace('#', '').trim();
return VALID_TABS.includes(hash) ? hash : 'dashboard';
}
// Nav clicks update hash (href="#tab" already does this) — intercept to call showTab
document.querySelectorAll('.nav-item[data-tab]').forEach(link => {
link.addEventListener('click', function (e) {
e.preventDefault();
const tab = this.dataset.tab;
history.pushState(null, '', '#' + tab);
showTab(tab);
});
});
// Back / forward button
window.addEventListener('popstate', () => showTab(getTabFromHash()));
// Initial load
showTab(getTabFromHash());
let chartsInstance = {};
async function loadDashboard() {
try {
const [usersRes, promosRes, analyticsRes] = await Promise.all([
fetch('/api/users'),
fetch('/api/promotions/all'), // Updated route from backend schema
fetch('/api/analytics/dashboard')
]);
const users = await usersRes.json();
const promos = await promosRes.json();
// Tratar Analytics (Auth failure or empty payload fallback)
let analyticsData = { revenueStats: {}, salesByDay: [], salesByHour: [], salesByRaffle: [] };
if (analyticsRes.ok) {
const contentType = analyticsRes.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
analyticsData = await analyticsRes.json();
} else {
throw new Error("Sessão expirada. Redirecionando...");
}
} else {
throw new Error("Sessão expirada. Redirecionando...");
}
const { revenueStats, salesByDay, salesByHour, salesByRaffle } = analyticsData;
document.getElementById('statLeads').textContent = users.length;
document.getElementById('statPromos').textContent = promos.length || 0;
document.getElementById('statTickets').textContent = revenueStats?.total_tickets_sold || 0;
document.getElementById('statRevenue').textContent = 'R$ ' + parseFloat(revenueStats?.total_revenue || 0).toFixed(2);
// Renderizar gráficos com Chart.js
if (typeof Chart === 'undefined') return;
Chart.defaults.color = '#94a3b8';
Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.08)';
if (chartsInstance.chartDay) chartsInstance.chartDay.destroy();
if (chartsInstance.chartHour) chartsInstance.chartHour.destroy();
if (chartsInstance.chartRaffle) chartsInstance.chartRaffle.destroy();
const ctxDay = document.getElementById('salesByDayChart')?.getContext('2d');
if (ctxDay && Array.isArray(salesByDay)) {
chartsInstance.chartDay = new Chart(ctxDay, {
type: 'line',
data: {
labels: salesByDay.map(d => formatDate(d.date).split(' ')[0]),
datasets: [{
label: 'Receita (R$)',
data: salesByDay.map(d => parseFloat(d.revenue || 0)),
borderColor: '#06b6d4',
backgroundColor: 'rgba(6, 182, 212, 0.1)',
fill: true,
tension: 0.4
}]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
const ctxHour = document.getElementById('salesByHourChart')?.getContext('2d');
if (ctxHour && Array.isArray(salesByHour)) {
chartsInstance.chartHour = new Chart(ctxHour, {
type: 'bar',
data: {
labels: salesByHour.map(h => h.hour_of_day + 'h'),
datasets: [{
label: 'Tickets Vendidos',
data: salesByHour.map(h => parseInt(h.tickets_sold || 0)),
backgroundColor: '#10b981',
borderRadius: 4
}]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
const ctxRaffle = document.getElementById('salesByRaffleChart')?.getContext('2d');
if (ctxRaffle && Array.isArray(salesByRaffle)) {
chartsInstance.chartRaffle = new Chart(ctxRaffle, {
type: 'bar',
data: {
labels: salesByRaffle.map(r => r.raffle_name),
datasets: [{
label: 'Receita (R$)',
data: salesByRaffle.map(r => parseFloat(r.revenue || 0)),
backgroundColor: '#f59e0b',
borderRadius: 4
}]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
} catch (err) {
console.error('Error loading dashboard:', err);
if (err.message.includes('Sessão expirada')) {
window.location.href = '/admin';
}
}
}
// --- Settings Management ---
async function loadSettings() {
try {
const res = await fetch('/api/settings/asaas');
const data = await res.json();
if (data.key) {
document.getElementById('settingAsaasKey').value = data.key;
}
} catch (e) { console.error('Erro ao buscar configs:', e); }
}
document.getElementById('settingsForm')?.addEventListener('submit', async function(e) {
e.preventDefault();
const btn = document.getElementById('btnSaveSettings');
btn.innerHTML = 'Salvando...';
btn.disabled = true;
try {
const asaasKey = document.getElementById('settingAsaasKey').value.trim();
const res = await fetch('/api/settings/asaas', {
method: 'POST',
headers: { 'Content-Type':'application/json' },
body: JSON.stringify({ key: asaasKey })
});
if (res.ok) {
const alert = document.getElementById('settingsAlert');
alert.classList.remove('d-none');
setTimeout(() => alert.classList.add('d-none'), 3000);
}
} catch (error) {
console.error('Erro ao salvar config', error);
} finally {
btn.innerHTML = 'Salvar Configurações';
btn.disabled = false;
}
});
// Promotions
function togglePromoForm() {
const f = document.getElementById('promoForm');
f.style.display = f.style.display === 'none' ? 'block' : 'none';
}
async function loadPromotions() {
try {
const res = await fetch('/api/promotions/all');
const promos = await res.json();
const tbody = document.getElementById('promosTable');
if (promos.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4" style="color:var(--text-muted)">Nenhuma promoção</td></tr>';
return;
}
tbody.innerHTML = promos.map(p => `
<tr>
<td>${p.id}</td>
<td><strong>${p.nome}</strong></td>
<td>${p.descricao || '-'}</td>
<td>${p.preco_original ? 'R$ ' + parseFloat(p.preco_original).toFixed(2) : '-'}</td>
<td style="color:var(--success);font-weight:700">R$ ${parseFloat(p.preco_promocional).toFixed(2)}</td>
<td><span class="badge-status ${p.ativo ? 'badge-active' : 'badge-inactive'}">${p.ativo ? 'Ativa' : 'Inativa'}</span></td>
<td><button class="btn btn-action btn-del" onclick="deletePromo(${p.id})"><i class="bi bi-trash"></i></button></td>
</tr>
`).join('');
} catch (err) { console.error(err); }
}
async function createPromotion() {
const nome = document.getElementById('promoNome').value.trim();
const preco_promocional = document.getElementById('promoPrecoPromo').value;
if (!nome || !preco_promocional) {
showToast('Preencha nome e preço promocional', 'error');
return;
}
try {
const res = await fetch('/api/promotions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
nome,
descricao: document.getElementById('promoDesc').value.trim(),
preco_original: document.getElementById('promoPrecoOrig').value || null,
preco_promocional
})
});
const data = await res.json();
if (data.success) {
showToast('Promoção criada!');
document.getElementById('promoNome').value = '';
document.getElementById('promoDesc').value = '';
document.getElementById('promoPrecoOrig').value = '';
document.getElementById('promoPrecoPromo').value = '';
loadPromotions();
} else showToast(data.error, 'error');
} catch (err) { showToast('Erro ao criar', 'error'); }
}
async function deletePromo(id) {
if (!confirm('Remover esta promoção?')) return;
try {
await fetch('/api/promotions/' + id, { method: 'DELETE' });
showToast('Promoção removida');
loadPromotions();
} catch (err) { showToast('Erro', 'error'); }
}
// Raffle
function toggleRaffleForm() {
const f = document.getElementById('raffleForm');
f.style.display = f.style.display === 'none' ? 'block' : 'none';
}
async function createRaffle() {
const titulo = document.getElementById('raffleTitulo').value.trim();
if (!titulo) { showToast('Digite o título', 'error'); return; }
try {
const res = await fetch('/api/raffle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
titulo,
descricao: document.getElementById('raffleDesc').value.trim(),
preco_numero: document.getElementById('rafflePreco').value,
total_numeros: document.getElementById('raffleTotal').value
})
});
const data = await res.json();
if (data.success) {
showToast('Sorteio criado!');
loadTickets();
} else showToast(data.error, 'error');
} catch (err) { showToast('Erro', 'error'); }
}
async function loadTickets() {
try {
const res = await fetch('/api/raffle/tickets');
const tickets = await res.json();
const tbody = document.getElementById('ticketsTable');
if (!Array.isArray(tickets)) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4" style="color:#f87171">Erro ao carregar tickets</td></tr>';
return;
}
if (tickets.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4" style="color:var(--text-muted)">Nenhum número vendido</td></tr>';
return;
}
tbody.innerHTML = tickets.map(t => `
<tr>
<td><strong style="color:var(--warning)">#${String(t.numero).padStart(3, '0')}</strong></td>
<td>${t.raffle_titulo || '-'}</td>
<td><strong>${t.usuario_nome}</strong></td>
<td>${formatPhone(t.usuario_telefone)}</td>
<td>${formatCPF(t.usuario_cpf)}</td>
<td>${formatDate(t.data_compra)}</td>
<td><button class="btn btn-action btn-del" onclick="deleteTicket(${t.id})"><i class="bi bi-trash"></i></button></td>
</tr>
`).join('');
} catch (err) { console.error(err); }
}
async function deleteTicket(id) {
if (!confirm('Remover este ticket?')) return;
try {
await fetch('/api/raffle/ticket/' + id, { method: 'DELETE' });
showToast('Ticket removido');
loadTickets();
} catch (err) { showToast('Erro', 'error'); }
}
// Leads
async function loadLeads() {
try {
const res = await fetch('/api/users');
const users = await res.json();
const tbody = document.getElementById('leadsTable');
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4" style="color:var(--text-muted)">Nenhum lead recebido</td></tr>';
return;
}
tbody.innerHTML = users.map(u => `
<tr>
<td>${u.id}</td>
<td><strong>${u.nome}</strong></td>
<td>${formatCPF(u.cpf)}</td>
<td>${formatPhone(u.telefone)}</td>
<td><code style="color:var(--info)">${u.ip || '-'}</code></td>
<td>${formatDate(u.timestamp)}</td>
<td><button class="btn btn-action btn-del" onclick="deleteLead(${u.id})"><i class="bi bi-trash"></i></button></td>
</tr>
`).join('');
} catch (err) { console.error(err); }
}
async function deleteLead(id) {
if (!confirm('Remover este lead?')) return;
try {
await fetch('/api/users/' + id, { method: 'DELETE' });
showToast('Lead removido');
loadLeads();
} catch (err) { showToast('Erro', 'error'); }
}
// Admin Users
function toggleAdminForm() {
const f = document.getElementById('adminForm');
f.style.display = f.style.display === 'none' ? 'block' : 'none';
}
async function loadAdmins() {
try {
const res = await fetch('/api/admins');
if (res.status === 401) return window.location.href = '/admin';
const admins = await res.json();
const tbody = document.getElementById('adminsTable');
if (admins.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4" style="color:var(--text-muted)">Nenhum administrador</td></tr>';
return;
}
window._adminMap = {}
tbody.innerHTML = admins.map(a => {
window._adminMap[a.id] = a
return `
<tr>
<td>${a.id}</td>
<td>
<div style="display:flex;align-items:center;gap:10px">
<div style="width:36px;height:36px;background:linear-gradient(135deg,var(--primary),var(--primary-dark));border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:14px;color:white;font-weight:700;flex-shrink:0">
${(a.nome || a.username).charAt(0).toUpperCase()}
</div>
<div>
<div style="font-weight:700">${a.nome || a.username}</div>
<div style="font-size:.75rem;color:var(--text-muted)">@${a.username}</div>
</div>
</div>
</td>
<td style="color:var(--text-muted);font-size:.85rem">${a.email || '-'}</td>
<td style="color:var(--text-muted);font-size:.85rem">${formatPhone(a.telefone) || '-'}</td>
<td><span class="badge-status badge-active">Ativo</span></td>
<td>
<div style="display:flex;gap:8px">
<button class="btn btn-action" style="background:rgba(99,102,241,0.1);border:1px solid rgba(99,102,241,0.2);color:#6366f1"
onclick="editAdmin(${a.id})">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-action btn-del" onclick="deleteAdmin(${a.id}, '${a.username}')">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>`
}).join('');
} catch (err) { console.error(err); }
}
async function createAdmin() {
const username = document.getElementById('adminUsername').value.trim();
const password = document.getElementById('adminPassword').value;
const passwordConfirm = document.getElementById('adminPasswordConfirm').value;
if (!username || !password) {
showToast('Preencha usuário e senha', 'error');
return;
}
if (username.length < 3) {
showToast('Usuário deve ter no mínimo 3 caracteres', 'error');
return;
}
if (password.length < 4) {
showToast('Senha deve ter no mínimo 4 caracteres', 'error');
return;
}
if (password !== passwordConfirm) {
showToast('As senhas não coincidem', 'error');
return;
}
try {
const res = await fetch('/api/admins', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (data.success) {
showToast('Administrador criado com sucesso!');
document.getElementById('adminUsername').value = '';
document.getElementById('adminPassword').value = '';
document.getElementById('adminPasswordConfirm').value = '';
document.getElementById('adminForm').style.display = 'none';
loadAdmins();
} else {
showToast(data.error, 'error');
}
} catch (err) { showToast('Erro ao criar administrador', 'error'); }
}
function editAdmin(id) {
const a = window._adminMap[id] || {}
document.getElementById('editAdminId').value = id;
document.getElementById('editAdminUsername').value = a.username || '';
document.getElementById('editAdminNome').value = a.nome || '';
document.getElementById('editAdminEmail').value = a.email || '';
document.getElementById('editAdminTelefone').value = a.telefone || '';
document.getElementById('editAdminPassword').value = '';
document.getElementById('editAdminPasswordConfirm').value = '';
new bootstrap.Modal(document.getElementById('editAdminModal')).show();
}
async function updateAdmin() {
const id = document.getElementById('editAdminId').value;
const username = document.getElementById('editAdminUsername').value.trim();
const nome = document.getElementById('editAdminNome').value.trim();
const email = document.getElementById('editAdminEmail').value.trim();
const telefone = document.getElementById('editAdminTelefone').value.trim();
const password = document.getElementById('editAdminPassword').value;
const passwordConfirm = document.getElementById('editAdminPasswordConfirm').value;
if (!username) { showToast('Nome de usuário é obrigatório', 'error'); return; }
if (password && password !== passwordConfirm) { showToast('As senhas não coincidem', 'error'); return; }
if (password && password.length < 4) { showToast('Senha deve ter no mínimo 4 caracteres', 'error'); return; }
try {
const body = { username, nome, email, telefone };
if (password) body.password = password;
const res = await fetch('/api/admins/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
if (data.success) {
showToast('Administrador atualizado!');
bootstrap.Modal.getInstance(document.getElementById('editAdminModal')).hide();
loadAdmins();
} else {
showToast(data.error, 'error');
}
} catch (err) { showToast('Erro ao atualizar', 'error'); }
}
async function deleteAdmin(id, username) {
if (!confirm('Tem certeza que deseja remover o administrador "' + username + '"?')) return;
try {
const res = await fetch('/api/admins/' + id, { method: 'DELETE' });
const data = await res.json();
if (data.success) {
showToast('Administrador removido');
loadAdmins();
} else {
showToast(data.error, 'error');
}
} catch (err) { showToast('Erro ao remover', 'error'); }
}
// ── Plugins ───────────────────────────────────────────────────────────
async function loadPlugins() {
const container = document.getElementById('plugin-list')
try {
const res = await fetch('/api/plugins', { credentials: 'include' })
const plugins = await res.json()
if (!Array.isArray(plugins) || plugins.length === 0) {
container.innerHTML = '<p style="color:var(--text-muted)">Nenhum plugin encontrado.</p>'
return
}
container.innerHTML = plugins.map(p => {
const cfgRows = (p.config_schema || []).map(f => `
<div class="mb-3">
<label class="form-label">${f.label}</label>
<input type="${f.type === 'password' ? 'password' : 'text'}"
class="form-control plugin-cfg-field"
data-key="${f.key}"
placeholder="${f.help || ''}"
id="cfg-${p.id}-${f.key}">
${f.help ? `<div style="color:var(--text-muted);font-size:.75rem;margin-top:4px">${f.help}</div>` : ''}
</div>`).join('')
const pairBtn = p.id === 'newwhats' ? `
<button class="btn btn-action" onclick="openPairModal()"
style="background:rgba(6,182,212,.15);border:1px solid rgba(6,182,212,.3);color:var(--primary);margin-bottom:16px;width:100%">
<i class="bi bi-link-45deg me-1"></i> Parear com NewWhats (gerar chave automaticamente)
</button>` : ''
return `
<div class="card-section" style="margin-bottom:20px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:12px">
<div>
<h3 style="margin:0 0 4px"><i class="bi bi-puzzle me-2" style="color:var(--primary)"></i>${p.name}</h3>
<span style="color:var(--text-muted);font-size:.8rem">v${p.version}${p.description ? ' — ' + p.description : ''}</span>
</div>
<div style="display:flex;align-items:center;gap:10px">
<span class="badge-status ${p.active ? 'badge-active' : 'badge-inactive'}" id="badge-${p.id}">
${p.active ? 'Ativo' : 'Inativo'}
</span>
<button class="btn btn-action ${p.active ? 'btn-del' : 'btn-add'}" id="toggle-${p.id}"
onclick="togglePlugin('${p.id}', ${p.active})">
${p.active ? '<i class="bi bi-stop-circle me-1"></i>Desativar' : '<i class="bi bi-play-circle me-1"></i>Ativar'}
</button>
</div>
</div>
${cfgRows ? `
<hr style="border-color:var(--glass-border);margin:16px 0">
<h4 style="font-size:.85rem;font-weight:700;color:var(--text-muted);margin-bottom:16px;text-transform:uppercase;letter-spacing:.05em">Configuração</h4>
${pairBtn}
<div class="form-custom" id="cfg-form-${p.id}">${cfgRows}</div>
<button class="btn btn-action btn-add" style="margin-top:8px" onclick="savePluginConfig('${p.id}')">
<i class="bi bi-floppy me-1"></i> Salvar configuração
</button>` : ''}
</div>`
}).join('')
// Carrega valores atuais de configuração
for (const p of plugins) {
if (!p.config_schema?.length) continue
const cfgRes = await fetch(`/api/plugins/${p.id}/config`, { credentials: 'include' })
const cfg = await cfgRes.json()
for (const f of p.config_schema) {
const el = document.getElementById(`cfg-${p.id}-${f.key}`)
if (el && cfg[f.key]) el.value = cfg[f.key]
}
}
} catch (err) {
container.innerHTML = '<p style="color:#f87171">Erro ao carregar plugins.</p>'
}
}
async function togglePlugin(id, currentlyActive) {
const action = currentlyActive ? 'deactivate' : 'activate'
try {
const res = await fetch(`/api/plugins/${id}/${action}`, { method: 'POST', credentials: 'include' })
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Erro')
showToast(`Plugin ${currentlyActive ? 'desativado' : 'ativado'}!`)
loadPlugins()
} catch (err) {
showToast(err.message, 'error')
}
}
async function savePluginConfig(id) {
const fields = document.querySelectorAll(`#cfg-form-${id} .plugin-cfg-field`)
const values = {}
fields.forEach(f => { if (f.value.trim()) values[f.dataset.key] = f.value.trim() })
try {
const res = await fetch(`/api/plugins/${id}/config`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(values),
})
if (!res.ok) throw new Error('Erro ao salvar')
showToast('Configuração salva!')
} catch (err) {
showToast(err.message, 'error')
}
}
// ── NewWhats Pairing ──────────────────────────────────────────────────
let pairStep = 1
function openPairModal() {
pairStep = 1
document.getElementById('pair-step1').style.display = ''
document.getElementById('pair-step2').style.display = 'none'
document.getElementById('pair-step3').style.display = 'none'
document.getElementById('pair-error1').style.display = 'none'
document.getElementById('pair-error2').style.display = 'none'
document.getElementById('pair-btn-next').style.display = ''
document.getElementById('pair-btn-next').textContent = 'Enviar código'
document.getElementById('pair-btn-next').innerHTML = 'Enviar código <i class="bi bi-arrow-right ms-1"></i>'
document.getElementById('pair-code').value = ''
// Pré-preenche URL salva
const urlEl = document.getElementById(`cfg-newwhats-newwhats_url`)
if (urlEl && urlEl.value) document.getElementById('pair-nw-url').value = urlEl.value
new bootstrap.Modal(document.getElementById('pairModal')).show()
}
async function pairNext() {
if (pairStep === 1) {
const email = document.getElementById('pair-email').value.trim()
const nwUrl = document.getElementById('pair-nw-url').value.trim().replace(/\/$/, '')
const errEl = document.getElementById('pair-error1')
errEl.style.display = 'none'
if (!email || !nwUrl) {
errEl.textContent = 'Preencha a URL e o e-mail.'
errEl.style.display = ''
return
}
document.getElementById('pair-btn-next').disabled = true
try {
const res = await fetch('/api/pair/proxy/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, nwUrl }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Erro ao solicitar código')
document.getElementById('pair-step1').style.display = 'none'
document.getElementById('pair-step2').style.display = ''
document.getElementById('pair-btn-next').innerHTML = 'Confirmar <i class="bi bi-check-lg ms-1"></i>'
const descEl = document.getElementById('pair-step2-desc')
const warn = document.getElementById('pair-error2')
if (data.dev_code) {
descEl.style.display = 'none'
document.getElementById('pair-code').value = data.dev_code
warn.innerHTML = '<i class="bi bi-exclamation-triangle me-1"></i>SMTP não configurado — código gerado localmente e preenchido automaticamente. Clique em <strong>Confirmar</strong> para continuar.'
warn.style.color = '#f59e0b'
warn.style.display = ''
} else {
descEl.style.display = ''
document.getElementById('pair-email-label').textContent = email
warn.style.display = 'none'
}
pairStep = 2
} catch (err) {
errEl.textContent = err.message
errEl.style.display = ''
} finally {
document.getElementById('pair-btn-next').disabled = false
}
} else if (pairStep === 2) {
const email = document.getElementById('pair-email').value.trim()
const nwUrl = document.getElementById('pair-nw-url').value.trim().replace(/\/$/, '')
const code = document.getElementById('pair-code').value.trim()
const errEl = document.getElementById('pair-error2')
errEl.style.display = 'none'
if (code.length !== 6) {
errEl.textContent = 'Digite o código de 6 dígitos.'
errEl.style.display = ''
return
}
document.getElementById('pair-btn-next').disabled = true
try {
const res = await fetch('/api/pair/proxy/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, code, nwUrl }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Código inválido')
// Salva a chave e URL no config do plugin
await fetch('/api/plugins/newwhats/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ integration_key: data.integration_key, newwhats_url: nwUrl }),
})
document.getElementById('pair-step2').style.display = 'none'
document.getElementById('pair-step3').style.display = ''
document.getElementById('pair-btn-next').style.display = 'none'
loadPlugins()
pairStep = 3
} catch (err) {
errEl.textContent = err.message
errEl.style.display = ''
} finally {
document.getElementById('pair-btn-next').disabled = false
}
}
}
// Fix Bootstrap aria-hidden focus warning — blur focused element before modal hides
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('hide.bs.modal', () => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur()
}
})
})
// Init — handled by showTab(getTabFromHash()) above
</script>
</body>
</html>