eubottura commited on
Commit
bf8863e
verified
1 Parent(s): 2a65caa

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +361 -437
index.html CHANGED
@@ -1,77 +1,83 @@
1
  <!DOCTYPE html>
2
  <html lang="pt-BR" data-theme="dark">
3
-
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Relat贸rio de Vendas UTM - Pro Analytics</title>
8
-
9
- <!-- Typography: Inter for clean, modern readability -->
10
  <link rel="preconnect" href="https://fonts.googleapis.com">
11
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
13
-
14
- <!-- Icons: Remix Icon for consistent, crisp iconography -->
15
  <link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
16
 
17
  <style>
18
  /* --- 1. DESIGN SYSTEM & VARIABLES --- */
19
  :root {
20
- /* Palette: Dark Mode Default (Premium Zinc & Emerald) */
21
- --bg-body: #09090b; /* Zinc 950 */
22
- --bg-card: #18181b; /* Zinc 900 */
23
- --bg-surface: #27272a; /* Zinc 800 */
24
- --bg-input: #09090b; /* Zinc 950 */
25
-
26
- --border-subtle: #27272a;
27
- --border-default: #3f3f46;
28
- --border-focus: #10b981; /* Emerald 500 */
29
 
30
  --text-main: #f4f4f5; /* Zinc 100 */
31
  --text-muted: #a1a1aa; /* Zinc 400 */
32
  --text-faint: #71717a; /* Zinc 500 */
33
 
34
  --primary: #10b981; /* Emerald 500 */
 
35
  --primary-dim: rgba(16, 185, 129, 0.1);
36
- --primary-glow: rgba(16, 185, 129, 0.25);
 
 
37
 
38
- --accent: #8b5cf6; /* Violet */
 
39
 
40
- --danger: #ef4444;
41
- --warning: #f59e0b;
42
- --info: #3b82f6;
43
 
44
- --shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
45
- --shadow-header: 0 1px 3px 0 rgba(0, 0, 0, 0.5);
46
 
47
- --radius-sm: 8px;
48
- --radius-md: 12px;
49
- --radius-lg: 16px;
50
 
51
  --font-main: 'Inter', sans-serif;
52
- --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
 
 
 
53
  }
54
 
55
  [data-theme="light"] {
56
- --bg-body: #f4f4f5; /* Zinc 100 */
57
  --bg-card: #ffffff;
58
- --bg-surface: #f4f4f5; /* Zinc 100 */
59
  --bg-input: #ffffff;
60
 
61
- --border-subtle: #e4e4e7;
62
- --border-default: #d4d4d8;
63
- --border-focus: #059669; /* Emerald 600 */
64
 
65
- --text-main: #18181b; /* Zinc 900 */
66
- --text-muted: #52525b; /* Zinc 600 */
67
- --text-faint: #a1a1aa; /* Zinc 400 */
68
 
69
- --primary: #059669; /* Emerald 600 */
 
70
  --primary-dim: rgba(5, 150, 105, 0.1);
71
  --primary-glow: rgba(5, 150, 105, 0.15);
72
 
73
  --shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
74
- --shadow-header: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
75
  }
76
 
77
  /* --- 2. RESET & BASE --- */
@@ -81,49 +87,50 @@
81
  font-family: var(--font-main);
82
  background-color: var(--bg-body);
83
  color: var(--text-main);
84
- line-height: 1.6;
85
  -webkit-font-smoothing: antialiased;
86
  transition: background-color 0.3s ease, color 0.3s ease;
87
- padding-bottom: 40px;
88
  }
89
 
90
  a { text-decoration: none; color: inherit; transition: color 0.2s; }
91
  ul { list-style: none; }
92
- button { font-family: inherit; }
93
 
94
- /* Scrollbar Styling */
95
  ::-webkit-scrollbar { width: 8px; height: 8px; }
96
- ::-webkit-scrollbar-track { background: var(--bg-body); }
97
  ::-webkit-scrollbar-thumb { background: var(--border-default); border-radius: 4px; }
98
- ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
99
 
100
  /* --- 3. LAYOUT UTILITIES --- */
101
- .container {
102
- max-width: 1280px;
103
- margin: 0 auto;
104
- padding: 0 24px;
105
- }
106
-
107
- .flex-between { display: flex; justify-content: space-between; align-items: center; }
108
- .flex-center { display: flex; justify-content: center; align-items: center; }
109
  .gap-2 { gap: 8px; }
110
  .gap-4 { gap: 16px; }
 
111
  .mt-4 { margin-top: 16px; }
112
  .mb-4 { margin-bottom: 16px; }
113
-
 
114
  /* --- 4. COMPONENTS --- */
115
 
116
  /* Header */
117
  header {
118
  position: sticky;
119
  top: 0;
120
- z-index: 100;
121
- background: rgba(9, 9, 11, 0.75);
122
- backdrop-filter: blur(16px);
123
- -webkit-backdrop-filter: blur(16px);
124
  border-bottom: 1px solid var(--border-subtle);
125
  padding: 16px 0;
126
- transition: all 0.3s ease;
127
  }
128
  [data-theme="light"] header { background: rgba(255, 255, 255, 0.85); }
129
 
@@ -132,10 +139,21 @@
132
  align-items: center;
133
  gap: 12px;
134
  font-weight: 700;
135
- font-size: 1.125rem;
136
- letter-spacing: -0.02em;
 
137
  }
138
- .brand i { color: var(--primary); font-size: 1.25rem; }
 
 
 
 
 
 
 
 
 
 
139
 
140
  /* Buttons */
141
  .btn {
@@ -143,14 +161,13 @@
143
  align-items: center;
144
  justify-content: center;
145
  gap: 8px;
146
- padding: 10px 18px;
147
  border-radius: var(--radius-sm);
148
  font-weight: 500;
149
  font-size: 0.875rem;
150
- cursor: pointer;
151
- transition: all 0.2s var(--ease-out);
152
- border: 1px solid transparent;
153
  white-space: nowrap;
 
154
  }
155
 
156
  .btn-primary {
@@ -159,9 +176,8 @@
159
  box-shadow: 0 0 0 1px var(--primary-glow), 0 4px 12px var(--primary-glow);
160
  }
161
  .btn-primary:hover:not(:disabled) {
 
162
  transform: translateY(-1px);
163
- box-shadow: 0 0 0 1px var(--primary-glow), 0 6px 16px var(--primary-glow);
164
- filter: brightness(1.1);
165
  }
166
 
167
  .btn-secondary {
@@ -170,40 +186,45 @@
170
  color: var(--text-main);
171
  }
172
  .btn-secondary:hover:not(:disabled) {
173
- border-color: var(--text-muted);
174
  background-color: var(--border-subtle);
 
175
  }
176
 
177
  .btn-ghost {
178
- background: transparent;
179
  color: var(--text-muted);
180
  }
181
- .btn-ghost:hover { color: var(--text-main); background-color: var(--bg-surface); }
182
-
183
- .btn-icon { padding: 8px; border-radius: 50%; aspect-ratio: 1; }
184
- .btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; box-shadow: none !important; }
 
 
 
 
 
 
 
185
 
186
  /* Cards */
187
  .card {
188
  background-color: var(--bg-card);
189
  border: 1px solid var(--border-subtle);
190
  border-radius: var(--radius-md);
191
- padding: 24px;
192
  box-shadow: var(--shadow-card);
 
193
  margin-bottom: 24px;
194
- position: relative;
195
- overflow: hidden;
196
- transition: transform 0.3s ease;
197
  }
 
198
 
199
  .card-header {
200
  display: flex;
201
  justify-content: space-between;
202
- align-items: flex-start;
203
  margin-bottom: 20px;
204
  }
205
  .card-title {
206
- font-size: 1rem;
207
  font-weight: 600;
208
  color: var(--text-main);
209
  display: flex;
@@ -211,13 +232,13 @@
211
  gap: 8px;
212
  }
213
 
214
- /* Forms & Inputs */
215
  .form-grid {
216
  display: grid;
217
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
218
- gap: 20px;
219
  }
220
- .input-group { display: flex; flex-direction: column; gap: 8px; }
221
 
