eubottura commited on
Commit
8a76265
·
verified ·
1 Parent(s): aa7c668

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +476 -495
index.html CHANGED
@@ -1,75 +1,115 @@
1
  <!DOCTYPE html>
2
- <html lang="pt-BR">
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 por UTM - Shopify Integration</title>
8
- <!-- Importing Remix Icon for modern UI icons -->
 
 
9
  <link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
10
 
11
  <style>
 
12
  :root {
13
- /* Shopify Polaris-inspired Color Palette */
14
- --color-bg-surface: #ffffff;
15
- --color-bg-surface-hover: #f6f6f7;
16
- --color-bg-surface-subdued: #f1f2f3;
17
- --color-text-primary: #202223;
18
- --color-text-secondary: #6d7175;
19
- --color-border: #e1e3e5;
20
- --color-border-hover: #babfc3;
21
-
22
- --color-primary: #008060;
23
- --color-primary-hover: #004c3f;
24
- --color-bg-primary: #008060;
25
- --color-text-on-primary: #ffffff;
26
-
27
- --color-critical: #d82c0d;
28
- --color-warning: #ffc453;
29
- --color-warning-bg: #fffaed;
30
- --color-success: #008060;
31
- --color-success-bg: #e4f5ec;
32
- --color-info: #202223;
33
-
34
- --font-family: -apple-system, BlinkMacSystemFont, "San Francisco", "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
35
- --border-radius: 8px;
36
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
37
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
38
-
39
- --spacing-base: 16px;
40
- --spacing-large: 24px;
 
 
 
41
  }
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  * {
44
  box-sizing: border-box;
45
  margin: 0;
46
  padding: 0;
 
47
  }
48
 
49
  body {
50
  font-family: var(--font-family);
51
- background-color: var(--color-bg-surface-subdued);
52
- color: var(--color-text-primary);
53
  line-height: 1.5;
54
  -webkit-font-smoothing: antialiased;
55
- padding-bottom: 40px;
 
56
  }
57
 
58
- /* Layout & Containers */
 
 
 
59
  .container {
60
- max-width: 1200px;
61
  margin: 0 auto;
62
- padding: 0 var(--spacing-base);
63
  }
64
 
 
65
  header {
66
- background-color: var(--color-bg-surface);
67
- border-bottom: 1px solid var(--color-border);
68
- padding: var(--spacing-base) 0;
69
- margin-bottom: var(--spacing-base);
70
  position: sticky;
71
  top: 0;
72
- z-index: 100;
 
 
 
 
 
73
  }
74
 
75
  .header-content {
@@ -78,553 +118,544 @@
78
  align-items: center;
79
  }
80
 
81
- h1 {
82
- font-size: 1.25rem;
83
- font-weight: 600;
84
- }
85
-
86
- .anycoder-link {
87
- font-size: 0.875rem;
88
- color: var(--color-text-secondary);
89
- text-decoration: none;
90
  display: flex;
91
  align-items: center;
92
- gap: 4px;
93
- transition: color 0.2s;
94
  }
95
 
96
- .anycoder-link:hover {
 
97
  color: var(--color-primary);
98
  }
99
 
100
- /* Sections */
101
- section {
102
- background: var(--color-bg-surface);
103
- border-radius: var(--border-radius);
104
- box-shadow: var(--shadow-sm);
105
- padding: var(--spacing-large);
106
- margin-bottom: var(--spacing-base);
107
- border: 1px solid var(--color-border);
108
- }
109
-
110
- .section-header {
111
- margin-bottom: var(--spacing-base);
112
- display: flex;
113
- justify-content: space-between;
114
- align-items: center;
115
- }
116
-
117
- h2 {
118
  font-size: 1.125rem;
119
- font-weight: 600;
120
- margin-bottom: 0.5rem;
121
- }
122
-
123
- /* Controls */
124
- .controls {
125
- display: flex;
126
- flex-wrap: wrap;
127
- gap: var(--spacing-base);
128
- align-items: flex-end;
129
  }
130
 
131
- .control-group {
132
  display: flex;
133
- flex-direction: column;
134
- gap: 6px;
135
- flex: 1;
136
- min-width: 200px;
137
- }
138
-
139
- label {
140
- font-size: 0.875rem;
141
- font-weight: 500;
142
- color: var(--color-text-primary);
143
- }
144
-
145
- select,
146
- input[type="text"],
147
- input[type="password"],
148
- input[type="date"] {
149
- padding: 8px 12px;
150
- border: 1px solid var(--color-border);
151
- border-radius: 4px;
152
- font-size: 1rem;
153
- background-color: var(--color-bg-surface);
154
- width: 100%;
155
- }
156
-
157
- select:focus,
158
- input:focus {
159
- outline: 2px solid var(--color-primary);
160
- outline-offset: -1px;
161
- border-color: transparent;
162
  }
163
 
164
- /* Buttons */
165
  .btn {
166
  display: inline-flex;
167
  align-items: center;
168
  justify-content: center;
169
  gap: 8px;
170
- padding: 8px 16px;
171
- border-radius: 4px;
172
  font-weight: 500;
173
- font-size: 1rem;
174
  cursor: pointer;
175
- transition: all 0.2s;
176
  border: 1px solid transparent;
177
- text-decoration: none;
178
- }
179
-
180
- .btn:disabled {
181
- opacity: 0.6;
182
- cursor: not-allowed;
183
  }
184
 
185
  .btn-primary {
186
- background-color: var(--color-bg-primary);
187
- color: var(--color-text-on-primary);
 
188
  }
189
 
190
  .btn-primary:hover:not(:disabled) {
191
  background-color: var(--color-primary-hover);
 
192
  }
193
 
194
  .btn-secondary {
195
- background-color: var(--color-bg-surface);
196
- border-color: var(--color-border);
197
- color: var(--color-text-primary);
198
  }
199
 
200
  .btn-secondary:hover:not(:disabled) {
201
- background-color: var(--color-bg-surface-hover);
202
- border-color: var(--color-border-hover);
203
  }
204
 
205
  .btn-ghost {
206
  background: transparent;
207
- color: var(--color-text-secondary);
208
- padding: 4px 8px;
209
  }
210
-
211
  .btn-ghost:hover {
212
- background: var(--color-bg-surface-hover);
213
- color: var(--color-text-primary);
214
  }
215
 
216
- /* Banners */
217
- .banner {
218
- padding: 12px 16px;
219
- border-radius: var(--border-radius);
220
- margin-bottom: var(--spacing-base);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  display: flex;
222
- gap: 12px;
223
- align-items: flex-start;
224
- font-size: 0.9375rem;
225
  }
226
 
227
- .banner-warning {
228
- background-color: var(--color-warning-bg);
229
- color: #5c4508;
230
- border: 1px solid #f0a83b;
 
 
 
 
231
  }
232
 
233
- .banner-critical {
234
- background-color: #fff1f0;
235
- color: #840505;
236
- border: 1px solid #d82c0d;
 
237
  }
238
 
239
- .banner-info {
240
- background-color: #edf7fe;
241
- color: #042a46;
242
- border: 1px solid #007ace;
 
 
 
243
  }
244
 
245
- .banner-icon {
246
- font-size: 1.25rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  }
248
 
249
- /* Metrics Grid */
 
 
 
 
 
250
  .metrics-grid {
251
  display: grid;
252
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
253
- gap: var(--spacing-base);
254
  }
255
 
256
  .metric-card {
257
- padding: var(--spacing-base);
258
- background-color: var(--color-bg-surface);
259
- border: 1px solid var(--color-border);
260
- border-radius: var(--border-radius);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  }
262
 
263
  .metric-label {
264
- font-size: 0.875rem;
265
- color: var(--color-text-secondary);
266
- margin-bottom: 4px;
267
  }
