1586 lines
70 KiB
HTML
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> |