222
  label {
223
  font-size: 0.75rem;
@@ -231,7 +252,7 @@
231
  background-color: var(--bg-input);
232
  border: 1px solid var(--border-default);
233
  color: var(--text-main);
234
- padding: 10px 14px;
235
  border-radius: var(--radius-sm);
236
  font-size: 0.9rem;
237
  transition: all 0.2s;
@@ -243,69 +264,71 @@
243
  }
244
 
245
  /* Metrics Grid */
246
- .metrics-container {
247
  display: grid;
248
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
249
  gap: 20px;
250
  margin-bottom: 24px;
251
  }
252
-
253
  .metric-card {
254
  background: linear-gradient(145deg, var(--bg-card), rgba(255,255,255,0.02));
255
  border: 1px solid var(--border-subtle);
256
  border-radius: var(--radius-md);
257
  padding: 20px;
258
- display: flex;
259
- flex-direction: column;
260
- gap: 6px;
261
  position: relative;
 
262
  }
263
- /* Decorative accent line */
264
- .metric-card::after {
265
  content: '';
266
  position: absolute;
267
- top: 0; left: 0; bottom: 0;
268
- width: 3px;
269
  background: var(--primary);
270
- opacity: 0.7;
271
- border-top-left-radius: var(--radius-md);
272
- border-bottom-left-radius: var(--radius-md);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  }
274
-
275
- .metric-label { font-size: 0.8rem; color: var(--text-muted); font-weight: 500; }
276
- .metric-value { font-size: 1.75rem; font-weight: 700; color: var(--text-main); letter-spacing: -0.03em; }
277
- .metric-sub { font-size: 0.75rem; color: var(--text-faint); display: flex; align-items: center; gap: 4px; }
278
 
279
  /* Table */
280
- .table-container {
281
  width: 100%;
282
  overflow-x: auto;
283
- border-radius: var(--radius-sm);
284
  border: 1px solid var(--border-subtle);
 
285
  }
286
-
287
  table {
288
  width: 100%;
289
  border-collapse: collapse;
290
  font-size: 0.875rem;
291
- white-space: nowrap;
292
  }
293
-
294
  th {
295
  background-color: var(--bg-surface);
296
  color: var(--text-muted);
297
  font-weight: 600;
298
  text-transform: uppercase;
299
- font-size: 0.7rem;
300
  padding: 14px 16px;
301
  text-align: left;
302
- letter-spacing: 0.05em;
303
  border-bottom: 1px solid var(--border-subtle);
304
- cursor: pointer;
305
- user-select: none;
306
- transition: color 0.2s;
307
  }
308
- th:hover { color: var(--text-main); }
 
309
 
310
  td {
311
  padding: 14px 16px;
@@ -319,123 +342,148 @@
319
  .text-right { text-align: right; }
320
  .text-center { text-align: center; }
321
 
322
- /* Badges & Pills */
323
  .badge {
324
- padding: 4px 8px;
325
- border-radius: 6px;
 
 
326
  font-size: 0.7rem;
327
  font-weight: 600;
328
  text-transform: uppercase;
329
  }
330
- .badge-success { background: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.2); }
331
- .badge-warning { background: rgba(245, 158, 11, 0.15); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.2); }
332
- .badge-danger { background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.2); }
333
-
334
- /* States (Loading, Empty, Alert) */
335
- .state-container {
336
- text-align: center;
337
- padding: 60px 20px;
338
- color: var(--text-muted);
339
- }
340
- .state-icon { font-size: 3rem; margin-bottom: 16px; display: block; opacity: 0.5; }
341
 
 
342
  .alert {
343
  padding: 12px 16px;
344
  border-radius: var(--radius-sm);
345
- margin-bottom: 20px;
346
  display: flex;
347
  align-items: center;
348
  gap: 12px;
349
- font-size: 0.875rem;
350
  border: 1px solid transparent;
351
  }
352
  .alert-info { background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.2); color: #60a5fa; }