268
 
269
  .metric-value {
270
- font-size: 1.5rem;
271
- font-weight: 600;
272
- color: var(--color-text-primary);
 
273
  }
274
 
275
  .metric-sub {
276
- font-size: 0.8125rem;
277
- color: var(--color-text-secondary);
278
- margin-top: 4px;
279
  }
280
 
281
- /* Table */
282
- .table-container {
283
  width: 100%;
284
  overflow-x: auto;
285
- border: 1px solid var(--color-border);
286
- border-radius: var(--border-radius);
287
  }
288
 
289
  table {
290
  width: 100%;
291
  border-collapse: collapse;
292
- font-size: 0.9375rem;
 
293
  }
294
 
295
  th {
296
- background-color: var(--color-bg-surface-subdued);
297
- text-align: left;
298
- padding: 12px 16px;
299
  font-weight: 600;
300
- color: var(--color-text-secondary);
301
- border-bottom: 1px solid var(--color-border);
302
- white-space: nowrap;
 
 
 
303
  cursor: pointer;
304
  user-select: none;
305
  }
306
 
307
- th:hover {
308
- background-color: #e3e4e5;
309
- }
310
 
311
  td {
312
- padding: 12px 16px;
313
- border-bottom: 1px solid var(--color-border);
314
- vertical-align: middle;
315
  }
316
 
317
- tr:last-child td {
318
- border-bottom: none;
 
 
319
  }
320
-
321
- tr.total-row td {
322
- background-color: var(--color-bg-surface-subdued);
323
- font-weight: 600;
324
- border-top: 2px solid var(--color-border);
325
  }
326
 
327
- /* Badges & Indicators */
 
 
 
328
  .badge {
329
- display: inline-flex;
330
- align-items: center;
331
- padding: 2px 8px;
332
- border-radius: 12px;
333
- font-size: 0.75rem;
334
  font-weight: 600;
335
  text-transform: uppercase;
336
  }
337
 
338
- .badge-success {
339
- background-color: var(--color-success-bg);
340
- color: var(--color-success);
341
- }
342
 
343
- .badge-warning {
344
- background-color: var(--color-warning-bg);
345
- color: #805b00;
 
 
 
 
 
 
 
346
  }
347
 
348
- .badge-critical {
349
- background-color: #fff1f0;
350
- color: var(--color-critical);
 
351
  }
352
 
353
- .status-bar-container {
354
- width: 120px;
355
- height: 8px;
356
- background-color: #e1e3e5;
357
- border-radius: 4px;
358
- overflow: hidden;
359
- display: inline-block;
360
- vertical-align: middle;
361
  }
362
 
363
- .status-bar-fill {
364
- height: 100%;
365
- background-color: var(--color-success);
 
366
  }
367
 
368
- /* Utility */
369
- .text-subdued {
370
- color: var(--color-text-secondary);
 
 
 
 
 
 
371
  }
 
 
372
 
373
- .text-strong {
374
- font-weight: 600;
 
 
 
 
 
375
  }
 
376
 
377
- .hidden {
378
- display: none !important;
 
 
 
379
  }
380
 
381
- /* Loading Spinner */
382
- .spinner {
383
- border: 3px solid rgba(0, 0, 0, 0.1);
384
- width: 24px;
385
- height: 24px;
386
  border-radius: 50%;
387
- border-left-color: var(--color-primary);
388
- animation: spin 1s linear infinite;
389
- }
390
-
391
- @keyframes spin {
392
- 0% { transform: rotate(0deg); }
393
- 100% { transform: rotate(360deg); }
394
  }
395
 
396
- /* Config Section Styles */
397
- .config-grid {
398
- display: grid;
399
- grid-template-columns: 1fr 1fr;
400
- gap: 16px;
401
- }
402
-
403
- .config-help {
404
- font-size: 0.8rem;
405
- color: var(--color-text-secondary);
406
- margin-top: 4px;
407
- }
408
 
409
- /* Responsive adjustments */
410
  @media (max-width: 768px) {
411
- .config-grid {
412
- grid-template-columns: 1fr;
413
- }
414
- .controls {
415
- flex-direction: column;
416
- align-items: stretch;
417
- }
418
- .header-content {
419
- flex-direction: column;
420
- gap: 10px;
421
- text-align: center;
422
- }
423
  }
424
  </style>
425
  </head>
426
 
427
  <body>
428
 
 
429
  <header>
430
  <div class="container header-content">
431
- <h1>Relatório Shopify: Vendas vs. Pagamentos por UTM</h1>
432
- <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
433
- Built with anycoder <i class="ri-external-link-line"></i>
434
- </a>
 
 
 
 
 
 
 
 
 
435
  </div>
436
  </header>
437
 
438
  <main class="container">
439
 
440
- <!-- API Configuration Section -->
441
- <section id="config-section">
442
- <div class="section-header">
443
- <h2>🔌 Configuração da API Shopify</h2>
444
- <button id="toggle-config-btn" class="btn btn-ghost" style="font-size: 0.875rem;">
445
- <i class="ri-settings-3-line"></i> Ocultar
 
 
446
  </button>
447
  </div>
448
-
449
- <div id="config-form" class="config-grid">
450
- <div class="control-group">
451
  <label for="store-name">Nome da Loja</label>
452
- <input type="text" id="store-name" placeholder="ex: minha-loja (sem .myshopify.com)">
453
- <p class="config-help">O nome que aparece na URL da sua admin.</p>
454
  </div>
455
-
456
- <div class="control-group">
457
- <label for="access-token">Access Token (Admin API)</label>
458
  <input type="password" id="access-token" placeholder="shpat_xxxxx...">
459
- <p class="config-help">Token de acesso de App Customizado ou Privado.</p>
460
  </div>
461
 
462
- <div class="control-group">
463
  <label for="cors-proxy">Proxy CORS (Opcional)</label>
464
- <input type="text" id="cors-proxy" placeholder="https://cors-anywhere.herokuapp.com/">
465
- <p class="config-help">⚠️ Necessário rodar localmente. Deixe vazio se tiver backend.</p>
466
  </div>
467
 
468
- <div class="control-group" style="justify-content: flex-end;">
469
  <button id="save-config-btn" class="btn btn-primary">
470
- <i class="ri-save-line"></i> Salvar Credenciais
471
  </button>
472
  </div>
473
  </div>
474
-
475
- <div id="config-status" class="hidden banner banner-info" style="margin-top: 12px;">
476
- <i class="ri-checkbox-circle-fill banner-icon"></i>
477
- <span>Credenciais salvas com sucesso no navegador.</span>
478
- </div>
479
- </section>
480
 
481
- <!-- Banner Limitação -->
482
- <div id="limitation-banner" class="banner banner-warning">
483
- <i class="ri-alert-fill banner-icon"></i>
484
- <div>
485
- <p class="text-strong">📌 Modo de Análise</p>
486
- <p>Este relatório processa os pedidos retornados pela API Shopify e os agrupa pelo atributo <code>utm_content</code>.</p>
487
  </div>
488
- </div>
489
 
490
- <!-- Controles -->
491
- <section id="filters-section">
492
- <div class="controls">
493
- <div class="control-group">
494
  <label for="date-range-select">Período</label>
495
  <select id="date-range-select">
496
  <option value="hoje">Hoje</option>
497
  <option value="ontem">Ontem</option>
498
  <option value="ultimos7">Últimos 7 dias</option>
499
  <option value="ultimos30">Últimos 30 dias</option>
500
- <option value="customizado">Período customizado</option>
501
  </select>
502
  </div>
503
 
