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

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +550 -624
index.html CHANGED
@@ -5,679 +5,628 @@
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 {
116
- display: flex;
117
- justify-content: space-between;
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
 
662
  </main>
663
 
664
  <script>
665
  /**
666
- * Shopify Service
667
- * Handles communication with Shopify Admin API
668
  */
669
- const ShopifyService = {
670
- config: {
671
- storeName: '',
672
- accessToken: '',
673
- corsProxy: '',
674
- apiVersion: '2024-01'
675
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676
 
677
  loadConfig() {
678
- const stored = localStorage.getItem('shopify_app_config');
679
  if (stored) {
680
- this.config = JSON.parse(stored);
681
  return this.config;
682
  }
683
  return null;
@@ -685,36 +634,33 @@
685
 
686
  saveConfig(newConfig) {
687
  this.config = { ...this.config, ...newConfig };
688
- localStorage.setItem('shopify_app_config', JSON.stringify(this.config));
689
  },
690
 
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'
 
699
  });
700
 
701
- if (pageinfo) {
702
- params.append('page_info', pageinfo);
703
- }
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;
713
  },
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 = [];
@@ -724,34 +670,26 @@
724
  try {
725
  const response = await fetch(url, {
726
  method: 'GET',
727
- headers: {
728
- 'X-Shopify-Access-Token': this.config.accessToken,
729
- 'Content-Type': 'application/json'
730
- }
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();
740
- if (data.orders) {
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');
747
- url = nextUrl;
748
- } else {
749
- url = null;
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
  }
@@ -765,245 +703,233 @@
765
  for (let link of links) {
766
  const parts = link.split(';');
767
  const url = parts[0].trim().replace(/<|>/g, '');
768
- if (parts[1].includes(`rel="${rel}"`)) {
769
- return url;
770
- }
771
  }
772
  return null;
773
  }
774
  };
775
 
776
  /**
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() {
803
  this.dom = {
804
  // Config
805
- configSection: document.getElementById('config-section'),
806
- configForm: document.getElementById('config-form'),
807
- toggleConfigBtn: document.getElementById('toggle-config-btn'),
808
- storeNameInput: document.getElementById('store-name'),
809
- accessTokenInput: document.getElementById('access-token'),
810
- corsProxyInput: document.getElementById('cors-proxy'),
811
- saveConfigBtn: document.getElementById('save-config-btn'),
812
- configStatus: document.getElementById('config-status'),
813
-
814
- // Filters
815
- dateSelect: document.getElementById('date-range-select'),
816
- customDateGroup: document.getElementById('custom-date-group'),
817
- customStart: document.getElementById('custom-date-start'),
818
- customEnd: document.getElementById('custom-date-end'),
819
- refreshBtn: document.getElementById('refresh-button'),
820
- exportBtn: document.getElementById('export-csv-button'),
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'),
831
- newOrdersSection: document.getElementById('new-orders-section'),
832
- newOrdersContent: document.getElementById('new-orders-content'),
833
- campaignBreakdown: document.getElementById('campaign-breakdown'),
834
- diffTime: document.getElementById('diff-time'),
 
 
 
835
 
836
  // Metrics
837
- summarySection: document.getElementById('summary-metrics-section'),
838
- metricTotalPedidos: document.getElementById('metric-total-pedidos'),
839
- metricUniqueUtms: document.getElementById('metric-unique-utms'),
840
- metricTaxaGeral: document.getElementById('metric-taxa-geral'),
841
- badgeTaxaGeral: document.getElementById('badge-taxa-geral'),
842
- metricTotalVendas: document.getElementById('metric-total-vendas'),
843
- metricVendasPagas: document.getElementById('metric-vendas-pagas'),
844
- metricCountPagas: document.getElementById('metric-count-pagas'),
845
 
846
  // Table
847
- tableSection: document.getElementById('table-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) {
880
- this.dom.storeNameInput.value = config.storeName || '';
881
- this.dom.accessTokenInput.value = config.accessToken || '';
882
- this.dom.corsProxyInput.value = config.corsProxy || '';
883
- this.enableButtons();
 
 
 
 
 
 
884
  }
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', () => {
895
- const storeName = this.dom.storeNameInput.value.trim();
896
- const token = this.dom.accessTokenInput.value.trim();
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
-
904
- ShopifyService.saveConfig({ storeName, accessToken: token, corsProxy: proxy });
905
- this.dom.configStatus.classList.remove('hidden');
906
- this.enableButtons();
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();
913
- });
914
 
915
- this.dom.refreshBtn.addEventListener('click', () => this.fetchOrders());
916
- this.dom.initialLoadBtn.addEventListener('click', () => this.fetchOrders());
917
- this.dom.resetSnapshotBtn.addEventListener('click', () => this.resetSnapshot());
918
- this.dom.exportBtn.addEventListener('click', () => this.exportCSV());
919
  },
920
 
921
- enableButtons() {
922
- this.dom.refreshBtn.disabled = false;
923
- this.dom.initialLoadBtn.disabled = false;
924
  },
925
 
926
- toggleCustomDateInput() {
927
- if (this.dateRangeOption === 'customizado') {
928
- this.dom.customDateGroup.classList.remove('hidden');
929
- } else {
930
- this.dom.customDateGroup.classList.add('hidden');
931
- }
932
  },
933
 
934
- checkInitialView() {
935
- this.dom.noDataSection.classList.remove('hidden');
936
- this.dom.loadingState.classList.add('hidden');
937
- this.dom.tableWrapper.classList.add('hidden');
938
- this.dom.emptyState.classList.add('hidden');
 
 
939
  },
940
 
941
- getDateRange() {
 
942
  const now = new Date();
943
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
944
- let startDate, endDate;
 
945
 
946
- switch (this.dateRangeOption) {
947
- case 'hoje':
948
- startDate = today;
949
- endDate = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1);
950
- break;
951
- case 'ontem':
952
- startDate = new Date(today.getTime() - 24 * 60 * 60 * 1000);
953
- endDate = new Date(today.getTime() - 1);
954
- break;
955
- case 'ultimos7':
956
- startDate = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
957
- endDate = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1);
958
- break;
959
- case 'ultimos30':
960
- startDate = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
961
- endDate = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1);
962
- break;
963
  case 'customizado':
964
- if (this.dom.customStart.value && this.dom.customEnd.value) {
965
- startDate = new Date(this.dom.customStart.value);
966
- endDate = new Date(this.dom.customEnd.value);
967
- endDate.setHours(23, 59, 59, 999);
968
  } else {
969
- startDate = today;
970
- endDate = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1);
971
  }
972
  break;
973
- default:
974
- startDate = today;
975
- endDate = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1);
976
  }
977
- return { startDate: startDate.toISOString(), endDate: endDate.toISOString() };
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
  }
987
 
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;
 
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 --- */
78
+ * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
79
+
80
  body {
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
 
130
+ .brand {
 
 
 
 
 
 
 
 
 
 
131
  display: flex;
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 {
142
  display: inline-flex;
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 {
157
+ background-color: var(--primary);
158
+ color: #fff;
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 {
168
  background-color: var(--bg-surface);
169
+ border-color: var(--border-default);
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;
210
  align-items: center;
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;
224
+ font-weight: 600;
225
+ color: var(--text-muted);
226
  text-transform: uppercase;
227
  letter-spacing: 0.05em;
228
  }
229
 
230
  input, select {
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;
238
  width: 100%;
239
  }
 
240
  input:focus, select:focus {
241
+ border-color: var(--primary);
242
+ box-shadow: 0 0 0 3px var(--primary-dim);
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;
312
+ border-bottom: 1px solid var(--border-subtle);
313
+ color: var(--text-main);
314
  }
 
315
  tr:last-child td { border-bottom: none; }
316
+ tr:hover td { background-color: rgba(255,255,255,0.02); }
317
+ [data-theme="light"] tr:hover td { background-color: rgba(0,0,0,0.02); }
 
 
 
 
 
318
 
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">
442
+ <label for="cors-proxy">CORS Proxy (Opcional)</label>
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>
465
+ <option value="ultimos7" selected>脷ltimos 7 dias</option>
466
  <option value="ultimos30">脷ltimos 30 dias</option>
467
  <option value="customizado">Customizado</option>
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);
594
+ },
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;
 
634
 
635
  saveConfig(newConfig) {
636
  this.config = { ...this.config, ...newConfig };
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
 
654
  if (this.config.corsProxy && this.config.corsProxy.trim() !== '') {
655
  const proxy = this.config.corsProxy.replace(/\/$/, '');
656
+ finalUrl = `${proxy}${encodeURIComponent(finalUrl)}`;
657
  }
 
658
  return finalUrl;
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 = [];
 
670
  try {
671
  const response = await fetch(url, {
672
  method: 'GET',
673
+ headers: { 'X-Shopify-Access-Token': this.config.accessToken }
 
 
 
674
  });
675
 
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
  }
 
703
  for (let link of links) {
704
  const parts = link.split(';');
705
  const url = parts[0].trim().replace(/<|>/g, '');
706
+ if (parts[1].includes(`rel="${rel}"`)) return url;
 
 
707
  }
708
  return null;
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