353
- .alert-warning { background: rgba(245, 158, 11, 0.1); border-color: rgba(245, 158, 11, 0.2); color: #fbbf24; }
354
- .alert-error { background: rgba(239, 68, 68, 0.1); border-color: rgba(239, 68, 68, 0.2); color: #f87171; }
355
 
356
- /* Animations */
357
- @keyframes spin { to { transform: rotate(360deg); } }
 
 
 
 
 
 
 
 
358
  .spinner {
359
- width: 24px; height: 24px;
360
- border: 2px solid var(--bg-surface);
 
361
  border-top-color: var(--primary);
362
  border-radius: 50%;
363
  animation: spin 0.8s linear infinite;
364
  }
 
365
 
366
- @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
367
- .animate-fade-in { animation: fadeIn 0.4s var(--ease-out) forwards; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
 
369
- /* Specific: New Orders Delta */
 
 
370
  .delta-card {
371
  border: 1px solid rgba(16, 185, 129, 0.3);
372
- background: radial-gradient(circle at top right, rgba(16, 185, 129, 0.05), transparent 40%);
373
  }
374
- .delta-list { display: flex; flex-direction: column; gap: 8px; }
375
  .delta-item {
376
- display: flex; justify-content: space-between; align-items: center;
 
 
377
  padding: 8px 12px;
378
  background: var(--bg-surface);
379
  border-radius: var(--radius-sm);
 
380
  font-size: 0.85rem;
381
  }
382
 
383
- /* Utilities */
384
- .hidden { display: none !important; }
385
- .link-external { font-size: 0.8rem; color: var(--text-faint); display: flex; align-items: center; gap: 4px; }
386
- .link-external:hover { color: var(--primary); }
387
-
388
  /* Responsive */
389
  @media (max-width: 768px) {
390
- .header-content { flex-direction: column; gap: 16px; text-align: center; }
391
- .card-header { flex-direction: column; gap: 12px; align-items: flex-start; }
392
- .controls-row { flex-direction: column; }
 
393
  .controls-row > * { width: 100%; }
394
  }
395
  </style>
396
  </head>
397
 
398
  <body>
 
399
  <!-- HEADER -->
400
  <header>
401
- <div class="container header-content flex-between">
402
  <div class="brand">
403
- <i class="ri-bar-chart-grouped-fill"></i>
404
  <span>UTM Analytics Pro</span>
405
  </div>
406
 
407
- <div class="header-actions flex-center gap-4">
408
- <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="link-external">
409
  Built with anycoder <i class="ri-external-link-line"></i>
410
  </a>
411
- <div id="theme-toggle" class="btn btn-ghost btn-icon" title="Alternar Tema">
412
  <i class="ri-sun-line"></i>
413
- </div>
414
  </div>
415
  </div>
416
  </header>
417
 
418
  <main class="container" style="padding-top: 32px;">
419
-
420
- <!-- 1. CONFIGURATION (Collapsible) -->
421
- <section class="card" id="config-card">
422
  <div class="card-header">
423
  <div class="card-title">
424
- <i class="ri-settings-3-line"></i> Configura莽茫o da API
425
  </div>
426
- <button id="toggle-config-btn" class="btn btn-ghost btn-icon">
427
- <i class="ri-arrow-down-s-line"></i>
428
  </button>
429
  </div>
430
 
431
- <div id="config-content" class="hidden">
432
  <div class="form-grid mb-4">
433
  <div class="input-group">
434
  <label for="store-name">Nome da Loja</label>
435
  <input type="text" id="store-name" placeholder="ex: minha-loja">
436
  </div>
437
  <div class="input-group">
438
- <label for="access-token">Access Token</label>
439
  <input type="password" id="access-token" placeholder="shpat_xxxxx...">
440
  </div>
441
  <div class="input-group">
@@ -443,22 +491,22 @@
443
  <input type="text" id="cors-proxy" placeholder="https://corsproxy.io/?">
444
  </div>
445
  </div>
446
- <div class="flex-between">
447
- <div id="config-msg" class="alert alert-info hidden" style="margin-bottom: 0; padding: 8px 12px;">
448
- <i class="ri-checkbox-circle-line"></i> <span>Salvo!</span>
449
- </div>
450
- <button id="save-config-btn" class="btn btn-primary">
451
- <i class="ri-save-3-line"></i> Salvar Configura莽玫es
452
  </button>
453
  </div>
454
  </div>
455
  </section>
456
 
457
- <!-- 2. CONTROLS & FILTERS -->
458
  <section class="card">
459
  <div class="form-grid" style="align-items: end;">
460
  <div class="input-group">
461
- <label for="date-range">Per铆odo de An谩lise</label>
462
  <select id="date-range">
463
  <option value="hoje">Hoje</option>
464
  <option value="ontem">Ontem</option>
@@ -468,126 +516,138 @@
468
  </select>
469
  </div>
470
 
471
- <div class="input-group hidden" id="custom-dates">
472
- <label>Intervalo</label>
473
- <div style="display: flex; gap: 8px;">
474
  <input type="date" id="date-start">
475
  <input type="date" id="date-end">
476
  </div>
477
  </div>
478
 
479
- <div class="controls-row flex-between" style="width: 100%; justify-content: flex-end;">
480
  <button id="btn-fetch" class="btn btn-primary" disabled>
481
  <i class="ri-refresh-line"></i> Atualizar Dados
482
  </button>
483
  <button id="btn-export" class="btn btn-secondary" disabled>
484
- <i class="ri-download-cloud-2-line"></i> CSV
485
  </button>
486
  </div>
487
  </div>
488
  </section>
489
 
490
- <!-- ALERTS & STATUS -->
491
- <div id="alert-area"></div>
492
 
493
- <!-- 3. NEW ORDERS DELTA (Visible only on update) -->
494
  <section id="delta-section" class="card delta-card hidden animate-fade-in">
495
  <div class="card-header">
496
  <div class="card-title" style="color: var(--primary);">
497
  <i class="ri-flashlight-fill"></i> Novas Vendas Detectadas
498
  </div>
499
- <button id="reset-snapshot" class="btn btn-ghost" style="font-size: 0.75rem;">Resetar Base</button>
500
- </div>
501
- <div class="flex-between mb-4" style="font-size: 0.9rem; color: var(--text-muted);">
502
- <span>Base de compara莽茫o: <span id="snapshot-time" style="color: var(--text-main);">--</span></span>
503
- <span>Total de novos pedidos: <strong id="new-orders-count">0</strong></span>
 
 
 
504
  </div>
505
- <div id="delta-content" class="delta-list">
506
- <!-- JS Injected -->
 
 
 
 
 
 
 
 
507
  </div>
508
- </section>
509
 
510
- <!-- 4. EMPTY STATE (Initial) -->
511
- <div id="empty-state" class="state-container">
512
- <i class="ri-database-2-line state-icon"></i>
513
- <h3 class="mb-4">Pronto para Analisar</h3>
514
- <p style="max-width: 400px; margin: 0 auto 24px;">
515
- Configure suas credenciais da Shopify acima e clique em "Atualizar Dados" para gerar o relat贸rio de UTM.
516
- </p>
517
- <button id="btn-initial-connect" class="btn btn-primary">Conectar e Buscar</button>
518
- </div>
519
-
520
- <!-- 5. LOADING STATE -->
521
- <div id="loading-state" class="state-container hidden">
522
- <div class="spinner" style="margin: 0 auto 16px;"></div>
523
- <p>Processando pedidos e UTMs...</p>
524
- </div>
525
 
526
- <!-- 6. DASHBOARD (Metrics & Table) -->
527
- <div id="dashboard-content" class="hidden animate-fade-in">
528
-
529
- <!-- Metrics Grid -->
530
- <section class="metrics-container">
531
  <div class="metric-card">
532
- <span class="metric-label">Total de Pedidos</span>
533
- <span class="metric-value" id="m-total-orders">0</span>
534
- <span class="metric-sub">UTMs 脷nicas: <span id="m-unique-utms">0</span></span>
535
  </div>
536
  <div class="metric-card">
537
- <span class="metric-label">Taxa de Convers茫o</span>
538
- <span class="metric-value" id="m-conversion-rate">0%</span>
539
- <span class="badge badge-success" id="m-conversion-badge">Calculando</span>
540
  </div>
541
  <div class="metric-card">
542
- <span class="metric-label">Faturamento Bruto</span>
543
- <span class="metric-value" id="m-total-revenue">R$ 0,00</span>
544
- <span class="metric-sub">Todos os status</span>
545
  </div>
546
  <div class="metric-card">
547
- <span class="metric-label">Faturamento Pago</span>
548
- <span class="metric-value" id="m-paid-revenue" style="color: var(--primary);">R$ 0,00</span>
549
- <span class="metric-sub" id="m-paid-count">0 pedidos pagos</span>
550
  </div>
551
- </section>
 
552
 
553
- <!-- Table Section -->
554
- <section class="card">
555
- <div class="card-header">
556
- <div class="card-title">
557
- <i class="ri-table-2"></i> Detalhamento por Campanha
558
- </div>
559
  </div>
560
-
561
- <div class="table-container">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
562
  <table>
563
  <thead>
564
  <tr>
565
- <th onclick="app.sortBy('utm')">Campanha / Conte煤do <i class="ri-arrow-up-down-line"></i></th>
566
- <th class="text-center">Pedidos <i class="ri-arrow-up-down-line opacity-0"></i></th>
567
- <th class="text-center" onclick="app.sortBy('clients')">Clientes <i class="ri-arrow-up-down-line"></i></th>
568
- <th class="text-right" onclick="app.sortBy('total')">Venda Total <i class="ri-arrow-up-down-line"></i></th>
569
- <th class="text-right" onclick="app.sortBy('paid')">Venda Paga <i class="ri-arrow-up-down-line"></i></th>
570
- <th class="text-right" onclick="app.sortBy('rate')">% Pago <i class="ri-arrow-up-down-line"></i></th>
571
  </tr>
572
  </thead>
573
  <tbody id="report-body">
574
- <!-- Rows injected via JS -->
575
  </tbody>
576
  </table>
577
  </div>
578
-
579
- <div id="no-data-msg" class="state-container hidden" style="padding: 20px;">
580
- <p style="font-size: 0.9rem;">Nenhum dado encontrado para o per铆odo selecionado.</p>
581
- </div>
582
- </section>
583
- </div>
584
 
585
  </main>
586
 
587
  <script>
588
  /**
589
- * UTILITIES
 
590
  */
 
591
  const Utils = {
592
  formatCurrency(value) {
593
  return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
@@ -595,41 +655,33 @@
595
  formatDate(isoString) {
596
  return new Date(isoString).toLocaleString('pt-BR');
597
  },
598
- showAlert(message, type = 'info') {
599
- const area = document.getElementById('alert-area');
600
- const alert = document.createElement('div');
601
- alert.className = `alert alert-${type} animate-fade-in`;
602
 
603
  let icon = 'ri-information-line';
604
  if(type === 'error') icon = 'ri-error-warning-line';
605
  if(type === 'warning') icon = 'ri-alert-line';
606
 
607
- alert.innerHTML = `<i class="${icon}"></i> <span>${message}</span>`;
608
- area.innerHTML = '';
609
- area.appendChild(alert);
610
-
611
- // Auto dismiss
612
  setTimeout(() => {
613
- alert.style.opacity = '0';
614
- setTimeout(() => alert.remove(), 300);
615
  }, 5000);
616
  }
617
  };
618
 
619
- /**
620
- * SHOPIFY SERVICE
621
- * Handles API communication
622
- */
623
  const ShopifyService = {
624
  config: { storeName: '', accessToken: '', corsProxy: '', apiVersion: '2024-01' },
625
 
626
  loadConfig() {
627
  const stored = localStorage.getItem('utm_pro_config');
628
- if (stored) {
629
- this.config = { ...this.config, ...JSON.parse(stored) };
630
- return this.config;
631
- }
632
- return null;
633
  },
634
 
635
  saveConfig(newConfig) {
@@ -637,17 +689,16 @@
637
  localStorage.setItem('utm_pro_config', JSON.stringify(this.config));
638
  },
639
 
640
- buildUrl(startDate, endDate, pageinfo = '') {
641
  const baseUrl = `https://${this.config.storeName}.myshopify.com/admin/api/${this.config.apiVersion}/orders.json`;
642
  const params = new URLSearchParams({
643
  status: 'any',
644
  created_at_min: startDate,
645
  created_at_max: endDate,
646
  limit: '250',
647
- fields: 'id,name,total_price,financial_status,processing_method,customer,created_at,landing_site_ref,source_name' // Efficient fields
648
  });
649
-
650
- if (pageinfo) params.append('page_info', pageinfo);
651
 
652
  let finalUrl = `${baseUrl}?${params.toString()}`;
653
 
@@ -659,9 +710,7 @@
659
  },
660
 
661
  async fetchAllOrders(startDate, endDate) {
662
- if (!this.config.storeName || !this.config.accessToken) {
663
- throw new Error("Credenciais ausentes.");
664
- }
665
 
666
  let orders = [];
667
  let url = this.buildUrl(startDate, endDate);
@@ -676,20 +725,18 @@
676
  if (!response.ok) {
677
  if (response.status === 401) throw new Error("Token inv谩lido (401).");
678
  if (response.status === 404) throw new Error("Loja n茫o encontrada (404).");
679
- throw new Error(`Erro na API: ${response.status}`);
680
  }
681
 
682
  const data = await response.json();
683
  if (data.orders) orders = orders.concat(data.orders);
684
 
685
- // Pagination
686
  const linkHeader = response.headers.get('Link');
687
  url = linkHeader ? this.parseLinkHeader(linkHeader, 'next') : null;
688
 
689
  } catch (error) {
690
- console.error(error);
691
  if (error.message.includes('Failed to fetch')) {
692
- throw new Error("Erro de CORS. Verifique o Proxy.");
693
  }
694
  throw error;
695
  }
@@ -709,227 +756,104 @@
709
  }
710
  };
711
 
712
- /**
713
- * APP LOGIC
714
- */
715
  const app = {
716
  state: {
717
  theme: 'dark',
718
  rawData: [],
719
  processedData: [],
720
- snapshot: null,
721
  sortCol: 'paid',
722
- sortAsc: false
 
723
  },
724
 
725
  init() {
726
  this.cacheDOM();
727
  this.bindEvents();
728
- this.loadSettings();
729
- this.checkTheme();
 
 
 
 
 
 
 
 
 
 
 
730
  },
731
 
732
  cacheDOM() {
733
  this.dom = {
734
- // Config
735
- configCard: document.getElementById('config-card'),
736
- configContent: document.getElementById('config-content'),
737
- toggleConfig: document.getElementById('toggle-config-btn'),
738
  inpStore: document.getElementById('store-name'),
739
  inpToken: document.getElementById('access-token'),
740
  inpProxy: document.getElementById('cors-proxy'),
741
- btnSave: document.getElementById('save-config-btn'),
742
- msgConfig: document.getElementById('config-msg'),
743
 
744
- // Filters
745
  dateRange: document.getElementById('date-range'),
746
- customDates: document.getElementById('custom-dates'),
747
  dateStart: document.getElementById('date-start'),
748
  dateEnd: document.getElementById('date-end'),
 
749
  btnFetch: document.getElementById('btn-fetch'),
750
  btnInitial: document.getElementById('btn-initial-connect'),
751
  btnExport: document.getElementById('btn-export'),
752
 
753
- // Views
754
- emptyState: document.getElementById('empty-state'),
755
- loadingState: document.getElementById('loading-state'),
756
- dashboard: document.getElementById('dashboard-content'),
757
  deltaSection: document.getElementById('delta-section'),
758
- deltaContent: document.getElementById('delta-content'),
759
  snapshotTime: document.getElementById('snapshot-time'),
760
  newOrdersCount: document.getElementById('new-orders-count'),
 
 
761
  resetSnapshot: document.getElementById('reset-snapshot'),
762
 
763
- // Metrics
764
  mTotalOrders: document.getElementById('m-total-orders'),
765
  mUniqueUtms: document.getElementById('m-unique-utms'),
766
  mConvRate: document.getElementById('m-conversion-rate'),
767
- mConvBadge: document.getElementById('m-conversion-badge'),
768
  mTotalRev: document.getElementById('m-total-revenue'),
769
  mPaidRev: document.getElementById('m-paid-revenue'),
770
- mPaidCount: document.getElementById('m-paid-count'),
771
-
772
- // Table
773
- tableBody: document.getElementById('report-body'),
774
- noDataMsg: document.getElementById('no-data-msg'),
775
 
776
- // Global
777
- themeToggle: document.getElementById('theme-toggle'),
778
- alertArea: document.getElementById('alert-area')
 
779
  };
780
  },
781
 
782
  bindEvents() {
783
- // Config
 
784
  this.dom.toggleConfig.addEventListener('click', () => {
785
- this.dom.configContent.classList.toggle('hidden');
786
  const icon = this.dom.toggleConfig.querySelector('i');
787
- icon.className = this.dom.configContent.classList.contains('hidden')
788
- ? 'ri-arrow-down-s-line'
789
- : 'ri-arrow-up-s-line';
790
  });
791
 
792
- this.dom.btnSave.addEventListener('click', () => this.saveSettings());
793
 
794
- // Data
795
  this.dom.dateRange.addEventListener('change', (e) => {
796
  this.dom.customDates.classList.toggle('hidden', e.target.value !== 'customizado');
797
  });
798
-
799
  this.dom.btnFetch.addEventListener('click', () => this.fetchData());
800
  this.dom.btnInitial.addEventListener('click', () => this.fetchData());
801
  this.dom.btnExport.addEventListener('click', () => this.exportCSV());
802
- this.dom.resetSnapshot.addEventListener('click', () => {
803
- localStorage.removeItem('utm_pro_snapshot');
804
- this.state.snapshot = null;
805
- this.dom.deltaSection.classList.add('hidden');
806
- Utils.showAlert('Base de compara莽茫o resetada.', 'info');
807
- });
808
-
809
- // Theme
810
- this.dom.themeToggle.addEventListener('click', () => this.toggleTheme());
811
- },
812
-
813
- loadSettings() {
814
- const config = ShopifyService.loadConfig();
815
- if (config) {
816
- this.dom.inpStore.value = config.storeName || '';
817
- this.dom.inpToken.value = config.accessToken || '';
818
- this.dom.inpProxy.value = config.corsProxy || '';
819
- this.enableControls();
820
- }
821
-
822
- // Load Snapshot
823
- const snap = localStorage.getItem('utm_pro_snapshot');
824
- if (snap) {
825
- this.state.snapshot = JSON.parse(snap);
826
- }
827
- },
828
-
829
- saveSettings() {
830
- const store = this.dom.inpStore.value.trim();
831
- const token = this.dom.inpToken.value.trim();
832
- const proxy = this.dom.inpProxy.value.trim();
833
-
834
- if (!store || !token) {
835
- Utils.showAlert('Preencha Nome da Loja e Token.', 'warning');
836
- return;
837
- }
838
-
839
- ShopifyService.saveConfig({ storeName: store, accessToken: token, corsProxy: proxy });
840
- this.dom.msgConfig.classList.remove('hidden');
841
- this.enableControls();
842
- setTimeout(() => this.dom.msgConfig.classList.add('hidden'), 3000);
843
- },
844
-
845
- enableControls() {
846
- this.dom.btnFetch.disabled = false;
847
- this.dom.btnInitial.disabled = false;
848
- },
849
-
850
- checkTheme() {
851
- const saved = localStorage.getItem('utm_pro_theme') || 'dark';
852
- document.documentElement.setAttribute('data-theme', saved);
853
- const icon = this.dom.themeToggle.querySelector('i');
854
- icon.className = saved === 'dark' ? 'ri-sun-line' : 'ri-moon-line';
855
  },
856
 
857
  toggleTheme() {
858
- const current = document.documentElement.getAttribute('data-theme');
859
- const next = current === 'dark' ? 'light' : 'dark';
860
- document.documentElement.setAttribute('data-theme', next);
861
- localStorage.setItem('utm_pro_theme', next);
862
  const icon = this.dom.themeToggle.querySelector('i');
863
- icon.className = next === 'dark' ? 'ri-sun-line' : 'ri-moon-line';
864
- },
865
-
866
- getDates() {
867
- const range = this.dom.dateRange.value;
868
- const now = new Date();
869
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
870
-
871
- let start, end = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1); // End of today
872
-
873
- const days = (n) => new Date(today.getTime() - n * 24 * 60 * 60 * 1000);
874
-
875
- switch (range) {
876
- case 'hoje': start = today; break;
877
- case 'ontem': start = days(1); end = days(0); break;
878
- case 'ultimos7': start = days(7); break;
879
- case 'ultimos30': start = days(30); break;
880
- case 'customizado':
881
- if(this.dom.dateStart.value && this.dom.dateEnd.value) {
882
- start = new Date(this.dom.dateStart.value);
883
- end = new Date(this.dom.dateEnd.value);
884
- end.setHours(23,59,59,999);
885
- } else {
886
- start = days(7); // Fallback
887
- }
888
- break;
889
- default: start = days(7);
890
- }
891
- return { start: start.toISOString(), end: end.toISOString() };
892
- },
893
-
894
- async fetchData() {
895
- const config = ShopifyService.loadConfig();
896
- if (!config || !config.storeName) {
897
- this.dom.configCard.scrollIntoView({ behavior: 'smooth' });
898
- Utils.showAlert('Configure a API primeiro.', 'warning');
899
- return;
900
- }
901
-
902
- this.setLoading(true);
903
- this.dom.alertArea.innerHTML = '';
904
- this.dom.deltaSection.classList.add('hidden');
905
-
906
- try {
907
- const { start, end } = this.getDates();
908
- const orders = await ShopifyService.fetchAllOrders(start, end);
909
-
910
- this.state.rawData = orders;
911
- this.processData(orders);
912
- this.checkForNewOrders(orders);
913
-
914
- // Save snapshot for next time
915
- this.state.snapshot = {
916
- timestamp: new Date().toISOString(),
917
- orderIds: orders.map(o => o.id)
918
- };
919
- localStorage.setItem('utm_pro_snapshot', JSON.stringify(this.state.snapshot));
920
-
921
- this.render();
922
-
923
- } catch (err) {
924
- Utils.showAlert(err.message, 'error');
925
- this.dom.dashboard.classList.add('hidden');
926
- this.dom.emptyState.classList.remove('hidden');
927
- } finally {
928
- this.setLoading(false);
929
- }
930
- },
931
-
932
- checkForNewOrders(currentOrders) {
933
- if (!this.state.snapshot) return;
934
-
935
- const previousIds = new Set(this
 
1
  <!DOCTYPE html>
2
  <html lang="pt-BR" data-theme="dark">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>UTM Sales Analytics Pro | Shopify</title>
7
+
8
+ <!-- Fonts: Inter for clean, modern readability -->
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
12
+
13
+ <!-- Icons: Remix Icon -->
14
  <link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
15
 
16
  <style>
17
  /* --- 1. DESIGN SYSTEM & VARIABLES --- */
18
  :root {
19
+ /* Palette: Premium Dark (Zinc/Slate) */
20
+ --bg-body: #09090b; /* Zinc 950 */
21
+ --bg-card: #121214; /* Zinc 900 */
22
+ --bg-surface: #1c1c1f; /* Zinc 800 */
23
+ --bg-input: #09090b;
24
+
25
+ --border-subtle: #1f1f23;
26
+ --border-default: #2e2e33;
27
+ --border-hover: #3f3f46;
28
 
29
  --text-main: #f4f4f5; /* Zinc 100 */
30
  --text-muted: #a1a1aa; /* Zinc 400 */
31
  --text-faint: #71717a; /* Zinc 500 */
32
 
33
  --primary: #10b981; /* Emerald 500 */
34
+ --primary-hover: #059669;
35
  --primary-dim: rgba(16, 185, 129, 0.1);
36
+ --primary-glow: rgba(16, 185, 129, 0.2);
37
+
38
+ --accent: #6366f1; /* Indigo */
39
 
40
+ --success-bg: rgba(16, 185, 129, 0.15);
41
+ --success-text: #34d399;
42
 
43
+ --warning-bg: rgba(245, 158, 11, 0.15);
44
+ --warning-text: #fbbf24;
 
45
 
46
+ --danger-bg: rgba(239, 68, 68, 0.15);
47
+ --danger-text: #f87171;
48
 
49
+ --radius-sm: 6px;
50
+ --radius-md: 10px;
51
+ --radius-lg: 14px;
52
 
53
  --font-main: 'Inter', sans-serif;
54
+ --shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.15);
55
+ --shadow-header: 0 4px 20px rgba(0,0,0,0.4);
56
+
57
+ --ease: cubic-bezier(0.16, 1, 0.3, 1);
58
  }
59
 
60
  [data-theme="light"] {
61
+ --bg-body: #f8fafc; /* Slate 50 */
62
  --bg-card: #ffffff;
63
+ --bg-surface: #f1f5f9; /* Slate 100 */
64
  --bg-input: #ffffff;
65
 
66
+ --border-subtle: #e2e8f0;
67
+ --border-default: #cbd5e1;
68
+ --border-hover: #94a3b8;
69
 
70
+ --text-main: #0f172a; /* Slate 900 */
71
+ --text-muted: #64748b; /* Slate 500 */
72
+ --text-faint: #94a3b8; /* Slate 400 */
73
 
74
+ --primary: #059669;
75
+ --primary-hover: #047857;
76
  --primary-dim: rgba(5, 150, 105, 0.1);
77
  --primary-glow: rgba(5, 150, 105, 0.15);
78
 
79
  --shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
80
+ --shadow-header: 0 4px 20px rgba(0,0,0,0.05);
81
  }
82
 
83
  /* --- 2. RESET & BASE --- */
 
87
  font-family: var(--font-main);
88
  background-color: var(--bg-body);
89
  color: var(--text-main);
90
+ line-height: 1.5;
91
  -webkit-font-smoothing: antialiased;
92
  transition: background-color 0.3s ease, color 0.3s ease;
93
+ padding-bottom: 60px;
94
  }
95
 
96
  a { text-decoration: none; color: inherit; transition: color 0.2s; }
97
  ul { list-style: none; }
98
+ button { font-family: inherit; border: none; background: none; cursor: pointer; }
99
 
100
+ /* Scrollbar */
101
  ::-webkit-scrollbar { width: 8px; height: 8px; }
102
+ ::-webkit-scrollbar-track { background: transparent; }
103
  ::-webkit-scrollbar-thumb { background: var(--border-default); border-radius: 4px; }
104
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
105
 
106
  /* --- 3. LAYOUT UTILITIES --- */
107
+ .container { max-width: 1280px; margin: 0 auto; padding: 0 24px; }
108
+ .flex { display: flex; }
109
+ .flex-col { flex-direction: column; }
110
+ .items-center { align-items: center; }
111
+ .justify-between { justify-content: space-between; }
112
+ .justify-end { justify-content: flex-end; }
113
+ .justify-center { justify-content: center; }
 
114
  .gap-2 { gap: 8px; }
115
  .gap-4 { gap: 16px; }
116
+ .gap-6 { gap: 24px; }
117
  .mt-4 { margin-top: 16px; }
118
  .mb-4 { margin-bottom: 16px; }
119
+ .w-full { width: 100%; }
120
+
121
  /* --- 4. COMPONENTS --- */
122
 
123
  /* Header */
124
  header {
125
  position: sticky;
126
  top: 0;
127
+ z-index: 50;
128
+ background: rgba(9, 9, 11, 0.8);
129
+ backdrop-filter: blur(12px);
130
+ -webkit-backdrop-filter: blur(12px);
131
  border-bottom: 1px solid var(--border-subtle);
132
  padding: 16px 0;
133
+ transition: background 0.3s;
134
  }
135
  [data-theme="light"] header { background: rgba(255, 255, 255, 0.85); }
136
 
 
139
  align-items: center;
140
  gap: 12px;
141
  font-weight: 700;
142
+ font-size: 1.25rem;
143
+ letter-spacing: -0.025em;
144
+ color: var(--text-main);
145
  }
146
+ .brand i { color: var(--primary); font-size: 1.5rem; }
147
+
148
+ .anycoder-link {
149
+ font-size: 0.8rem;
150
+ color: var(--text-muted);
151
+ display: flex;
152
+ align-items: center;
153
+ gap: 4px;
154
+ font-weight: 500;
155
+ }
156
+ .anycoder-link:hover { color: var(--primary); }
157
 
158
  /* Buttons */
159
  .btn {
 
161
  align-items: center;
162
  justify-content: center;
163
  gap: 8px;
164
+ padding: 10px 16px;
165
  border-radius: var(--radius-sm);
166
  font-weight: 500;
167
  font-size: 0.875rem;
168
+ transition: all 0.2s var(--ease);
 
 
169
  white-space: nowrap;
170
+ border: 1px solid transparent;
171
  }
172
 
173
  .btn-primary {
 
176
  box-shadow: 0 0 0 1px var(--primary-glow), 0 4px 12px var(--primary-glow);
177
  }
178
  .btn-primary:hover:not(:disabled) {
179
+ background-color: var(--primary-hover);
180
  transform: translateY(-1px);
 
 
181
  }
182
 
183
  .btn-secondary {
 
186
  color: var(--text-main);
187
  }
188
  .btn-secondary:hover:not(:disabled) {
 
189
  background-color: var(--border-subtle);
190
+ border-color: var(--text-faint);
191
  }
192
 
193
  .btn-ghost {
 
194
  color: var(--text-muted);
195
  }
196
+ .btn-ghost:hover {
197
+ color: var(--text-main);
198
+ background-color: var(--bg-surface);
199
+ }
200
+
201
+ .btn:disabled {
202
+ opacity: 0.5;
203
+ cursor: not-allowed;
204
+ transform: none !important;
205
+ box-shadow: none !important;
206
+ }
207
 
208
  /* Cards */
209
  .card {
210
  background-color: var(--bg-card);
211
  border: 1px solid var(--border-subtle);
212
  border-radius: var(--radius-md);
 
213
  box-shadow: var(--shadow-card);
214
+ padding: 24px;
215
  margin-bottom: 24px;
216
+ transition: border-color 0.2s;
 
 
217
  }
218
+ .card:hover { border-color: var(--border-default); }
219
 
220
  .card-header {
221
  display: flex;
222
  justify-content: space-between;
223
+ align-items: center;
224
  margin-bottom: 20px;
225
  }
226
  .card-title {
227
+ font-size: 1.1rem;
228
  font-weight: 600;
229
  color: var(--text-main);
230
  display: flex;
 
232
  gap: 8px;
233
  }
234
 
235
+ /* Forms */
236
  .form-grid {
237
  display: grid;
238
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
239
+ gap: 16px;
240
  }
241
+ .input-group { display: flex; flex-direction: column; gap: 6px; }
242
 
243
  label {
244
  font-size: 0.75rem;
 
252
  background-color: var(--bg-input);
253
  border: 1px solid var(--border-default);
254
  color: var(--text-main);
255
+ padding: 10px 12px;
256
  border-radius: var(--radius-sm);
257
  font-size: 0.9rem;
258
  transition: all 0.2s;
 
264
  }
265
 
266
  /* Metrics Grid */
267
+ .metrics-grid {
268
  display: grid;
269
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
270
  gap: 20px;
271
  margin-bottom: 24px;
272
  }
 
273
  .metric-card {
274
  background: linear-gradient(145deg, var(--bg-card), rgba(255,255,255,0.02));
275
  border: 1px solid var(--border-subtle);
276
  border-radius: var(--radius-md);
277
  padding: 20px;
 
 
 
278
  position: relative;
279
+ overflow: hidden;
280
  }
281
+ .metric-card::before {
 
282
  content: '';
283
  position: absolute;
284
+ top: 0; left: 0; width: 4px; height: 100%;
 
285
  background: var(--primary);
286
+ opacity: 0.6;
287
+ }
288
+ .metric-label {
289
+ font-size: 0.85rem;
290
+ color: var(--text-muted);
291
+ font-weight: 500;
292
+ margin-bottom: 8px;
293
+ }
294
+ .metric-value {
295
+ font-size: 1.8rem;
296
+ font-weight: 700;
297
+ color: var(--text-main);
298
+ letter-spacing: -0.03em;
299
+ line-height: 1.2;
300
+ }
301
+ .metric-sub {
302
+ font-size: 0.75rem;
303
+ color: var(--text-faint);
304
+ margin-top: 4px;
305
  }
 
 
 
 
306
 
307
  /* Table */
308
+ .table-wrapper {
309
  width: 100%;
310
  overflow-x: auto;
 
311
  border: 1px solid var(--border-subtle);
312
+ border-radius: var(--radius-sm);
313
  }
 
314
  table {
315
  width: 100%;
316
  border-collapse: collapse;
317
  font-size: 0.875rem;
 
318
  }
 
319
  th {
320
  background-color: var(--bg-surface);
321
  color: var(--text-muted);
322
  font-weight: 600;
323
  text-transform: uppercase;
324
+ font-size: 0.75rem;
325
  padding: 14px 16px;
326
  text-align: left;
 
327
  border-bottom: 1px solid var(--border-subtle);
328
+ white-space: nowrap;
 
 
329
  }
330
+ th.clickable { cursor: pointer; user-select: none; transition: color 0.2s; }
331
+ th.clickable:hover { color: var(--primary); }
332
 
333
  td {
334
  padding: 14px 16px;
 
342
  .text-right { text-align: right; }
343
  .text-center { text-align: center; }
344
 
345
+ /* Badges */
346
  .badge {
347
+ display: inline-flex;
348
+ align-items: center;
349
+ padding: 2px 8px;
350
+ border-radius: 4px;
351
  font-size: 0.7rem;
352
  font-weight: 600;
353
  text-transform: uppercase;
354
  }
355
+ .badge-success { background: var(--success-bg); color: var(--success-text); }
356
+ .badge-warning { background: var(--warning-bg); color: var(--warning-text); }
357
+ .badge-danger { background: var(--danger-bg); color: var(--danger-text); }
358
+ .badge-neutral { background: var(--bg-surface); color: var(--text-muted); border: 1px solid var(--border-default); }
 
 
 
 
 
 
 
359
 
360
+ /* Alerts */
361
  .alert {
362
  padding: 12px 16px;
363
  border-radius: var(--radius-sm);
364
+ margin-bottom: 16px;
365
  display: flex;
366
  align-items: center;
367
  gap: 12px;
368
+ font-size: 0.9rem;
369
  border: 1px solid transparent;
370
  }
371
  .alert-info { background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.2); color: #60a5fa; }
372
+ .alert-warning { background: var(--warning-bg); border-color: rgba(245, 158, 11, 0.2); color: var(--warning-text); }
373
+ .alert-error { background: var(--danger-bg); border-color: rgba(239, 68, 68, 0.2); color: var(--danger-text); }
374
 
375
+ /* Loading & States */
376
+ .loading-overlay {
377
+ display: flex;
378
+ flex-direction: column;
379
+ align-items: center;
380
+ justify-content: center;
381
+ padding: 40px;
382
+ color: var(--text-muted);
383
+ gap: 16px;
384
+ }
385
  .spinner {
386
+ width: 32px;
387
+ height: 32px;
388
+ border: 3px solid var(--bg-surface);
389
  border-top-color: var(--primary);
390
  border-radius: 50%;
391
  animation: spin 0.8s linear infinite;
392
  }
393
+ @keyframes spin { to { transform: rotate(360deg); } }
394
 
395
+ .empty-state {
396
+ text-align: center;
397
+ padding: 40px 20px;
398
+ color: var(--text-muted);
399
+ }
400
+ .empty-state i { font-size: 2.5rem; opacity: 0.3; margin-bottom: 12px; display: block; }
401
+
402
+ /* Progress Bar */
403
+ .progress-bar-bg {
404
+ width: 100px;
405
+ height: 6px;
406
+ background: var(--bg-surface);
407
+ border-radius: 3px;
408
+ overflow: hidden;
409
+ }
410
+ .progress-bar-fill {
411
+ height: 100%;
412
+ background: var(--primary);
413
+ border-radius: 3px;
414
+ }
415
 
416
+ .hidden { display: none !important; }
417
+
418
+ /* Custom Delta Section Styling */
419
  .delta-card {
420
  border: 1px solid rgba(16, 185, 129, 0.3);
421
+ background: radial-gradient(circle at top right, rgba(16, 185, 129, 0.08), transparent 50%);
422
  }
 
423
  .delta-item {
424
+ display: flex;
425
+ justify-content: space-between;
426
+ align-items: center;
427
  padding: 8px 12px;
428
  background: var(--bg-surface);
429
  border-radius: var(--radius-sm);
430
+ margin-bottom: 8px;
431
  font-size: 0.85rem;
432
  }
433
 
 
 
 
 
 
434
  /* Responsive */
435
  @media (max-width: 768px) {
436
+ .header-content { flex-direction: column; gap: 12px; align-items: flex-start; }
437
+ .metrics-grid { grid-template-columns: 1fr; }
438
+ .card-header { flex-direction: column; align-items: flex-start; gap: 12px; }
439
+ .controls-row { width: 100%; flex-direction: column; }
440
  .controls-row > * { width: 100%; }
441
  }
442
  </style>
443
  </head>
444
 
445
  <body>
446
+
447
  <!-- HEADER -->
448
  <header>
449
+ <div class="container header-content flex justify-between items-center">
450
  <div class="brand">
451
+ <i class="ri-bar-chart-box-fill"></i>
452
  <span>UTM Analytics Pro</span>
453
  </div>
454
 
455
+ <div class="flex items-center gap-4">
456
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
457
  Built with anycoder <i class="ri-external-link-line"></i>
458
  </a>
459
+ <button id="theme-toggle" class="btn btn-ghost" style="padding: 8px;">
460
  <i class="ri-sun-line"></i>
461
+ </button>
462
  </div>
463
  </div>
464
  </header>
465
 
466
  <main class="container" style="padding-top: 32px;">
467
+
468
+ <!-- 1. CONFIGURATION -->
469
+ <section class="card" id="config-section">
470
  <div class="card-header">
471
  <div class="card-title">
472
+ <i class="ri-settings-4-line"></i> Configura莽茫o da API
473
  </div>
474
+ <button id="toggle-config" class="btn btn-ghost" style="font-size: 0.8rem;">
475
+ <i class="ri-arrow-down-s-line"></i> Expandir
476
  </button>
477
  </div>
478
 
479
+ <div id="config-content" class="hidden mt-4">
480
  <div class="form-grid mb-4">
481
  <div class="input-group">
482
  <label for="store-name">Nome da Loja</label>
483
  <input type="text" id="store-name" placeholder="ex: minha-loja">
484
  </div>
485
  <div class="input-group">
486
+ <label for="access-token">Access Token (Admin API)</label>
487
  <input type="password" id="access-token" placeholder="shpat_xxxxx...">
488
  </div>
489
  <div class="input-group">
 
491
  <input type="text" id="cors-proxy" placeholder="https://corsproxy.io/?">
492
  </div>
493
  </div>
494
+ <div class="flex justify-between items-center">
495
+ <span id="config-status" style="font-size: 0.8rem; color: var(--success-text);" class="hidden">
496
+ <i class="ri-checkbox-circle-line"></i> Configura莽玫es salvas
497
+ </span>
498
+ <button id="save-config" class="btn btn-primary">
499
+ <i class="ri-save-line"></i> Salvar Configura莽玫es
500
  </button>
501
  </div>
502
  </div>
503
  </section>
504
 
505
+ <!-- 2. CONTROLS -->
506
  <section class="card">
507
  <div class="form-grid" style="align-items: end;">
508
  <div class="input-group">
509
+ <label for="date-range">Per铆odo</label>
510
  <select id="date-range">
511
  <option value="hoje">Hoje</option>
512
  <option value="ontem">Ontem</option>
 
516
  </select>
517
  </div>
518
 
519
+ <div class="input-group hidden" id="custom-dates-container">
520
+ <label>Intervalo Personalizado</label>
521
+ <div class="flex gap-2">
522
  <input type="date" id="date-start">
523
  <input type="date" id="date-end">
524
  </div>
525
  </div>
526
 
527
+ <div class="controls-row flex gap-4 justify-end w-full">
528
  <button id="btn-fetch" class="btn btn-primary" disabled>
529
  <i class="ri-refresh-line"></i> Atualizar Dados
530
  </button>
531
  <button id="btn-export" class="btn btn-secondary" disabled>
532
+ <i class="ri-download-cloud-2-line"></i> Exportar CSV
533
  </button>
534
  </div>
535
  </div>
536
  </section>
537
 
538
+ <!-- ALERTS CONTAINER -->
539
+ <div id="alert-container"></div>
540
 
541
+ <!-- 3. NEW ORDERS DELTA -->
542
  <section id="delta-section" class="card delta-card hidden animate-fade-in">
543
  <div class="card-header">
544
  <div class="card-title" style="color: var(--primary);">
545
  <i class="ri-flashlight-fill"></i> Novas Vendas Detectadas
546
  </div>
547
+ <div class="flex items-center gap-4">
548
+ <span style="font-size: 0.8rem; color: var(--text-muted);">
549
+ Base: <span id="snapshot-time" style="color: var(--text-main);">--</span>
550
+ </span>
551
+ <button id="reset-snapshot" class="btn btn-ghost" style="font-size: 0.75rem; padding: 4px 8px;">
552
+ Resetar Base
553
+ </button>
554
+ </div>
555
  </div>
556
+
557
+ <div class="flex gap-6 mb-4">
558
+ <div>
559
+ <div style="font-size: 0.8rem; color: var(--text-muted);">Total Novos Pedidos</div>
560
+ <div style="font-size: 1.5rem; font-weight: 700;" id="new-orders-count">0</div>
561
+ </div>
562
+ <div>
563
+ <div style="font-size: 0.8rem; color: var(--text-muted);">Valor Novo (Bruto)</div>
564
+ <div style="font-size: 1.5rem; font-weight: 700;" id="new-orders-total">R$ 0,00</div>
565
+ </div>
566
  </div>
 
567
 
568
+ <div id="delta-list" class="flex-col gap-2"></div>
569
+ </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
570
 
571
+ <!-- 4. METRICS (Visible after load) -->
572
+ <section id="metrics-section" class="hidden">
573
+ <div class="metrics-grid">
 
 
574
  <div class="metric-card">
575
+ <div class="metric-label">Total de Pedidos</div>
576
+ <div class="metric-value" id="m-total-orders">0</div>
577
+ <div class="metric-sub">UTMs 脷nicas: <span id="m-unique-utms">0</span></div>
578
  </div>
579
  <div class="metric-card">
580
+ <div class="metric-label">Taxa de Pagamento</div>
581
+ <div class="metric-value" id="m-conversion-rate">0%</div>
582
+ <div class="metric-sub"><span id="m-paid-count" class="badge badge-success">0 pagos</span></div>
583
  </div>
584
  <div class="metric-card">
585
+ <div class="metric-label">Venda Bruta</div>
586
+ <div class="metric-value" id="m-total-revenue">R$ 0,00</div>
587
+ <div class="metric-sub">Todos os pedidos</div>
588
  </div>
589
  <div class="metric-card">
590
+ <div class="metric-label">Venda Paga</div>
591
+ <div class="metric-value" style="color: var(--primary);" id="m-paid-revenue">R$ 0,00</div>
592
+ <div class="metric-sub">Dinheiro confirmado</div>
593
  </div>
594
+ </div>
595
+ </section>
596
 
597
+ <!-- 5. DATA TABLE -->
598
+ <section class="card">
599
+ <div class="card-header">
600
+ <div class="card-title">
601
+ <i class="ri-table-line"></i> Detalhamento por Campanha (UTM Content)
 
602
  </div>
603
+ </div>
604
+
605
+ <!-- Loading State -->
606
+ <div id="loading-state" class="loading-overlay hidden">
607
+ <div class="spinner"></div>
608
+ <p>Processando pedidos e UTMs...</p>
609
+ </div>
610
+
611
+ <!-- Empty State -->
612
+ <div id="empty-state" class="empty-state">
613
+ <i class="ri-database-2-line"></i>
614
+ <h3>Pronto para Analisar</h3>
615
+ <p style="max-width: 400px; margin: 8px auto;">
616
+ Configure suas credenciais e clique em "Atualizar Dados" para gerar o relat贸rio.
617
+ </p>
618
+ <button id="btn-initial-connect" class="btn btn-primary mt-4">Conectar e Buscar</button>
619
+ </div>
620
+
621
+ <!-- Table Content -->
622
+ <div id="table-container" class="hidden">
623
+ <div class="table-wrapper">
624
  <table>
625
  <thead>
626
  <tr>
627
+ <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>
628
+ <th class="text-center">Pedidos <span style="font-weight:400; font-size:0.7em; opacity:0.7;">(Pagos/Total)</span></th>
629
+ <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>
630
+ <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>
631
+ <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>
632
+ <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>
633
  </tr>
634
  </thead>
635
  <tbody id="report-body">
636
+ <!-- Rows -->
637
  </tbody>
638
  </table>
639
  </div>
640
+ </div>
641
+ </section>
 
 
 
 
642
 
643
  </main>
644
 
645
  <script>
646
  /**
647
+ * CORE LOGIC
648
+ * Translating the Preact logic to Vanilla JS
649
  */
650
+
651
  const Utils = {
652
  formatCurrency(value) {
653
  return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
 
655
  formatDate(isoString) {
656
  return new Date(isoString).toLocaleString('pt-BR');
657
  },
658
+ showAlert(msg, type = 'info') {
659
+ const container = document.getElementById('alert-container');
660
+ const el = document.createElement('div');
661
+ el.className = `alert alert-${type} animate-fade-in`;
662
 
663
  let icon = 'ri-information-line';
664
  if(type === 'error') icon = 'ri-error-warning-line';
665
  if(type === 'warning') icon = 'ri-alert-line';
666
 
667
+ el.innerHTML = `<i class="${icon}" style="font-size:1.2em;"></i> <span>${msg}</span>`;
668
+ container.innerHTML = '';
669
+ container.appendChild(el);
670
+
 
671
  setTimeout(() => {
672
+ el.style.opacity = '0';
673
+ setTimeout(() => el.remove(), 300);
674
  }, 5000);
675
  }
676
  };
677
 
 
 
 
 
678
  const ShopifyService = {
679
  config: { storeName: '', accessToken: '', corsProxy: '', apiVersion: '2024-01' },
680
 
681
  loadConfig() {
682
  const stored = localStorage.getItem('utm_pro_config');
683
+ if (stored) this.config = { ...this.config, ...JSON.parse(stored) };
684
+ return this.config;
 
 
 
685
  },
686
 
687
  saveConfig(newConfig) {
 
689
  localStorage.setItem('utm_pro_config', JSON.stringify(this.config));
690
  },
691
 
692
+ buildUrl(startDate, endDate, pageInfo = '') {
693
  const baseUrl = `https://${this.config.storeName}.myshopify.com/admin/api/${this.config.apiVersion}/orders.json`;
694
  const params = new URLSearchParams({
695
  status: 'any',
696
  created_at_min: startDate,
697
  created_at_max: endDate,
698
  limit: '250',
699
+ fields: 'id,name,total_price,financial_status,customer,created_at,processing_method,custom_attributes'
700
  });
701
+ if (pageInfo) params.append('page_info', pageInfo);
 
702
 
703
  let finalUrl = `${baseUrl}?${params.toString()}`;
704
 
 
710
  },
711
 
712
  async fetchAllOrders(startDate, endDate) {
713
+ if (!this.config.storeName || !this.config.accessToken) throw new Error("Credenciais ausentes.");
 
 
714
 
715
  let orders = [];
716
  let url = this.buildUrl(startDate, endDate);
 
725
  if (!response.ok) {
726
  if (response.status === 401) throw new Error("Token inv谩lido (401).");
727
  if (response.status === 404) throw new Error("Loja n茫o encontrada (404).");
728
+ throw new Error(`Erro na API Shopify: ${response.status}`);
729
  }
730
 
731
  const data = await response.json();
732
  if (data.orders) orders = orders.concat(data.orders);
733
 
 
734
  const linkHeader = response.headers.get('Link');
735
  url = linkHeader ? this.parseLinkHeader(linkHeader, 'next') : null;
736
 
737
  } catch (error) {
 
738
  if (error.message.includes('Failed to fetch')) {
739
+ throw new Error("Erro de CORS ou Rede. Tente usar um CORS Proxy.");
740
  }
741
  throw error;
742
  }
 
756
  }
757
  };
758
 
 
 
 
759
  const app = {
760
  state: {
761
  theme: 'dark',
762
  rawData: [],
763
  processedData: [],
764
+ snapshot: null, // { timestamp: '', orderIds: [] }
765
  sortCol: 'paid',
766
+ sortAsc: false,
767
+ hasLoaded: false
768
  },
769
 
770
  init() {
771
  this.cacheDOM();
772
  this.bindEvents();
773
+ this.loadTheme();
774
+
775
+ const config = ShopifyService.loadConfig();
776
+ if (config.storeName && config.accessToken) {
777
+ this.dom.inpStore.value = config.storeName;
778
+ this.dom.inpToken.value = config.accessToken;
779
+ this.dom.inpProxy.value = config.corsProxy || '';
780
+ this.enableControls();
781
+ }
782
+
783
+ // Load snapshot
784
+ const snap = localStorage.getItem('utm_pro_snapshot');
785
+ if (snap) this.state.snapshot = JSON.parse(snap);
786
  },
787
 
788
  cacheDOM() {
789
  this.dom = {
790
+ themeToggle: document.getElementById('theme-toggle'),
791
+ configSection: document.getElementById('config-content'),
792
+ toggleConfig: document.getElementById('toggle-config'),
 
793
  inpStore: document.getElementById('store-name'),
794
  inpToken: document.getElementById('access-token'),
795
  inpProxy: document.getElementById('cors-proxy'),
796
+ btnSaveConfig: document.getElementById('save-config'),
797
+ configStatus: document.getElementById('config-status'),
798
 
 
799
  dateRange: document.getElementById('date-range'),
800
+ customDates: document.getElementById('custom-dates-container'),
801
  dateStart: document.getElementById('date-start'),
802
  dateEnd: document.getElementById('date-end'),
803
+
804
  btnFetch: document.getElementById('btn-fetch'),
805
  btnInitial: document.getElementById('btn-initial-connect'),
806
  btnExport: document.getElementById('btn-export'),
807
 
808
+ alertContainer: document.getElementById('alert-container'),
809
+
 
 
810
  deltaSection: document.getElementById('delta-section'),
 
811
  snapshotTime: document.getElementById('snapshot-time'),
812
  newOrdersCount: document.getElementById('new-orders-count'),
813
+ newOrdersTotal: document.getElementById('new-orders-total'),
814
+ deltaList: document.getElementById('delta-list'),
815
  resetSnapshot: document.getElementById('reset-snapshot'),
816
 
817
+ metricsSection: document.getElementById('metrics-section'),
818
  mTotalOrders: document.getElementById('m-total-orders'),
819
  mUniqueUtms: document.getElementById('m-unique-utms'),
820
  mConvRate: document.getElementById('m-conversion-rate'),
821
+ mPaidCount: document.getElementById('m-paid-count'),
822
  mTotalRev: document.getElementById('m-total-revenue'),
823
  mPaidRev: document.getElementById('m-paid-revenue'),
 
 
 
 
 
824
 
825
+ loadingState: document.getElementById('loading-state'),
826
+ emptyState: document.getElementById('empty-state'),
827
+ tableContainer: document.getElementById('table-container'),
828
+ tableBody: document.getElementById('report-body')
829
  };
830
  },
831
 
832
  bindEvents() {
833
+ this.dom.themeToggle.addEventListener('click', () => this.toggleTheme());
834
+
835
  this.dom.toggleConfig.addEventListener('click', () => {
836
+ this.dom.configSection.classList.toggle('hidden');
837
  const icon = this.dom.toggleConfig.querySelector('i');
838
+ icon.className = this.dom.configSection.classList.contains('hidden')
839
+ ? 'ri-arrow-down-s-line' : 'ri-arrow-up-s-line';
 
840
  });
841
 
842
+ this.dom.btnSaveConfig.addEventListener('click', () => this.saveConfig());
843
 
 
844
  this.dom.dateRange.addEventListener('change', (e) => {
845
  this.dom.customDates.classList.toggle('hidden', e.target.value !== 'customizado');
846
  });
847
+
848
  this.dom.btnFetch.addEventListener('click', () => this.fetchData());
849
  this.dom.btnInitial.addEventListener('click', () => this.fetchData());
850
  this.dom.btnExport.addEventListener('click', () => this.exportCSV());
851
+ this.dom.resetSnapshot.addEventListener('click', () => this.resetSnapshot());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
852
  },
853
 
854
  toggleTheme() {
855
+ this.state.theme = this.state.theme === 'dark' ? 'light' : 'dark';
856
+ document.documentElement.setAttribute('data-theme', this.state.theme);
857
+ localStorage.setItem('utm_pro_theme', this.state.theme);
 
858
  const icon = this.dom.themeToggle.querySelector('i');
859
+ icon.className = this.state.theme === 'dark' ? 'ri-s