504
- <div class="control-group hidden" id="custom-date-group">
505
- <label for="custom-date-picker">Data Início - Fim</label>
506
- <input type="date" id="custom-date-start">
507
- <input type="date" id="custom-date-end" style="margin-top: 5px;">
 
 
508
  </div>
509
 
510
- <div style="display: flex; gap: 12px; margin-top: auto;">
511
  <button id="refresh-button" class="btn btn-primary" disabled>
512
- <i class="ri-refresh-line"></i> Buscar Dados Shopify
513
  </button>
514
  <button id="export-csv-button" class="btn btn-secondary" disabled>
515
- <i class="ri-download-line"></i> Exportar CSV
516
  </button>
517
  </div>
518
  </div>
519
  </section>
520
 
521
- <!-- Erro -->
522
- <div id="error-banner" class="banner banner-critical hidden">
523
- <i class="ri-error-warning-fill banner-icon"></i>
524
- <div>
525
- <p class="text-strong">Erro na Requisição</p>
526
- <span id="error-message"></span>
527
- </div>
528
- </div>
529
-
530
- <!-- Info Snapshot -->
531
- <div id="last-update-info" class="hidden">
532
- <div style="background: var(--color-bg-surface-subdued); padding: 12px; border-radius: 8px; border: 1px solid var(--color-border);">
533
- <p class="text-strong">📅 Última Sincronização: <span id="snapshot-time"></span></p>
534
- <p class="text-subdued">Dados vindos da Shopify</p>
535
- </div>
536
  </div>
537
 
538
- <!-- Dia Mudou -->
539
- <div id="day-changed-banner" class="banner banner-info hidden">
540
- <i class="ri-sun-fill banner-icon"></i>
541
- <div>
542
- <p class="text-strong">🌅 Novo dia detectado</p>
543
- <p>Clique em 'Buscar Dados' para atualizar com as vendas mais recentes.</p>
544
- </div>
545
  </div>
546
 
547
- <!-- Estado Vazio Inicial -->
548
- <div id="no-data-section" class="hidden" style="text-align: center; padding: 40px;">
549
- <h2>Pronto para conectar</h2>
550
- <p class="text-subdued" style="margin-bottom: 20px;">Configure as credenciais da Shopify acima e clique em buscar.</p>
551
- <button id="initial-load-button" class="btn btn-primary">🔄 Buscar Dados Shopify</button>
 
552
  </div>
553
 
554
- <!-- Novos Pedidos (Delta) -->
555
- <div id="new-orders-section" class="hidden">
556
- <div style="background: var(--color-bg-surface-subdued); padding: 16px; border-radius: 8px; border: 1px solid var(--color-border);">
557
- <h2>🔔 Novas Vendas desde a Última Atualização</h2>
558
- <div style="margin-top: 12px;">
559
- <p><span class="text-strong">⏱️ Tempo desde última atualização:</span> <span id="diff-time"></span></p>
560
- <div id="new-orders-content" style="margin-top: 10px;"></div>
561
  </div>
562
- <div id="campaign-breakdown" style="margin-top: 16px;"></div>
563
- <hr style="border: 0; border-top: 1px solid var(--color-border); margin: 16px 0;">
564
- <button id="reset-snapshot-button" class="btn btn-secondary" style="font-size: 0.875rem;">🔄 Resetar Ponto de Partida</button>
 
 
 
565
  </div>
566
  </div>
567
 
568
- <!-- Métricas -->
569
- <section id="summary-metrics-section" class="hidden">
570
- <h2>Resumo Geral</h2>
571
- <div class="metrics-grid">
572
- <div class="metric-card">
573
- <p class="metric-label">📦 Total de Pedidos</p>
574
- <p class="metric-value" id="metric-total-pedidos">0</p>
575
- <p class="metric-sub" id="metric-unique-utms">0 UTMs únicos</p>
576
- </div>
577
- <div class="metric-card">
578
- <p class="metric-label">📊 Taxa de Pagamento Geral</p>
579
- <p class="metric-value" id="metric-taxa-geral">0%</p>
580
- <span class="badge" id="badge-taxa-geral">-</span>
581
- </div>
582
- <div class="metric-card">
583
- <p class="metric-label">💰 Total de Vendas</p>
584
- <p class="metric-value" id="metric-total-vendas">R$ 0,00</p>
585
- <p class="metric-sub">Bruto</p>
586
- </div>
587
- <div class="metric-card">
588
- <p class="metric-label">✅ Vendas Pagas</p>
589
- <p class="metric-value" id="metric-vendas-pagas">R$ 0,00</p>
590
- <p class="metric-sub" id="metric-count-pagas">0 pedidos pagos</p>
591
- </div>
592
  </div>
593
  </section>
594
 
595
- <!-- Tabela -->
596
- <section id="table-section">
 
597
  <div id="loading-state" class="hidden" style="text-align: center; padding: 40px;">
598
- <div style="display: inline-block; vertical-align: middle;">
599
- <div class="spinner"></div>
600
- </div>
601
- <h2 style="margin-top: 10px; display: inline-block; margin-left: 10px;">Conectando à Shopify...</h2>
602
- <p class="text-subdued">Buscando pedidos e processando UTMs</p>
603
  </div>
604
 
 
605
  <div id="empty-state" class="hidden" style="text-align: center; padding: 40px;">
606
- <h2>Nenhum pedido encontrado</h2>
607
- <p class="text-subdued">Não há pedidos para o período selecionado ou com as UTMs esperadas.</p>
608
  </div>
609
 
 
610
  <div id="table-wrapper" class="hidden">
611
- <div class="table-container">
612
- <table id="utm-report-table">
613
- <thead>
614
- <tr>
615
- <th onclick="app.handleSort('lastParameter')">Campanha / UTM ↕</th>
616
- <th>Pagos / Total</th>
617
- <th onclick="app.handleSort('clientesUnicos')">Clientes Únicos ↕</th>
618
- <th onclick="app.handleSort('totalVendas')">Total Vendas ↕</th>
619
- <th onclick="app.handleSort('vendasPagas')">Vendas Pagas ↕</th>
620
- <th onclick="app.handleSort('taxaPagamento')">Taxa Pagamento ↕</th>
621
- </tr>
622
- </thead>
623
- <tbody id="table-body">
624
- <!-- Rows injected here -->
625
- </tbody>
626
- </table>
627
- </div>
628
  </div>
629
  </section>
630
 
@@ -660,9 +691,8 @@
660
  buildUrl(startDate, endDate, pageinfo = '') {
661
  const baseUrl = `https://${this.config.storeName}.myshopify.com/admin/api/${this.config.apiVersion}/orders.json`;
662
 
663
- // Construct query params
664
  const params = new URLSearchParams({
665
- status: 'any', // open, closed, cancelled, any
666
  created_at_min: startDate,
667
  created_at_max: endDate,
668
  limit: '250'
@@ -674,11 +704,9 @@
674
 
675
  let finalUrl = `${baseUrl}?${params.toString()}`;
676
 
677
- // Add CORS Proxy if configured
678
  if (this.config.corsProxy && this.config.corsProxy.trim() !== '') {
679
- // Ensure proxy doesn't have double slashes if user pasted full url
680
  const proxy = this.config.corsProxy.replace(/\/$/, '');
681
- finalUrl = `${proxy}/${finalUrl}`;
682
  }
683
 
684
  return finalUrl;
@@ -686,7 +714,7 @@
686
 
687
  async fetchAllOrders(startDate, endDate) {
688
  if (!this.config.storeName || !this.config.accessToken) {
689
- throw new Error("Credenciais da Shopify não configuradas.");
690
  }
691
 
692
  let orders = [];
@@ -703,9 +731,9 @@
703
  });
704
 
705
  if (!response.ok) {
706
- if (response.status === 401) throw new Error("Erro de autenticação (401). Verifique o Token.");
707
- if (response.status === 404) throw new Error("Loja não encontrada (404). Verifique o nome da loja.");
708
- throw new Error(`Erro na API Shopify: ${response.status} ${response.statusText}`);
709
  }
710
 
711
  const data = await response.json();
@@ -713,7 +741,6 @@
713
  orders = orders.concat(data.orders);
714
  }
715
 
716
- // Check Link header for pagination
717
  const linkHeader = response.headers.get('Link');
718
  if (linkHeader) {
719
  const nextUrl = this.parseLinkHeader(linkHeader, 'next');
@@ -723,9 +750,8 @@
723
  }
724
 
725
  } catch (error) {
726
- // Specific CORS error message
727
  if (error.name === 'TypeError' && error.message === 'Failed to fetch') {
728
- throw new Error("Erro de bloqueio CORS. O navegador bloqueou o pedido direto. Tente usar um campo 'Proxy CORS' nas configurações ou use uma extensão como 'Allow CORS'.");
729
  }
730
  throw error;
731
  }
