anycoder-c8aae04a / index.html
eubottura's picture
Upload folder using huggingface_hub
2a65caa verified
raw
history blame
32.9 kB
<!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>Relatório de Vendas UTM - Pro Analytics</title>
<!-- Typography: 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 for consistent, crisp iconography -->
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
/* --- 1. DESIGN SYSTEM & VARIABLES --- */
:root {
/* Palette: Dark Mode Default (Premium Zinc & Emerald) */
--bg-body: #09090b; /* Zinc 950 */
--bg-card: #18181b; /* Zinc 900 */
--bg-surface: #27272a; /* Zinc 800 */
--bg-input: #09090b; /* Zinc 950 */
--border-subtle: #27272a;
--border-default: #3f3f46;
--border-focus: #10b981; /* Emerald 500 */
--text-main: #f4f4f5; /* Zinc 100 */
--text-muted: #a1a1aa; /* Zinc 400 */
--text-faint: #71717a; /* Zinc 500 */
--primary: #10b981; /* Emerald 500 */
--primary-dim: rgba(16, 185, 129, 0.1);
--primary-glow: rgba(16, 185, 129, 0.25);
--accent: #8b5cf6; /* Violet */
--danger: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
--shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
--shadow-header: 0 1px 3px 0 rgba(0, 0, 0, 0.5);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--font-main: 'Inter', sans-serif;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
[data-theme="light"] {
--bg-body: #f4f4f5; /* Zinc 100 */
--bg-card: #ffffff;
--bg-surface: #f4f4f5; /* Zinc 100 */
--bg-input: #ffffff;
--border-subtle: #e4e4e7;
--border-default: #d4d4d8;
--border-focus: #059669; /* Emerald 600 */
--text-main: #18181b; /* Zinc 900 */
--text-muted: #52525b; /* Zinc 600 */
--text-faint: #a1a1aa; /* Zinc 400 */
--primary: #059669; /* Emerald 600 */
--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 1px 3px 0 rgba(0, 0, 0, 0.1);
}
/* --- 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.6;
-webkit-font-smoothing: antialiased;
transition: background-color 0.3s ease, color 0.3s ease;
padding-bottom: 40px;
}
a { text-decoration: none; color: inherit; transition: color 0.2s; }
ul { list-style: none; }
button { font-family: inherit; }
/* Scrollbar Styling */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--bg-body); }
::-webkit-scrollbar-thumb { background: var(--border-default); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
/* --- 3. LAYOUT UTILITIES --- */
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 24px;
}
.flex-between { display: flex; justify-content: space-between; align-items: center; }
.flex-center { display: flex; justify-content: center; align-items: center; }
.gap-2 { gap: 8px; }
.gap-4 { gap: 16px; }
.mt-4 { margin-top: 16px; }
.mb-4 { margin-bottom: 16px; }
/* --- 4. COMPONENTS --- */
/* Header */
header {
position: sticky;
top: 0;
z-index: 100;
background: rgba(9, 9, 11, 0.75);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border-subtle);
padding: 16px 0;
transition: all 0.3s ease;
}
[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.125rem;
letter-spacing: -0.02em;
}
.brand i { color: var(--primary); font-size: 1.25rem; }
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 18px;
border-radius: var(--radius-sm);
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s var(--ease-out);
border: 1px solid transparent;
white-space: nowrap;
}
.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) {
transform: translateY(-1px);
box-shadow: 0 0 0 1px var(--primary-glow), 0 6px 16px var(--primary-glow);
filter: brightness(1.1);
}
.btn-secondary {
background-color: var(--bg-surface);
border-color: var(--border-default);
color: var(--text-main);
}
.btn-secondary:hover:not(:disabled) {
border-color: var(--text-muted);
background-color: var(--border-subtle);
}
.btn-ghost {
background: transparent;
color: var(--text-muted);
}
.btn-ghost:hover { color: var(--text-main); background-color: var(--bg-surface); }
.btn-icon { padding: 8px; border-radius: 50%; aspect-ratio: 1; }
.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);
padding: 24px;
box-shadow: var(--shadow-card);
margin-bottom: 24px;
position: relative;
overflow: hidden;
transition: transform 0.3s ease;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
}
.card-title {
font-size: 1rem;
font-weight: 600;
color: var(--text-main);
display: flex;
align-items: center;
gap: 8px;
}
/* Forms & Inputs */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
}
.input-group { display: flex; flex-direction: column; gap: 8px; }
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 14px;
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-container {
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;
display: flex;
flex-direction: column;
gap: 6px;
position: relative;
}
/* Decorative accent line */
.metric-card::after {
content: '';
position: absolute;
top: 0; left: 0; bottom: 0;
width: 3px;
background: var(--primary);
opacity: 0.7;
border-top-left-radius: var(--radius-md);
border-bottom-left-radius: var(--radius-md);
}
.metric-label { font-size: 0.8rem; color: var(--text-muted); font-weight: 500; }
.metric-value { font-size: 1.75rem; font-weight: 700; color: var(--text-main); letter-spacing: -0.03em; }
.metric-sub { font-size: 0.75rem; color: var(--text-faint); display: flex; align-items: center; gap: 4px; }
/* Table */
.table-container {
width: 100%;
overflow-x: auto;
border-radius: var(--radius-sm);
border: 1px solid var(--border-subtle);
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
white-space: nowrap;
}
th {
background-color: var(--bg-surface);
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
font-size: 0.7rem;
padding: 14px 16px;
text-align: left;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--border-subtle);
cursor: pointer;
user-select: none;
transition: color 0.2s;
}
th:hover { color: var(--text-main); }
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 & Pills */
.badge {
padding: 4px 8px;
border-radius: 6px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-success { background: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.2); }
.badge-warning { background: rgba(245, 158, 11, 0.15); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.2); }
.badge-danger { background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.2); }
/* States (Loading, Empty, Alert) */
.state-container {
text-align: center;
padding: 60px 20px;
color: var(--text-muted);
}
.state-icon { font-size: 3rem; margin-bottom: 16px; display: block; opacity: 0.5; }
.alert {
padding: 12px 16px;
border-radius: var(--radius-sm);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
font-size: 0.875rem;
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: rgba(245, 158, 11, 0.1); border-color: rgba(245, 158, 11, 0.2); color: #fbbf24; }
.alert-error { background: rgba(239, 68, 68, 0.1); border-color: rgba(239, 68, 68, 0.2); color: #f87171; }
/* Animations */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner {
width: 24px; height: 24px;
border: 2px solid var(--bg-surface);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.animate-fade-in { animation: fadeIn 0.4s var(--ease-out) forwards; }
/* Specific: New Orders Delta */
.delta-card {
border: 1px solid rgba(16, 185, 129, 0.3);
background: radial-gradient(circle at top right, rgba(16, 185, 129, 0.05), transparent 40%);
}
.delta-list { display: flex; flex-direction: column; gap: 8px; }
.delta-item {
display: flex; justify-content: space-between; align-items: center;
padding: 8px 12px;
background: var(--bg-surface);
border-radius: var(--radius-sm);
font-size: 0.85rem;
}
/* Utilities */
.hidden { display: none !important; }
.link-external { font-size: 0.8rem; color: var(--text-faint); display: flex; align-items: center; gap: 4px; }
.link-external:hover { color: var(--primary); }
/* Responsive */
@media (max-width: 768px) {
.header-content { flex-direction: column; gap: 16px; text-align: center; }
.card-header { flex-direction: column; gap: 12px; align-items: flex-start; }
.controls-row { flex-direction: column; }
.controls-row > * { width: 100%; }
}
</style>
</head>
<body>
<!-- HEADER -->
<header>
<div class="container header-content flex-between">
<div class="brand">
<i class="ri-bar-chart-grouped-fill"></i>
<span>UTM Analytics Pro</span>
</div>
<div class="header-actions flex-center gap-4">
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="link-external">
Built with anycoder <i class="ri-external-link-line"></i>
</a>
<div id="theme-toggle" class="btn btn-ghost btn-icon" title="Alternar Tema">
<i class="ri-sun-line"></i>
</div>
</div>
</div>
</header>
<main class="container" style="padding-top: 32px;">
<!-- 1. CONFIGURATION (Collapsible) -->
<section class="card" id="config-card">
<div class="card-header">
<div class="card-title">
<i class="ri-settings-3-line"></i> Configuração da API
</div>
<button id="toggle-config-btn" class="btn btn-ghost btn-icon">
<i class="ri-arrow-down-s-line"></i>
</button>
</div>
<div id="config-content" class="hidden">
<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</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-between">
<div id="config-msg" class="alert alert-info hidden" style="margin-bottom: 0; padding: 8px 12px;">
<i class="ri-checkbox-circle-line"></i> <span>Salvo!</span>
</div>
<button id="save-config-btn" class="btn btn-primary">
<i class="ri-save-3-line"></i> Salvar Configurações
</button>
</div>
</div>
</section>
<!-- 2. CONTROLS & FILTERS -->
<section class="card">
<div class="form-grid" style="align-items: end;">
<div class="input-group">
<label for="date-range">Período de Análise</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">
<label>Intervalo</label>
<div style="display: flex; gap: 8px;">
<input type="date" id="date-start">
<input type="date" id="date-end">
</div>
</div>
<div class="controls-row flex-between" style="width: 100%; justify-content: flex-end;">
<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> CSV
</button>
</div>
</div>
</section>
<!-- ALERTS & STATUS -->
<div id="alert-area"></div>
<!-- 3. NEW ORDERS DELTA (Visible only on update) -->
<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>
<button id="reset-snapshot" class="btn btn-ghost" style="font-size: 0.75rem;">Resetar Base</button>
</div>
<div class="flex-between mb-4" style="font-size: 0.9rem; color: var(--text-muted);">
<span>Base de comparação: <span id="snapshot-time" style="color: var(--text-main);">--</span></span>
<span>Total de novos pedidos: <strong id="new-orders-count">0</strong></span>
</div>
<div id="delta-content" class="delta-list">
<!-- JS Injected -->
</div>
</section>
<!-- 4. EMPTY STATE (Initial) -->
<div id="empty-state" class="state-container">
<i class="ri-database-2-line state-icon"></i>
<h3 class="mb-4">Pronto para Analisar</h3>
<p style="max-width: 400px; margin: 0 auto 24px;">
Configure suas credenciais da Shopify acima e clique em "Atualizar Dados" para gerar o relatório de UTM.
</p>
<button id="btn-initial-connect" class="btn btn-primary">Conectar e Buscar</button>
</div>
<!-- 5. LOADING STATE -->
<div id="loading-state" class="state-container hidden">
<div class="spinner" style="margin: 0 auto 16px;"></div>
<p>Processando pedidos e UTMs...</p>
</div>
<!-- 6. DASHBOARD (Metrics & Table) -->
<div id="dashboard-content" class="hidden animate-fade-in">
<!-- Metrics Grid -->
<section class="metrics-container">
<div class="metric-card">
<span class="metric-label">Total de Pedidos</span>
<span class="metric-value" id="m-total-orders">0</span>
<span class="metric-sub">UTMs Únicas: <span id="m-unique-utms">0</span></span>
</div>
<div class="metric-card">
<span class="metric-label">Taxa de Conversão</span>
<span class="metric-value" id="m-conversion-rate">0%</span>
<span class="badge badge-success" id="m-conversion-badge">Calculando</span>
</div>
<div class="metric-card">
<span class="metric-label">Faturamento Bruto</span>
<span class="metric-value" id="m-total-revenue">R$ 0,00</span>
<span class="metric-sub">Todos os status</span>
</div>
<div class="metric-card">
<span class="metric-label">Faturamento Pago</span>
<span class="metric-value" id="m-paid-revenue" style="color: var(--primary);">R$ 0,00</span>
<span class="metric-sub" id="m-paid-count">0 pedidos pagos</span>
</div>
</section>
<!-- Table Section -->
<section class="card">
<div class="card-header">
<div class="card-title">
<i class="ri-table-2"></i> Detalhamento por Campanha
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th onclick="app.sortBy('utm')">Campanha / Conteúdo <i class="ri-arrow-up-down-line"></i></th>
<th class="text-center">Pedidos <i class="ri-arrow-up-down-line opacity-0"></i></th>
<th class="text-center" onclick="app.sortBy('clients')">Clientes <i class="ri-arrow-up-down-line"></i></th>
<th class="text-right" onclick="app.sortBy('total')">Venda Total <i class="ri-arrow-up-down-line"></i></th>
<th class="text-right" onclick="app.sortBy('paid')">Venda Paga <i class="ri-arrow-up-down-line"></i></th>
<th class="text-right" onclick="app.sortBy('rate')">% Pago <i class="ri-arrow-up-down-line"></i></th>
</tr>
</thead>
<tbody id="report-body">
<!-- Rows injected via JS -->
</tbody>
</table>
</div>
<div id="no-data-msg" class="state-container hidden" style="padding: 20px;">
<p style="font-size: 0.9rem;">Nenhum dado encontrado para o período selecionado.</p>
</div>
</section>
</div>
</main>
<script>
/**
* UTILITIES
*/
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(message, type = 'info') {
const area = document.getElementById('alert-area');
const alert = document.createElement('div');
alert.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';
alert.innerHTML = `<i class="${icon}"></i> <span>${message}</span>`;
area.innerHTML = '';
area.appendChild(alert);
// Auto dismiss
setTimeout(() => {
alert.style.opacity = '0';
setTimeout(() => alert.remove(), 300);
}, 5000);
}
};
/**
* SHOPIFY SERVICE
* Handles API communication
*/
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;
}
return null;
},
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,processing_method,customer,created_at,landing_site_ref,source_name' // Efficient fields
});
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: ${response.status}`);
}
const data = await response.json();
if (data.orders) orders = orders.concat(data.orders);
// Pagination
const linkHeader = response.headers.get('Link');
url = linkHeader ? this.parseLinkHeader(linkHeader, 'next') : null;
} catch (error) {
console.error(error);
if (error.message.includes('Failed to fetch')) {
throw new Error("Erro de CORS. Verifique o 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;
}
};
/**
* APP LOGIC
*/
const app = {
state: {
theme: 'dark',
rawData: [],
processedData: [],
snapshot: null,
sortCol: 'paid',
sortAsc: false
},
init() {
this.cacheDOM();
this.bindEvents();
this.loadSettings();
this.checkTheme();
},
cacheDOM() {
this.dom = {
// Config
configCard: document.getElementById('config-card'),
configContent: document.getElementById('config-content'),
toggleConfig: document.getElementById('toggle-config-btn'),
inpStore: document.getElementById('store-name'),
inpToken: document.getElementById('access-token'),
inpProxy: document.getElementById('cors-proxy'),
btnSave: document.getElementById('save-config-btn'),
msgConfig: document.getElementById('config-msg'),
// Filters
dateRange: document.getElementById('date-range'),
customDates: document.getElementById('custom-dates'),
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'),
// Views
emptyState: document.getElementById('empty-state'),
loadingState: document.getElementById('loading-state'),
dashboard: document.getElementById('dashboard-content'),
deltaSection: document.getElementById('delta-section'),
deltaContent: document.getElementById('delta-content'),
snapshotTime: document.getElementById('snapshot-time'),
newOrdersCount: document.getElementById('new-orders-count'),
resetSnapshot: document.getElementById('reset-snapshot'),
// Metrics
mTotalOrders: document.getElementById('m-total-orders'),
mUniqueUtms: document.getElementById('m-unique-utms'),
mConvRate: document.getElementById('m-conversion-rate'),
mConvBadge: document.getElementById('m-conversion-badge'),
mTotalRev: document.getElementById('m-total-revenue'),
mPaidRev: document.getElementById('m-paid-revenue'),
mPaidCount: document.getElementById('m-paid-count'),
// Table
tableBody: document.getElementById('report-body'),
noDataMsg: document.getElementById('no-data-msg'),
// Global
themeToggle: document.getElementById('theme-toggle'),
alertArea: document.getElementById('alert-area')
};
},
bindEvents() {
// Config
this.dom.toggleConfig.addEventListener('click', () => {
this.dom.configContent.classList.toggle('hidden');
const icon = this.dom.toggleConfig.querySelector('i');
icon.className = this.dom.configContent.classList.contains('hidden')
? 'ri-arrow-down-s-line'
: 'ri-arrow-up-s-line';
});
this.dom.btnSave.addEventListener('click', () => this.saveSettings());
// Data
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', () => {
localStorage.removeItem('utm_pro_snapshot');
this.state.snapshot = null;
this.dom.deltaSection.classList.add('hidden');
Utils.showAlert('Base de comparação resetada.', 'info');
});
// Theme
this.dom.themeToggle.addEventListener('click', () => this.toggleTheme());
},
loadSettings() {
const config = ShopifyService.loadConfig();
if (config) {
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);
}
},
saveSettings() {
const store = this.dom.inpStore.value.trim();
const token = this.dom.inpToken.value.trim();
const proxy = this.dom.inpProxy.value.trim();
if (!store || !token) {
Utils.showAlert('Preencha Nome da Loja e Token.', 'warning');
return;
}
ShopifyService.saveConfig({ storeName: store, accessToken: token, corsProxy: proxy });
this.dom.msgConfig.classList.remove('hidden');
this.enableControls();
setTimeout(() => this.dom.msgConfig.classList.add('hidden'), 3000);
},
enableControls() {
this.dom.btnFetch.disabled = false;
this.dom.btnInitial.disabled = false;
},
checkTheme() {
const saved = localStorage.getItem('utm_pro_theme') || 'dark';
document.documentElement.setAttribute('data-theme', saved);
const icon = this.dom.themeToggle.querySelector('i');
icon.className = saved === 'dark' ? 'ri-sun-line' : 'ri-moon-line';
},
toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('utm_pro_theme', next);
const icon = this.dom.themeToggle.querySelector('i');
icon.className = next === 'dark' ? 'ri-sun-line' : 'ri-moon-line';
},
getDates() {
const range = this.dom.dateRange.value;
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
let start, end = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1); // End of today
const days = (n) => new Date(today.getTime() - n * 24 * 60 * 60 * 1000);
switch (range) {
case 'hoje': start = today; break;
case 'ontem': start = days(1); end = days(0); break;
case 'ultimos7': start = days(7); break;
case 'ultimos30': start = days(30); break;
case 'customizado':
if(this.dom.dateStart.value && this.dom.dateEnd.value) {
start = new Date(this.dom.dateStart.value);
end = new Date(this.dom.dateEnd.value);
end.setHours(23,59,59,999);
} else {
start = days(7); // Fallback
}
break;
default: start = days(7);
}
return { start: start.toISOString(), end: end.toISOString() };
},
async fetchData() {
const config = ShopifyService.loadConfig();
if (!config || !config.storeName) {
this.dom.configCard.scrollIntoView({ behavior: 'smooth' });
Utils.showAlert('Configure a API primeiro.', 'warning');
return;
}
this.setLoading(true);
this.dom.alertArea.innerHTML = '';
this.dom.deltaSection.classList.add('hidden');
try {
const { start, end } = this.getDates();
const orders = await ShopifyService.fetchAllOrders(start, end);
this.state.rawData = orders;
this.processData(orders);
this.checkForNewOrders(orders);
// Save snapshot for next time
this.state.snapshot = {
timestamp: new Date().toISOString(),
orderIds: orders.map(o => o.id)
};
localStorage.setItem('utm_pro_snapshot', JSON.stringify(this.state.snapshot));
this.render();
} catch (err) {
Utils.showAlert(err.message, 'error');
this.dom.dashboard.classList.add('hidden');
this.dom.emptyState.classList.remove('hidden');
} finally {
this.setLoading(false);
}
},
checkForNewOrders(currentOrders) {
if (!this.state.snapshot) return;
const previousIds = new Set(this