Spaces:
Running
Running
| <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 ; | |
| box-shadow: none ; | |
| } | |
| /* 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 ; } | |
| /* 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 |