@@ -751,30 +777,26 @@
751
  * Main Application Logic
752
  */
753
  const app = {
754
- // State
755
  dateRangeOption: 'hoje',
756
  loading: false,
757
  error: null,
758
  utmData: [],
759
  totalOrders: 0,
760
  lastUpdate: '',
761
- sortColumn: 'totalPedidos',
762
  sortDirection: 'desc',
763
- snapshot: null, // { timestamp, orderIds: Set }
764
  newOrdersReport: null,
765
  isFirstLoad: true,
766
  hasLoadedData: false,
767
- dayChanged: false,
768
-
769
- // In-memory store
770
  allSessionOrders: [],
771
- sessionOrderIds: new Set(),
772
 
773
  init() {
774
  this.cacheDOM();
775
  this.bindEvents();
776
  this.loadInitialConfig();
777
  this.checkInitialView();
 
778
  },
779
 
780
  cacheDOM() {
@@ -799,13 +821,10 @@
799
  initialLoadBtn: document.getElementById('initial-load-button'),
800
  resetSnapshotBtn: document.getElementById('reset-snapshot-button'),
801
 
802
- // Banners & Info
803
  limitationBanner: document.getElementById('limitation-banner'),
804
  errorBanner: document.getElementById('error-banner'),
805
  errorMsg: document.getElementById('error-message'),
806
- lastUpdateInfo: document.getElementById('last-update-info'),
807
- snapshotTime: document.getElementById('snapshot-time'),
808
- dayChangedBanner: document.getElementById('day-changed-banner'),
809
 
810
  // Sections
811
  noDataSection: document.getElementById('no-data-section'),
@@ -829,10 +848,32 @@
829
  loadingState: document.getElementById('loading-state'),
830
  emptyState: document.getElementById('empty-state'),
831
  tableWrapper: document.getElementById('table-wrapper'),
832
- tableBody: document.getElementById('table-body')
 
 
 
 
833
  };
834
  },
