anycoder-c8aae04a / index.html
eubottura's picture
Upload folder using huggingface_hub
bf8863e verified
<!DOCTYPE html>
<html lang="pt-BR" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UTM Sales Analytics Pro | Shopify</title>
<!-- Fonts: Inter for clean, modern readability -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Icons: Remix Icon -->
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
/* --- 1. DESIGN SYSTEM & VARIABLES --- */
:root {
/* Palette: Premium Dark (Zinc/Slate) */
--bg-body: #09090b; /* Zinc 950 */
--bg-card: #121214; /* Zinc 900 */
--bg-surface: #1c1c1f; /* Zinc 800 */
--bg-input: #09090b;
--border-subtle: #1f1f23;
--border-default: #2e2e33;
--border-hover: #3f3f46;
--text-main: #f4f4f5; /* Zinc 100 */
--text-muted: #a1a1aa; /* Zinc 400 */
--text-faint: #71717a; /* Zinc 500 */
--primary: #10b981; /* Emerald 500 */
--primary-hover: #059669;
--primary-dim: rgba(16, 185, 129, 0.1);
--primary-glow: rgba(16, 185, 129, 0.2);
--accent: #6366f1; /* Indigo */
--success-bg: rgba(16, 185, 129, 0.15);
--success-text: #34d399;
--warning-bg: rgba(245, 158, 11, 0.15);
--warning-text: #fbbf24;
--danger-bg: rgba(239, 68, 68, 0.15);
--danger-text: #f87171;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--font-main: 'Inter', sans-serif;
--shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.15);
--shadow-header: 0 4px 20px rgba(0,0,0,0.4);
--ease: cubic-bezier(0.16, 1, 0.3, 1);
}
[data-theme="light"] {
--bg-body: #f8fafc; /* Slate 50 */
--bg-card: #ffffff;
--bg-surface: #f1f5f9; /* Slate 100 */
--bg-input: #ffffff;
--border-subtle: #e2e8f0;
--border-default: #cbd5e1;
--border-hover: #94a3b8;
--text-main: #0f172a; /* Slate 900 */
--text-muted: #64748b; /* Slate 500 */
--text-faint: #94a3b8; /* Slate 400 */
--primary: #059669;
--primary-hover: #047857;
--primary-dim: rgba(5, 150, 105, 0.1);
--primary-glow: rgba(5, 150, 105, 0.15);
--shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
--shadow-header: 0 4px 20px rgba(0,0,0,0.05);
}
/* --- 2. RESET & BASE --- */
* { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
body {
font-family: var(--font-main);
background-color: var(--bg-body);
color: var(--text-main);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
transition: background-color 0.3s ease, color 0.3s ease;
padding-bottom: 60px;
}
a { text-decoration: none; color: inherit; transition: color 0.2s; }
ul { list-style: none; }
button { font-family: inherit; border: none; background: none; cursor: pointer; }
/* Scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-default); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
/* --- 3. LAYOUT UTILITIES --- */
.container { max-width: 1280px; margin: 0 auto; padding: 0 24px; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.justify-end { justify-content: flex-end; }
.justify-center { justify-content: center; }
.gap-2 { gap: 8px; }
.gap-4 { gap: 16px; }
.gap-6 { gap: 24px; }
.mt-4 { margin-top: 16px; }
.mb-4 { margin-bottom: 16px; }
.w-full { width: 100%; }
/* --- 4. COMPONENTS --- */
/* Header */
header {
position: sticky;
top: 0;
z-index: 50;
background: rgba(9, 9, 11, 0.8);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-subtle);
padding: 16px 0;
transition: background 0.3s;
}
[data-theme="light"] header { background: rgba(255, 255, 255, 0.85); }
.brand {
display: flex;
align-items: center;
gap: 12px;
font-weight: 700;
font-size: 1.25rem;
letter-spacing: -0.025em;
color: var(--text-main);
}
.brand i { color: var(--primary); font-size: 1.5rem; }
.anycoder-link {
font-size: 0.8rem;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 4px;
font-weight: 500;
}
.anycoder-link:hover { color: var(--primary); }
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 16px;
border-radius: var(--radius-sm);
font-weight: 500;
font-size: 0.875rem;
transition: all 0.2s var(--ease);
white-space: nowrap;
border: 1px solid transparent;
}
.btn-primary {
background-color: var(--primary);
color: #fff;
box-shadow: 0 0 0 1px var(--primary-glow), 0 4px 12px var(--primary-glow);
}
.btn-primary:hover:not(:disabled) {
background-color: var(--primary-hover);
transform: translateY(-1px);
}
.btn-secondary {
background-color: var(--bg-surface);
border-color: var(--border-default);
color: var(--text-main);
}
.btn-secondary:hover:not(:disabled) {
background-color: var(--border-subtle);
border-color: var(--text-faint);
}
.btn-ghost {
color: var(--text-muted);
}
.btn-ghost:hover {
color: var(--text-main);
background-color: var(--bg-surface);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
box-shadow: none !important;
}
/* Cards */
.card {
background-color: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
box-shadow: var(--shadow-card);
padding: 24px;
margin-bottom: 24px;
transition: border-color 0.2s;
}
.card:hover { border-color: var(--border-default); }
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-main);
display: flex;
align-items: center;
gap: 8px;
}
/* Forms */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.input-group { display: flex; flex-direction: column; gap: 6px; }
label {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
input, select {
background-color: var(--bg-input);
border: 1px solid var(--border-default);
color: var(--text-main);
padding: 10px 12px;
border-radius: var(--radius-sm);
font-size: 0.9rem;
transition: all 0.2s;
width: 100%;
}
input:focus, select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-dim);
}
/* Metrics Grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.metric-card {
background: linear-gradient(145deg, var(--bg-card), rgba(255,255,255,0.02));
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: 20px;
position: relative;
overflow: hidden;
}
.metric-card::before {
content: '';
position: absolute;
top: 0; left: 0; width: 4px; height: 100%;
background: var(--primary);
opacity: 0.6;
}
.metric-label {
font-size: 0.85rem;
color: var(--text-muted);
font-weight: 500;
margin-bottom: 8px;
}
.metric-value {
font-size: 1.8rem;
font-weight: 700;
color: var(--text-main);
letter-spacing: -0.03em;
line-height: 1.2;
}
.metric-sub {
font-size: 0.75rem;
color: var(--text-faint);
margin-top: 4px;
}
/* Table */
.table-wrapper {
width: 100%;
overflow-x: auto;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
th {
background-color: var(--bg-surface);
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
padding: 14px 16px;
text-align: left;
border-bottom: 1px solid var(--border-subtle);
white-space: nowrap;
}
th.clickable { cursor: pointer; user-select: none; transition: color 0.2s; }
th.clickable:hover { color: var(--primary); }
td {
padding: 14px 16px;
border-bottom: 1px solid var(--border-subtle);
color: var(--text-main);
}
tr:last-child td { border-bottom: none; }
tr:hover td { background-color: rgba(255,255,255,0.02); }
[data-theme="light"] tr:hover td { background-color: rgba(0,0,0,0.02); }
.text-right { text-align: right; }
.text-center { text-align: center; }
/* Badges */
.badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-success { background: var(--success-bg); color: var(--success-text); }
.badge-warning { background: var(--warning-bg); color: var(--warning-text); }
.badge-danger { background: var(--danger-bg); color: var(--danger-text); }
.badge-neutral { background: var(--bg-surface); color: var(--text-muted); border: 1px solid var(--border-default); }
/* Alerts */
.alert {
padding: 12px 16px;
border-radius: var(--radius-sm);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
font-size: 0.9rem;
border: 1px solid transparent;
}
.alert-info { background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.2); color: #60a5fa; }
.alert-warning { background: var(--warning-bg); border-color: rgba(245, 158, 11, 0.2); color: var(--warning-text); }
.alert-error { background: var(--danger-bg); border-color: rgba(239, 68, 68, 0.2); color: var(--danger-text); }
/* Loading & States */
.loading-overlay {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
color: var(--text-muted);
gap: 16px;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--bg-surface);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.empty-state {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
.empty-state i { font-size: 2.5rem; opacity: 0.3; margin-bottom: 12px; display: block; }
/* Progress Bar */
.progress-bar-bg {
width: 100px;
height: 6px;
background: var(--bg-surface);
border-radius: 3px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: var(--primary);
border-radius: 3px;
}
.hidden { display: none !important; }
/* Custom Delta Section Styling */
.delta-card {
border: 1px solid rgba(16, 185, 129, 0.3);
background: radial-gradient(circle at top right, rgba(16, 185, 129, 0.08), transparent 50%);
}
.delta-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: var(--bg-surface);
border-radius: var(--radius-sm);
margin-bottom: 8px;
font-size: 0.85rem;
}
/* Responsive */
@media (max-width: 768px) {
.header-content { flex-direction: column; gap: 12px; align-items: flex-start; }
.metrics-grid { grid-template-columns: 1fr; }
.card-header { flex-direction: column; align-items: flex-start; gap: 12px; }
.controls-row { width: 100%; flex-direction: column; }
.controls-row > * { width: 100%; }
}
</style>
</head>
<body>
<!-- HEADER -->
<header>
<div class="container header-content flex justify-between items-center">
<div class="brand">
<i class="ri-bar-chart-box-fill"></i>
<span>UTM Analytics Pro</span>
</div>
<div class="flex items-center gap-4">
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
Built with anycoder <i class="ri-external-link-line"></i>
</a>
<button id="theme-toggle" class="btn btn-ghost" style="padding: 8px;">
<i class="ri-sun-line"></i>
</button>
</div>
</div>
</header>
<main class="container" style="padding-top: 32px;">
<!-- 1. CONFIGURATION -->
<section class="card" id="config-section">
<div class="card-header">
<div class="card-title">
<i class="ri-settings-4-line"></i> Configuração da API
</div>
<button id="toggle-config" class="btn btn-ghost" style="font-size: 0.8rem;">
<i class="ri-arrow-down-s-line"></i> Expandir
</button>
</div>
<div id="config-content" class="hidden mt-4">
<div class="form-grid mb-4">
<div class="input-group">
<label for="store-name">Nome da Loja</label>
<input type="text" id="store-name" placeholder="ex: minha-loja">
</div>
<div class="input-group">
<label for="access-token">Access Token (Admin API)</label>
<input type="password" id="access-token" placeholder="shpat_xxxxx...">
</div>
<div class="input-group">
<label for="cors-proxy">CORS Proxy (Opcional)</label>
<input type="text" id="cors-proxy" placeholder="https://corsproxy.io/?">
</div>
</div>
<div class="flex justify-between items-center">
<span id="config-status" style="font-size: 0.8rem; color: var(--success-text);" class="hidden">
<i class="ri-checkbox-circle-line"></i> Configurações salvas
</span>
<button id="save-config" class="btn btn-primary">
<i class="ri-save-line"></i> Salvar Configurações
</button>
</div>
</div>
</section>
<!-- 2. CONTROLS -->
<section class="card">
<div class="form-grid" style="align-items: end;">
<div class="input-group">
<label for="date-range">Período</label>
<select id="date-range">
<option value="hoje">Hoje</option>
<option value="ontem">Ontem</option>
<option value="ultimos7" selected>Últimos 7 dias</option>
<option value="ultimos30">Últimos 30 dias</option>
<option value="customizado">Customizado</option>
</select>
</div>
<div class="input-group hidden" id="custom-dates-container">
<label>Intervalo Personalizado</label>
<div class="flex gap-2">
<input type="date" id="date-start">
<input type="date" id="date-end">
</div>
</div>
<div class="controls-row flex gap-4 justify-end w-full">
<button id="btn-fetch" class="btn btn-primary" disabled>
<i class="ri-refresh-line"></i> Atualizar Dados
</button>
<button id="btn-export" class="btn btn-secondary" disabled>
<i class="ri-download-cloud-2-line"></i> Exportar CSV
</button>
</div>
</div>
</section>
<!-- ALERTS CONTAINER -->
<div id="alert-container"></div>
<!-- 3. NEW ORDERS DELTA -->
<section id="delta-section" class="card delta-card hidden animate-fade-in">
<div class="card-header">
<div class="card-title" style="color: var(--primary);">
<i class="ri-flashlight-fill"></i> Novas Vendas Detectadas
</div>
<div class="flex items-center gap-4">
<span style="font-size: 0.8rem; color: var(--text-muted);">
Base: <span id="snapshot-time" style="color: var(--text-main);">--</span>
</span>
<button id="reset-snapshot" class="btn btn-ghost" style="font-size: 0.75rem; padding: 4px 8px;">
Resetar Base
</button>
</div>
</div>
<div class="flex gap-6 mb-4">
<div>
<div style="font-size: 0.8rem; color: var(--text-muted);">Total Novos Pedidos</div>
<div style="font-size: 1.5rem; font-weight: 700;" id="new-orders-count">0</div>
</div>
<div>
<div style="font-size: 0.8rem; color: var(--text-muted);">Valor Novo (Bruto)</div>
<div style="font-size: 1.5rem; font-weight: 700;" id="new-orders-total">R$ 0,00</div>
</div>
</div>
<div id="delta-list" class="flex-col gap-2"></div>
</section>
<!-- 4. METRICS (Visible after load) -->
<section id="metrics-section" class="hidden">
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">Total de Pedidos</div>
<div class="metric-value" id="m-total-orders">0</div>
<div class="metric-sub">UTMs Únicas: <span id="m-unique-utms">0</span></div>
</div>
<div class="metric-card">
<div class="metric-label">Taxa de Pagamento</div>
<div class="metric-value" id="m-conversion-rate">0%</div>
<div class="metric-sub"><span id="m-paid-count" class="badge badge-success">0 pagos</span></div>
</div>
<div class="metric-card">
<div class="metric-label">Venda Bruta</div>
<div class="metric-value" id="m-total-revenue">R$ 0,00</div>
<div class="metric-sub">Todos os pedidos</div>
</div>
<div class="metric-card">
<div class="metric-label">Venda Paga</div>
<div class="metric-value" style="color: var(--primary);" id="m-paid-revenue">R$ 0,00</div>
<div class="metric-sub">Dinheiro confirmado</div>
</div>
</div>
</section>
<!-- 5. DATA TABLE -->
<section class="card">
<div class="card-header">
<div class="card-title">
<i class="ri-table-line"></i> Detalhamento por Campanha (UTM Content)
</div>
</div>
<!-- Loading State -->
<div id="loading-state" class="loading-overlay hidden">
<div class="spinner"></div>
<p>Processando pedidos e UTMs...</p>
</div>
<!-- Empty State -->
<div id="empty-state" class="empty-state">
<i class="ri-database-2-line"></i>
<h3>Pronto para Analisar</h3>
<p style="max-width: 400px; margin: 8px auto;">
Configure suas credenciais e clique em "Atualizar Dados" para gerar o relatório.
</p>
<button id="btn-initial-connect" class="btn btn-primary mt-4">Conectar e Buscar</button>
</div>
<!-- Table Content -->
<div id="table-container" class="hidden">
<div class="table-wrapper">
<table>
<thead>
<tr>
<th class="clickable" onclick="app.sort('utm')">Campanha (UTM) <i class="ri-arrow-up-down-line" style="opacity:0.5; font-size:0.8em;"></i></th>
<th class="text-center">Pedidos <span style="font-weight:400; font-size:0.7em; opacity:0.7;">(Pagos/Total)</span></th>
<th class="text-center clickable" onclick="app.sort('clients')">Clientes <i class="ri-arrow-up-down-line" style="opacity:0.5; font-size:0.8em;"></i></th>
<th class="text-right clickable" onclick="app.sort('total')">Venda Total <i class="ri-arrow-up-down-line" style="opacity:0.5; font-size:0.8em;"></i></th>
<th class="text-right clickable" onclick="app.sort('paid')">Venda Paga <i class="ri-arrow-up-down-line" style="opacity:0.5; font-size:0.8em;"></i></th>
<th class="text-right clickable" onclick="app.sort('rate')">% Taxa <i class="ri-arrow-up-down-line" style="opacity:0.5; font-size:0.8em;"></i></th>
</tr>
</thead>
<tbody id="report-body">
<!-- Rows -->
</tbody>
</table>
</div>
</div>
</section>
</main>
<script>
/**
* CORE LOGIC
* Translating the Preact logic to Vanilla JS
*/
const Utils = {
formatCurrency(value) {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
},
formatDate(isoString) {
return new Date(isoString).toLocaleString('pt-BR');
},
showAlert(msg, type = 'info') {
const container = document.getElementById('alert-container');
const el = document.createElement('div');
el.className = `alert alert-${type} animate-fade-in`;
let icon = 'ri-information-line';
if(type === 'error') icon = 'ri-error-warning-line';
if(type === 'warning') icon = 'ri-alert-line';
el.innerHTML = `<i class="${icon}" style="font-size:1.2em;"></i> <span>${msg}</span>`;
container.innerHTML = '';
container.appendChild(el);
setTimeout(() => {
el.style.opacity = '0';
setTimeout(() => el.remove(), 300);
}, 5000);
}
};
const ShopifyService = {
config: { storeName: '', accessToken: '', corsProxy: '', apiVersion: '2024-01' },
loadConfig() {
const stored = localStorage.getItem('utm_pro_config');
if (stored) this.config = { ...this.config, ...JSON.parse(stored) };
return this.config;
},
saveConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
localStorage.setItem('utm_pro_config', JSON.stringify(this.config));
},
buildUrl(startDate, endDate, pageInfo = '') {
const baseUrl = `https://${this.config.storeName}.myshopify.com/admin/api/${this.config.apiVersion}/orders.json`;
const params = new URLSearchParams({
status: 'any',
created_at_min: startDate,
created_at_max: endDate,
limit: '250',
fields: 'id,name,total_price,financial_status,customer,created_at,processing_method,custom_attributes'
});
if (pageInfo) params.append('page_info', pageInfo);
let finalUrl = `${baseUrl}?${params.toString()}`;
if (this.config.corsProxy && this.config.corsProxy.trim() !== '') {
const proxy = this.config.corsProxy.replace(/\/$/, '');
finalUrl = `${proxy}${encodeURIComponent(finalUrl)}`;
}
return finalUrl;
},
async fetchAllOrders(startDate, endDate) {
if (!this.config.storeName || !this.config.accessToken) throw new Error("Credenciais ausentes.");
let orders = [];
let url = this.buildUrl(startDate, endDate);
do {
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'X-Shopify-Access-Token': this.config.accessToken }
});
if (!response.ok) {
if (response.status === 401) throw new Error("Token inválido (401).");
if (response.status === 404) throw new Error("Loja não encontrada (404).");
throw new Error(`Erro na API Shopify: ${response.status}`);
}
const data = await response.json();
if (data.orders) orders = orders.concat(data.orders);
const linkHeader = response.headers.get('Link');
url = linkHeader ? this.parseLinkHeader(linkHeader, 'next') : null;
} catch (error) {
if (error.message.includes('Failed to fetch')) {
throw new Error("Erro de CORS ou Rede. Tente usar um CORS Proxy.");
}
throw error;
}
} while (url);
return orders;
},
parseLinkHeader(header, rel) {
const links = header.split(',');
for (let link of links) {
const parts = link.split(';');
const url = parts[0].trim().replace(/<|>/g, '');
if (parts[1].includes(`rel="${rel}"`)) return url;
}
return null;
}
};
const app = {
state: {
theme: 'dark',
rawData: [],
processedData: [],
snapshot: null, // { timestamp: '', orderIds: [] }
sortCol: 'paid',
sortAsc: false,
hasLoaded: false
},
init() {
this.cacheDOM();
this.bindEvents();
this.loadTheme();
const config = ShopifyService.loadConfig();
if (config.storeName && config.accessToken) {
this.dom.inpStore.value = config.storeName;
this.dom.inpToken.value = config.accessToken;
this.dom.inpProxy.value = config.corsProxy || '';
this.enableControls();
}
// Load snapshot
const snap = localStorage.getItem('utm_pro_snapshot');
if (snap) this.state.snapshot = JSON.parse(snap);
},
cacheDOM() {
this.dom = {
themeToggle: document.getElementById('theme-toggle'),
configSection: document.getElementById('config-content'),
toggleConfig: document.getElementById('toggle-config'),
inpStore: document.getElementById('store-name'),
inpToken: document.getElementById('access-token'),
inpProxy: document.getElementById('cors-proxy'),
btnSaveConfig: document.getElementById('save-config'),
configStatus: document.getElementById('config-status'),
dateRange: document.getElementById('date-range'),
customDates: document.getElementById('custom-dates-container'),
dateStart: document.getElementById('date-start'),
dateEnd: document.getElementById('date-end'),
btnFetch: document.getElementById('btn-fetch'),
btnInitial: document.getElementById('btn-initial-connect'),
btnExport: document.getElementById('btn-export'),
alertContainer: document.getElementById('alert-container'),
deltaSection: document.getElementById('delta-section'),
snapshotTime: document.getElementById('snapshot-time'),
newOrdersCount: document.getElementById('new-orders-count'),
newOrdersTotal: document.getElementById('new-orders-total'),
deltaList: document.getElementById('delta-list'),
resetSnapshot: document.getElementById('reset-snapshot'),
metricsSection: document.getElementById('metrics-section'),
mTotalOrders: document.getElementById('m-total-orders'),
mUniqueUtms: document.getElementById('m-unique-utms'),
mConvRate: document.getElementById('m-conversion-rate'),
mPaidCount: document.getElementById('m-paid-count'),
mTotalRev: document.getElementById('m-total-revenue'),
mPaidRev: document.getElementById('m-paid-revenue'),
loadingState: document.getElementById('loading-state'),
emptyState: document.getElementById('empty-state'),
tableContainer: document.getElementById('table-container'),
tableBody: document.getElementById('report-body')
};
},
bindEvents() {
this.dom.themeToggle.addEventListener('click', () => this.toggleTheme());
this.dom.toggleConfig.addEventListener('click', () => {
this.dom.configSection.classList.toggle('hidden');
const icon = this.dom.toggleConfig.querySelector('i');
icon.className = this.dom.configSection.classList.contains('hidden')
? 'ri-arrow-down-s-line' : 'ri-arrow-up-s-line';
});
this.dom.btnSaveConfig.addEventListener('click', () => this.saveConfig());
this.dom.dateRange.addEventListener('change', (e) => {
this.dom.customDates.classList.toggle('hidden', e.target.value !== 'customizado');
});
this.dom.btnFetch.addEventListener('click', () => this.fetchData());
this.dom.btnInitial.addEventListener('click', () => this.fetchData());
this.dom.btnExport.addEventListener('click', () => this.exportCSV());
this.dom.resetSnapshot.addEventListener('click', () => this.resetSnapshot());
},
toggleTheme() {
this.state.theme = this.state.theme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', this.state.theme);
localStorage.setItem('utm_pro_theme', this.state.theme);
const icon = this.dom.themeToggle.querySelector('i');
icon.className = this.state.theme === 'dark' ? 'ri-s