835
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
836
  loadInitialConfig() {
837
  const config = ShopifyService.loadConfig();
838
  if (config) {
@@ -844,16 +885,10 @@
844
  },
845
 
846
  bindEvents() {
847
- // Config
848
  this.dom.toggleConfigBtn.addEventListener('click', () => {
849
  const isHidden = this.dom.configForm.classList.contains('hidden');
850
- if (isHidden) {
851
- this.dom.configForm.classList.remove('hidden');
852
- this.dom.toggleConfigBtn.innerHTML = '<i class="ri-eye-off-line"></i> Ocultar';
853
- } else {
854
- this.dom.configForm.classList.add('hidden');
855
- this.dom.toggleConfigBtn.innerHTML = '<i class="ri-settings-3-line"></i> Configurar';
856
- }
857
  });
858
 
859
  this.dom.saveConfigBtn.addEventListener('click', () => {
@@ -862,7 +897,7 @@
862
  const proxy = this.dom.corsProxyInput.value.trim();
863
 
864
  if (!storeName || !token) {
865
- alert("Por favor, preencha o nome da loja e o token de acesso.");
866
  return;
867
  }
868
 
@@ -872,7 +907,6 @@
872
  setTimeout(() => this.dom.configStatus.classList.add('hidden'), 3000);
873
  });
874
 
875
- // Filters
876
  this.dom.dateSelect.addEventListener('change', (e) => {
877
  this.dateRangeOption = e.target.value;
878
  this.toggleCustomDateInput();
@@ -944,10 +978,9 @@
944
  },
945
 
946
  async fetchOrders() {
947
- // Check config
948
  const config = ShopifyService.loadConfig();
949
  if (!config || !config.storeName || !config.accessToken) {
950
- alert("Por favor, configure as credenciais da Shopify primeiro.");
951
  this.dom.configSection.scrollIntoView({ behavior: 'smooth' });
952
  return;
953
  }
@@ -955,74 +988,22 @@
955
  this.setLoading(true);
956
  this.error = null;
957
  this.dom.errorBanner.classList.add('hidden');
 
958
 
959
  const { startDate, endDate } = this.getDateRange();
960
 
961
  try {
962
- // Fetch from Shopify API
963
  const fetchedOrders = await ShopifyService.fetchAllOrders(startDate, endDate);
964
 
965
- // If we are just reloading "Today", we usually get the same data.
966
- // To simulate "New Orders" detection, we compare with local snapshot.
967
-
968
- // Logic:
969
- // 1. If First Load -> Just show data.
970
- // 2. If Update -> Compare IDs with snapshot to find "New" ones.
971
-
972
  const report = this.analyzeNewOrders(fetchedOrders, this.snapshot);
973
  this.newOrdersReport = report;
974
 
975
  const newSnapshot = this.createSnapshot(fetchedOrders);
976
  this.snapshot = newSnapshot;
977
 
978
- this.allSessionOrders = fetchedOrders; // Replace current view with fetched data
979
  this.isFirstLoad = false;
980
  this.hasLoadedData = true;
981
- this.dayChanged = false;
982
 
983
  this.processOrders(this.allSessionOrders);
984
- this.totalOrders = this.allSessionOrders.length;
985
-
986
- const now = new Date();
987
- this.lastUpdate = now.toLocaleString('pt-BR');
988
-
989
- this.updateUI();
990
-
991
- } catch (err) {
992
- console.error(err);
993
- this.error = err.message || 'Erro ao carregar pedidos.';
994
- this.dom.errorMsg.textContent = this.error;
995
- this.dom.errorBanner.classList.remove('hidden');
996
- } finally {
997
- this.setLoading(false);
998
- }
999
- },
1000
-
1001
- analyzeNewOrders(currentOrders, previousSnapshot) {
1002
- if (!previousSnapshot) return null;
1003
-
1004
- const previousOrderIds = previousSnapshot.orderIds;
1005
- // Find orders in currentOrders that were NOT in previousSnapshot
1006
- const newOrders = currentOrders.filter((order) => !previousOrderIds.has(order.id));
1007
-
1008
- if (newOrders.length === 0) {
1009
- return {
1010
- newOrderCount: 0,
1011
- newOrdersTotal: 0,
1012
- newOrdersPaid: 0,
1013
- newOrdersPaidCount: 0,
1014
- newOrderNumbers: [],
1015
- timeDifference: this.calculateTimeDifference(previousSnapshot.timestamp),
1016
- campaignBreakdown: []
1017
- };
1018
- }
1019
-
1020
- const newOrdersTotal = newOrders.reduce((sum, order) => sum + parseFloat(order.total_price), 0);
1021
- const paidOrders = newOrders.filter((o) => o.financial_status === 'paid');
1022
- const newOrdersPaid = paidOrders.reduce((sum, order) => sum + parseFloat(order.total_price), 0);
1023
- const newOrderNumbers = newOrders.map((order) => order.name);
1024
-
1025
- const campaignGroups = new Map();
1026
- newOrders.forEach((order) => {
1027
- const utmContent = this.getUtmContent(order);
1028
- if (!campaignGroups.has
 
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
+ <!-- Importing Inter Font -->
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
+ <!-- Importing Remix Icon -->
11
  <link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
12
 
13
  <style>
14
+ /* --- CSS VARIABLES & THEME CONFIGURATION --- */
15
  :root {
16
+ /* Dark Theme (Default) - Profitfy Inspired */
17
+ --bg-body: #09090b; /* Zinc 950 */
18
+ --bg-card: #18181b; /* Zinc 900 */
19
+ --bg-surface: #27272a; /* Zinc 800 */
20
+ --bg-hover: #3f3f46; /* Zinc 700 */
21
+
22
+ --border-color: #27272a;
23
+ --border-light: #3f3f46;
24
+
25
+ --text-primary: #f4f4f5; /* Zinc 100 */
26
+ --text-secondary: #a1a1aa; /* Zinc 400 */
27
+ --text-muted: #71717a; /* Zinc 500 */
28
+
29
+ --color-primary: #10b981; /* Emerald 500 */
30
+ --color-primary-hover: #059669;
31
+ --color-primary-glow: rgba(16, 185, 129, 0.2);
32
+
33
+ --color-accent: #8b5cf6; /* Violet */
34
+
35
+ --color-danger: #ef4444;
36
+ --color-warning: #f59e0b;
37
+ --color-info: #3b82f6;
38
+
39
+ --shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.15);
40
+ --shadow-glow: 0 0 15px var(--color-primary-glow);
41
+
42
+ --radius-md: 12px;
43
+ --radius-sm: 8px;
44
+
45
+ --font-family: 'Inter', sans-serif;
46
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
47
  }
48
 
49
+ [data-theme="light"] {
50
+ /* Light Theme */
51
+ --bg-body: #f4f4f5;
52
+ --bg-card: #ffffff;
53
+ --bg-surface: #e4e4e7;
54
+ --bg-hover: #d4d4d8;
55
+
56
+ --border-color: #e4e4e7;
57
+ --border-light: #d4d4d8;
58
+
59
+ --text-primary: #18181b;
60
+ --text-secondary: #52525b;
61
+ --text-muted: #a1a1aa;
62
+
63
+ --color-primary: #059669;
64
+ --color-primary-hover: #047857;
65
+ --color-primary-glow: rgba(5, 150, 105, 0.1);
66
+
67
+ --shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
68
+ --shadow-glow: none;
69
+ }
70
+
71
+ /* --- RESET & BASE --- */
72
  * {
73
  box-sizing: border-box;
74
  margin: 0;
75
  padding: 0;
76
+ outline: none;
77
  }
78
 
79
  body {
80
  font-family: var(--font-family);
81
+ background-color: var(--bg-body);
82
+ color: var(--text-primary);
83
  line-height: 1.5;
84
  -webkit-font-smoothing: antialiased;
85
+ transition: background-color 0.3s ease, color 0.3s ease;
86
+ padding-bottom: 80px;
87
  }
88
 
89
+ a { text-decoration: none; color: inherit; }
90
+ ul { list-style: none; }
91
+
92
+ /* --- LAYOUT --- */
93
  .container {
94
+ max-width: 1280px;
95
  margin: 0 auto;
96
+ padding: 0 24px;
97
  }
98
 
99
+ /* --- HEADER --- */
100
  header {
101
+ background-color: rgba(9, 9, 11, 0.8);
102
+ backdrop-filter: blur(12px);
103
+ -webkit-backdrop-filter: blur(12px);
104
+ border-bottom: 1px solid var(--border-color);
105
  position: sticky;
106
  top: 0;
107
+ z-index: 50;
108
+ padding: 16px 0;
109
+ }
110
+
111
+ [data-theme="light"] header {
112
+ background-color: rgba(255, 255, 255, 0.8);
113
  }
114
 
115
  .header-content {
 
118
  align-items: center;
119
  }
120
 
121
+ .logo-area {
 
 
 
 
 
 
 
 
122
  display: flex;
123
  align-items: center;
124
+ gap: 12px;
 
125
  }
126
 
127
+ .logo-icon {
128
+ font-size: 24px;
129
  color: var(--color-primary);
130
  }
131
 
132
+ h1 {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  font-size: 1.125rem;
134
+ font-weight: 700;
135
+ letter-spacing: -0.025em;
 
 
 
 
 
 
 
 
136
  }
137
 
138
+ .header-actions {
139
  display: flex;
140
+ gap: 16px;
141
+ align-items: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  }
143
 
144
+ /* --- BUTTONS & CONTROLS --- */
145
  .btn {
146
  display: inline-flex;
147
  align-items: center;
148
  justify-content: center;
149
  gap: 8px;
150
+ padding: 10px 20px;
151
+ border-radius: var(--radius-sm);
152
  font-weight: 500;
153
+ font-size: 0.875rem;
154
  cursor: pointer;
155
+ transition: var(--transition);
156
  border: 1px solid transparent;
157
+ white-space: nowrap;
 
 
 
 
 
158
  }
159
 
160
  .btn-primary {
161
+ background-color: var(--color-primary);
162
+ color: #ffffff;
163
+ box-shadow: 0 0 10px var(--color-primary-glow);
164
  }
165
 
166
  .btn-primary:hover:not(:disabled) {
167
  background-color: var(--color-primary-hover);
168
+ transform: translateY(-1px);
169
  }
170
 
171
  .btn-secondary {
172
+ background-color: var(--bg-surface);
173
+ color: var(--text-primary);
174
+ border: 1px solid var(--border-color);
175
  }
176
 
177
  .btn-secondary:hover:not(:disabled) {
178
+ background-color: var(--bg-hover);
179
+ border-color: var(--border-light);
180
  }
181
 
182
  .btn-ghost {
183
  background: transparent;
184
+ color: var(--text-secondary);
 
185
  }
186
+
187
  .btn-ghost:hover {
188
+ color: var(--text-primary);
189
+ background-color: var(--bg-hover);
190
  }
191
 
192
+ .btn-icon {
193
+ padding: 8px;
194
+ border-radius: 50%;
195
+ }
196
+
197
+ .btn:disabled {
198
+ opacity: 0.5;
199
+ cursor: not-allowed;
200
+ transform: none !important;
201
+ box-shadow: none !important;
202
+ }
203
+
204
+ /* --- THEME TOGGLE --- */
205
+ .theme-toggle {
206
+ cursor: pointer;
207
+ font-size: 1.25rem;
208
+ color: var(--text-secondary);
209
+ transition: var(--transition);
210
+ }
211
+ .theme-toggle:hover { color: var(--color-primary); }
212
+
213
+ /* --- SECTIONS & CARDS --- */
214
+ main {
215
+ padding-top: 32px;
216
  display: flex;
217
+ flex-direction: column;
218
+ gap: 24px;
 
219
  }
220
 
221
+ .card {
222
+ background-color: var(--bg-card);
223
+ border: 1px solid var(--border-color);
224
+ border-radius: var(--radius-md);
225
+ padding: 24px;
226
+ box-shadow: var(--shadow-card);
227
+ position: relative;
228
+ overflow: hidden;
229
  }
230
 
231
+ .card-header {
232
+ display: flex;
233
+ justify-content: space-between;
234
+ align-items: center;
235
+ margin-bottom: 20px;
236
  }
237
 
238
+ .card-title {
239
+ font-size: 1rem;
240
+ font-weight: 600;
241
+ color: var(--text-primary);
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 8px;
245
  }
246
 
247
+ /* --- FORMS --- */
248
+ .form-grid {
249
+ display: grid;
250
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
251
+ gap: 20px;
252
+ }
253
+
254
+ .input-group {
255
+ display: flex;
256
+ flex-direction: column;
257
+ gap: 8px;
258
+ }
259
+
260
+ label {
261
+ font-size: 0.8rem;
262
+ font-weight: 500;
263
+ color: var(--text-secondary);
264
+ text-transform: uppercase;
265
+ letter-spacing: 0.05em;
266
+ }
267
+
268
+ input, select {
269
+ background-color: var(--bg-body);
270
+ border: 1px solid var(--border-color);
271
+ color: var(--text-primary);
272
+ padding: 12px;
273
+ border-radius: var(--radius-sm);
274
+ font-size: 0.95rem;
275
+ transition: var(--transition);
276
+ width: 100%;
277
  }
278
 
279
+ input:focus, select:focus {
280
+ border-color: var(--color-primary);
281
+ box-shadow: 0 0 0 2px var(--color-primary-glow);
282
+ }
283
+
284
+ /* --- METRICS GRID --- */
285
  .metrics-grid {
286
  display: grid;
287
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
288
+ gap: 20px;
289
  }
290
 
291
  .metric-card {
292
+ background: linear-gradient(145deg, var(--bg-card), var(--bg-surface));
293
+ border: 1px solid var(--border-color);
294
+ border-radius: var(--radius-md);
295
+ padding: 20px;
296
+ display: flex;
297
+ flex-direction: column;
298
+ gap: 8px;
299
+ position: relative;
300
+ }
301
+
302
+ .metric-card::before {
303
+ content: '';
304
+ position: absolute;
305
+ top: 0; left: 0; width: 4px; height: 100%;
306
+ background-color: var(--color-primary);
307
+ border-top-left-radius: var(--radius-md);
308
+ border-bottom-left-radius: var(--radius-md);
309
+ opacity: 0.5;
310
  }
311
 
312
  .metric-label {
313
+ font-size: 0.8rem;
314
+ color: var(--text-secondary);
315
+ font-weight: 500;
316
  }
317
 
318
  .metric-value {
319
+ font-size: 1.75rem;
320
+ font-weight: 700;
321
+ color: var(--text-primary);
322
+ letter-spacing: -0.03em;
323
  }
324
 
325
  .metric-sub {
326
+ font-size: 0.75rem;
327
+ color: var(--text-muted);
 
328
  }
329
 
330
+ /* --- TABLE --- */
331
+ .table-wrapper {
332
  width: 100%;
333
  overflow-x: auto;
334
+ border-radius: var(--radius-sm);
335
+ border: 1px solid var(--border-color);
336
  }
337
 
338
  table {
339
  width: 100%;
340
  border-collapse: collapse;
341
+ font-size: 0.9rem;
342
+ white-space: nowrap;
343
  }
344
 
345
  th {
346
+ background-color: var(--bg-surface);
347
+ color: var(--text-secondary);
 
348
  font-weight: 600;
349
+ text-transform: uppercase;
350
+ font-size: 0.75rem;
351
+ padding: 16px;
352
+ text-align: left;
353
+ letter-spacing: 0.05em;
354
+ border-bottom: 1px solid var(--border-color);
355
  cursor: pointer;
356
  user-select: none;
357
  }
358
 
359
+ th:hover { color: var(--text-primary); }
 
 
360
 
361
  td {
362
+ padding: 16px;
363
+ border-bottom: 1px solid var(--border-color);
364
+ color: var(--text-primary);
365
  }
366
 
367
+ tr:last-child td { border-bottom: none; }
368
+
369
+ tr:hover td {
370
+ background-color: rgba(255, 255, 255, 0.02);
371
  }
372
+ [data-theme="light"] tr:hover td {
373
+ background-color: rgba(0, 0, 0, 0.02);
 
 
 
374
  }
375
 
376
+ .text-right { text-align: right; }
377
+ .text-center { text-align: center; }
378
+
379
+ /* --- BADGES --- */
380
  .badge {
381
+ padding: 4px 10px;
382
+ border-radius: 20px;
383
+ font-size: 0.7rem;
 
 
384
  font-weight: 600;
385
  text-transform: uppercase;
386
  }
387
 
388
+ .badge-success { background-color: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.2); }
389
+ .badge-warning { background-color: rgba(245, 158, 11, 0.15); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.2); }
390
+ .badge-danger { background-color: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.2); }
 
391
 
392
+ /* --- ALERTS & BANNERS --- */
393
+ .alert {
394
+ padding: 16px;
395
+ border-radius: var(--radius-sm);
396
+ margin-bottom: 20px;
397
+ display: flex;
398
+ gap: 12px;
399
+ align-items: center;
400
+ border: 1px solid transparent;
401
+ font-size: 0.9rem;
402
  }
403
 
404
+ .alert-warning {
405
+ background-color: rgba(245, 158, 11, 0.1);
406
+ border-color: rgba(245, 158, 11, 0.2);
407
+ color: #fbbf24;
408
  }
409
 
410
+ .alert-info {
411
+ background-color: rgba(59, 130, 246, 0.1);
412
+ border-color: rgba(59, 130, 246, 0.2);
413
+ color: #60a5fa;
 
 
 
 
414
  }
415
 
416
+ .alert-error {
417
+ background-color: rgba(239, 68, 68, 0.1);
418
+ border-color: rgba(239, 68, 68, 0.2);
419
+ color: #f87171;
420
  }
421
 
422
+ /* --- UTILITIES --- */
423
+ .hidden { display: none !important; }
424
+
425
+ .status-pill {
426
+ height: 6px;
427
+ width: 6px;
428
+ border-radius: 50%;
429
+ display: inline-block;
430
+ margin-right: 6px;
431
  }
432
+ .status-pill.green { background-color: var(--color-primary); box-shadow: 0 0 8px var(--color-primary); }
433
+ .status-pill.red { background-color: var(--color-danger); }
434
 
435
+ .anycoder-link {
436
+ font-size: 0.8rem;
437
+ color: var(--text-muted);
438
+ display: flex;
439
+ align-items: center;
440
+ gap: 6px;
441
+ transition: color 0.2s;
442
  }
443
+ .anycoder-link:hover { color: var(--color-primary); }
444
 
445
+ /* --- ANIMATIONS --- */
446
+ @keyframes pulse {
447
+ 0% { opacity: 1; }
448
+ 50% { opacity: 0.5; }
449
+ 100% { opacity: 1; }
450
  }
451
 
452
+ .loading-spinner {
453
+ width: 40px;
454
+ height: 40px;
455
+ border: 3px solid var(--bg-surface);
456
+ border-top-color: var(--color-primary);
457
  border-radius: 50%;
458
+ animation: spin 0.8s linear infinite;
459
+ margin: 0 auto 16px;
 
 
 
 
 
460
  }
461
 
462
+ @keyframes spin { to { transform: rotate(360deg); } }
 
 
 
 
 
 
 
 
 
 
 
463
 
464
+ /* --- RESPONSIVE --- */
465
  @media (max-width: 768px) {
466
+ .header-content { flex-direction: column; gap: 16px; text-align: center; }
467
+ .card-header { flex-direction: column; gap: 12px; align-items: flex-start; }
468
+ .controls { flex-direction: column; width: 100%; }
469
+ .controls > * { width: 100%; }
470
+ .form-grid { grid-template-columns: 1fr; }
 
 
 
 
 
 
 
471
  }
472
  </style>
473
  </head>
474
 
475
  <body>
476
 
477
+ <!-- Header -->
478
  <header>
479
  <div class="container header-content">
480
+ <div class="logo-area">
481
+ <i class="ri-bar-chart-box-fill logo-icon"></i>
482
+ <h1>UTM Analytics Pro</h1>
483
+ </div>
484
+
485
+ <div class="header-actions">
486
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
487
+ Built with anycoder <i class="ri-external-link-line"></i>
488
+ </a>
489
+ <div class="theme-toggle" id="theme-toggle" title="Alternar Tema">
490
+ <i class="ri-sun-line" id="theme-icon"></i>
491
+ </div>
492
+ </div>
493
  </div>
494
  </header>
495
 
496
  <main class="container">
497
 
498
+ <!-- API Configuration -->
499
+ <section class="card" id="config-section">
500
+ <div class="card-header">
501
+ <div class="card-title">
502
+ <i class="ri-settings-4-line"></i> Configuração da API
503
+ </div>
504
+ <button id="toggle-config-btn" class="btn btn-ghost btn-icon">
505
+ <i class="ri-arrow-up-s-line"></i>
506
  </button>
507
  </div>
508
+
509
+ <div id="config-form" class="form-grid">
510
+ <div class="input-group">
511
  <label for="store-name">Nome da Loja</label>
512
+ <input type="text" id="store-name" placeholder="minha-loja">
 
513
  </div>
514
+
515
+ <div class="input-group">
516
+ <label for="access-token">Access Token</label>
517
  <input type="password" id="access-token" placeholder="shpat_xxxxx...">
 
518
  </div>
519
 
520
+ <div class="input-group">
521
  <label for="cors-proxy">Proxy CORS (Opcional)</label>
522
+ <input type="text" id="cors-proxy" placeholder="https://corsproxy.io/?">
 
523
  </div>
524
 
525
+ <div class="input-group" style="justify-content: flex-end;">
526
  <button id="save-config-btn" class="btn btn-primary">
527
+ <i class="ri-save-3-line"></i> Salvar Config
528
  </button>
529
  </div>
530
  </div>
 
 
 
 
 
 
531
 
532
+ <div id="config-status" class="hidden alert alert-info" style="margin-top: 16px;">
533
+ <i class="ri-checkbox-circle-line"></i>
534
+ <span>Configurações salvas com sucesso.</span>
 
 
 
535
  </div>
536
+ </section>
537
 
538
+ <!-- Filters & Controls -->
539
+ <section class="card" id="filters-section">
540
+ <div class="form-grid" style="align-items: end;">
541
+ <div class="input-group">
542
  <label for="date-range-select">Período</label>
543
  <select id="date-range-select">
544
  <option value="hoje">Hoje</option>
545
  <option value="ontem">Ontem</option>
546
  <option value="ultimos7">Últimos 7 dias</option>
547
  <option value="ultimos30">Últimos 30 dias</option>
548
+ <option value="customizado">Customizado</option>
549
  </select>
550
  </div>
551
 
552
+ <div class="input-group hidden" id="custom-date-group">
553
+ <label>Data Início - Fim</label>
554
+ <div style="display: flex; gap: 8px;">
555
+ <input type="date" id="custom-date-start">
556
+ <input type="date" id="custom-date-end">
557
+ </div>
558
  </div>
559
 
560
+ <div style="display: flex; gap: 12px; width: 100%; justify-content: flex-end;">
561
  <button id="refresh-button" class="btn btn-primary" disabled>
562
+ <i class="ri-refresh-line"></i> Buscar Dados
563
  </button>
564
  <button id="export-csv-button" class="btn btn-secondary" disabled>
565
+ <i class="ri-file-download-line"></i> CSV
566
  </button>
567
  </div>
568
  </div>
569
  </section>
570
 
571
+ <!-- Alerts -->
572
+ <div id="error-banner" class="alert alert-error hidden">
573
+ <i class="ri-error-warning-fill"></i>
574
+ <span id="error-message"></span>
 
 
 
 
 
 
 
 
 
 
 
575
  </div>
576
 
577
+ <div id="limitation-banner" class="alert alert-warning">
578
+ <i class="ri-information-fill"></i>
579
+ <span>Este relatório agrupa pedidos pelo atributo <code>utm_content</code> via API Shopify.</span>
 
 
 
 
580
  </div>
581
 
582
+ <!-- Empty State -->
583
+ <div id="no-data-section" class="card hidden" style="text-align: center; padding: 60px 20px;">
584
+ <i class="ri-database-2-line" style="font-size: 48px; color: var(--text-muted);"></i>
585
+ <h3 style="margin-top: 16px; color: var(--text-primary);">Aguardando Conexão</h3>
586
+ <p style="color: var(--text-secondary); margin-bottom: 24px;">Configure as credenciais e inicie a busca.</p>
587
+ <button id="initial-load-button" class="btn btn-primary">Conectar Shopify</button>
588
  </div>
589
 
590
+ <!-- New Orders Snapshot -->
591
+ <div id="new-orders-section" class="card hidden">
592
+ <div class="card-header">
593
+ <div class="card-title">
594
+ <span class="status-pill green"></span> Novo Delta Detectado
 
 
595
  </div>
596
+ <button id="reset-snapshot-button" class="btn btn-ghost" style="font-size: 0.8rem;">Resetar Base</button>
597
+ </div>
598
+ <div style="display: grid; gap: 12px;">
599
+ <p><strong>Tempo decorrido:</strong> <span id="diff-time" style="color: var(--text-muted);">--</span></p>
600
+ <div id="new-orders-content"></div>
601
+ <div id="campaign-breakdown"></div>
602
  </div>
603
  </div>
604
 
605
+ <!-- Metrics -->
606
+ <section id="summary-metrics-section" class="metrics-grid hidden">
607
+ <div class="metric-card">
608
+ <span class="metric-label">Total de Pedidos</span>
609
+ <span class="metric-value" id="metric-total-pedidos">0</span>
610
+ <span class="metric-sub">UTMs Únicas: <span id="metric-unique-utms">0</span></span>
611
+ </div>
612
+ <div class="metric-card">
613
+ <span class="metric-label">Taxa de Conversão Financeira</span>
614
+ <span class="metric-value" id="metric-taxa-geral">0%</span>
615
+ <span class="badge badge-success" id="badge-taxa-geral">OK</span>
616
+ </div>
617
+ <div class="metric-card">
618
+ <span class="metric-label">Faturamento Bruto</span>
619
+ <span class="metric-value" id="metric-total-vendas">R$ 0,00</span>
620
+ <span class="metric-sub">Total de pedidos</span>
621
+ </div>
622
+ <div class="metric-card">
623
+ <span class="metric-label">Faturamento Pago</span>
624
+ <span class="metric-value" id="metric-vendas-pagas" style="color: var(--color-primary);">R$ 0,00</span>
625
+ <span class="metric-sub" id="metric-count-pagas">0 pedidos</span>
 
 
 
626
  </div>
627
  </section>
628
 
629
+ <!-- Table Section -->
630
+ <section class="card" id="table-section">
631
+ <!-- Loading State -->
632
  <div id="loading-state" class="hidden" style="text-align: center; padding: 40px;">
633
+ <div class="loading-spinner"></div>
634
+ <p style="color: var(--text-secondary);">Processando dados da API...</p>
 
 
 
635
  </div>
636
 
637
+ <!-- Empty Data State -->
638
  <div id="empty-state" class="hidden" style="text-align: center; padding: 40px;">
639
+ <p style="color: var(--text-secondary);">Nenhum dado encontrado para este período.</p>
 
640
  </div>
641
 
642
+ <!-- Data Table -->
643
  <div id="table-wrapper" class="hidden">
644
+ <table id="utm-report-table">
645
+ <thead>
646
+ <tr>
647
+ <th onclick="app.handleSort('lastParameter')">Campanha / UTM <i class="ri-arrow-up-down-line"></i></th>
648
+ <th class="text-center">Pagos / Total</th>
649
+ <th onclick="app.handleSort('clientesUnicos')" class="text-center">Clientes <i class="ri-arrow-up-down-line"></i></th>
650
+ <th onclick="app.handleSort('totalVendas')" class="text-right">Total Vendas <i class="ri-arrow-up-down-line"></i></th>
651
+ <th onclick="app.handleSort('vendasPagas')" class="text-right">Vendas Pagas <i class="ri-arrow-up-down-line"></i></th>
652
+ <th onclick="app.handleSort('taxaPagamento')" class="text-right">Taxa Pagto <i class="ri-arrow-up-down-line"></i></th>
653
+ </tr>
654
+ </thead>
655
+ <tbody id="table-body">
656
+ <!-- Rows injected here -->
657
+ </tbody>
658
+ </table>
 
 
659
  </div>
660
  </section>
661
 
 
691
  buildUrl(startDate, endDate, pageinfo = '') {
692
  const baseUrl = `https://${this.config.storeName}.myshopify.com/admin/api/${this.config.apiVersion}/orders.json`;
693
 
 
694
  const params = new URLSearchParams({
695
+ status: 'any',
696
  created_at_min: startDate,
697
  created_at_max: endDate,
698
  limit: '250'
 
704
 
705
  let finalUrl = `${baseUrl}?${params.toString()}`;
706
 
 
707
  if (this.config.corsProxy && this.config.corsProxy.trim() !== '') {
 
708
  const proxy = this.config.corsProxy.replace(/\/$/, '');
709
+ finalUrl = `${proxy}${finalUrl}`;
710
  }
711
 
712
  return finalUrl;
 
714
 
715
  async fetchAllOrders(startDate, endDate) {
716
  if (!this.config.storeName || !this.config.accessToken) {
717
+ throw new Error("Credenciais não configuradas.");
718
  }
719
 
720
  let orders = [];
 
731
  });
732
 
733
  if (!response.ok) {
734
+ if (response.status === 401) throw new Error("Erro de Autenticação (401).");
735
+ if (response.status === 404) throw new Error("Loja não encontrada (404).");
736
+ throw new Error(`API Error: ${response.status}`);
737
  }
738
 
739
  const data = await response.json();
 
741
  orders = orders.concat(data.orders);
742
  }
743
 
 
744
  const linkHeader = response.headers.get('Link');
745
  if (linkHeader) {
746
  const nextUrl = this.parseLinkHeader(linkHeader, 'next');
 
750
  }
751
 
752
  } catch (error) {
 
753
  if (error.name === 'TypeError' && error.message === 'Failed to fetch') {
754
+ throw new Error("Erro de CORS. Use um Proxy.");
755
  }
756
  throw error;
757
  }
 
777
  * Main Application Logic
778
  */
779
  const app = {
 
780
  dateRangeOption: 'hoje',
781
  loading: false,
782
  error: null,
783
  utmData: [],
784
  totalOrders: 0,
785
  lastUpdate: '',
786
+ sortColumn: 'vendasPagas',
787
  sortDirection: 'desc',
788
+ snapshot: null,
789
  newOrdersReport: null,
790
  isFirstLoad: true,
791
  hasLoadedData: false,
 
 
 
792
  allSessionOrders: [],
 
793
 
794
  init() {
795
  this.cacheDOM();
796
  this.bindEvents();
797
  this.loadInitialConfig();
798
  this.checkInitialView();
799
+ this.initTheme();
800
  },
801
 
802
  cacheDOM() {
 
821
  initialLoadBtn: document.getElementById('initial-load-button'),
822
  resetSnapshotBtn: document.getElementById('reset-snapshot-button'),
823
 
824
+ // Banners
825
  limitationBanner: document.getElementById('limitation-banner'),
826
  errorBanner: document.getElementById('error-banner'),
827
  errorMsg: document.getElementById('error-message'),
 
 
 
828
 
829
  // Sections
830
  noDataSection: document.getElementById('no-data-section'),
 
848
  loadingState: document.getElementById('loading-state'),
849
  emptyState: document.getElementById('empty-state'),
850
  tableWrapper: document.getElementById('table-wrapper'),
851
+ tableBody: document.getElementById('table-body'),
852
+
853
+ // Theme
854
+ themeToggle: document.getElementById('theme-toggle'),
855
+ themeIcon: document.getElementById('theme-icon')
856
  };
857
  },
858
 
859
+ initTheme() {
860
+ const savedTheme = localStorage.getItem('theme') || 'dark';
861
+ document.documentElement.setAttribute('data-theme', savedTheme);
862
+ this.updateThemeIcon(savedTheme);
863
+
864
+ this.dom.themeToggle.addEventListener('click', () => {
865
+ const current = document.documentElement.getAttribute('data-theme');
866
+ const next = current === 'dark' ? 'light' : 'dark';
867
+ document.documentElement.setAttribute('data-theme', next);
868
+ localStorage.setItem('theme', next);
869
+ this.updateThemeIcon(next);
870
+ });
871
+ },
872
+
873
+ updateThemeIcon(theme) {
874
+ this.dom.themeIcon.className = theme === 'dark' ? 'ri-sun-line' : 'ri-moon-line';
875
+ },
876
+
877
  loadInitialConfig() {
878
  const config = ShopifyService.loadConfig();
879
  if (config) {
 
885
  },
886
 
887
  bindEvents() {
 
888
  this.dom.toggleConfigBtn.addEventListener('click', () => {
889
  const isHidden = this.dom.configForm.classList.contains('hidden');
890
+ this.dom.configForm.classList.toggle('hidden');
891
+ this.dom.toggleConfigBtn.querySelector('i').className = isHidden ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line';
 
 
 
 
 
892
  });
893
 
894
  this.dom.saveConfigBtn.addEventListener('click', () => {
 
897
  const proxy = this.dom.corsProxyInput.value.trim();
898
 
899
  if (!storeName || !token) {
900
+ alert("Preencha o nome da loja e o token.");
901
  return;
902
  }
903
 
 
907
  setTimeout(() => this.dom.configStatus.classList.add('hidden'), 3000);
908
  });
909
 
 
910
  this.dom.dateSelect.addEventListener('change', (e) => {
911
  this.dateRangeOption = e.target.value;
912
  this.toggleCustomDateInput();
 
978
  },
979
 
980
  async fetchOrders() {
 
981
  const config = ShopifyService.loadConfig();
982
  if (!config || !config.storeName || !config.accessToken) {
983
+ alert("Configure as credenciais primeiro.");
984
  this.dom.configSection.scrollIntoView({ behavior: 'smooth' });
985
  return;
986
  }
 
988
  this.setLoading(true);
989
  this.error = null;
990
  this.dom.errorBanner.classList.add('hidden');
991
+ this.dom.newOrdersSection.classList.add('hidden');
992
 
993
  const { startDate, endDate } = this.getDateRange();
994
 
995
  try {
 
996
  const fetchedOrders = await ShopifyService.fetchAllOrders(startDate, endDate);
997
 
 
 
 
 
 
 
 
998
  const report = this.analyzeNewOrders(fetchedOrders, this.snapshot);
999
  this.newOrdersReport = report;
1000
 
1001
  const newSnapshot = this.createSnapshot(fetchedOrders);
1002
  this.snapshot = newSnapshot;
1003
 
1004
+ this.allSessionOrders = fetchedOrders;
1005
  this.isFirstLoad = false;
1006
  this.hasLoadedData = true;
 
1007
 
1008
  this.processOrders(this.allSessionOrders);
1009
+ this.totalOrders = this.allSessionOrders.length;