eubottura commited on
Commit
49eb513
·
verified ·
1 Parent(s): 03dfb76

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +817 -736
index.html CHANGED
@@ -3,44 +3,40 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Relatório UTM Content | Shopify Analytics</title>
7
- <!-- Importando ícones modernos -->
8
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
 
10
  <style>
11
  :root {
12
- /* Palette - Profitfy Style (Dark/Pro) */
13
- --bg-body: #0f1115; /* Deep dark gray/black */
14
- --bg-card: #1c1f26; /* Slightly lighter card bg */
15
- --bg-hover: #252932;
16
- --border-color: #2e333d;
 
 
 
17
 
18
- --text-primary: #ffffff;
19
- --text-secondary: #9ca3af;
20
- --text-muted: #6b7280;
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- /* Status Colors */
23
- --success: #10b981; /* Emerald 500 */
24
- --success-glow: rgba(16, 185, 129, 0.2);
25
- --warning: #f59e0b; /* Amber 500 */
26
- --danger: #ef4444; /* Red 500 */
27
- --info: #3b82f6; /* Blue 500 */
28
-
29
- /* Metrics Colors */
30
- --metric-card-bg: #141719;
31
- --metric-card-border: #2e333d;
32
-
33
- /* Spacing & Radius */
34
- --radius-sm: 6px;
35
- --radius-md: 10px;
36
- --radius-lg: 16px;
37
- --spacing-xs: 4px;
38
- --spacing-sm: 8px;
39
- --spacing-md: 16px;
40
- --spacing-lg: 24px;
41
-
42
- /* Typography */
43
- --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
44
  }
45
 
46
  * {
@@ -50,858 +46,943 @@
50
  }
51
 
52
  body {
53
- background-color: var(--bg-body);
54
- color: var(--text-primary);
55
  font-family: var(--font-family);
 
 
56
  line-height: 1.5;
57
- min-height: 100vh;
58
- display: flex;
59
- flex-direction: column;
 
 
 
 
 
 
60
  }
61
 
62
- /* --- Header --- */
63
  header {
64
- background-color: var(--bg-card);
65
- border-bottom: 1px solid var(--border-color);
66
- padding: var(--spacing-md) var(--spacing-lg);
67
- display: flex;
68
- justify-content: space-between;
69
- align-items: center;
70
  position: sticky;
71
  top: 0;
72
  z-index: 100;
73
- backdrop-filter: blur(10px);
74
- -webkit-backdrop-filter: blur(10px);
75
  }
76
 
77
- .brand {
78
  display: flex;
 
79
  align-items: center;
80
- gap: var(--spacing-sm);
81
- font-weight: 700;
82
- font-size: 1.1rem;
83
- color: var(--text-primary);
84
- text-decoration: none;
85
  }
86
 
87
- .brand i {
88
- color: var(--info);
 
89
  }
90
 
91
- .status-badge {
92
- font-size: 0.75rem;
93
- padding: 4px 8px;
94
- border-radius: 100px;
95
- background: rgba(255, 255, 255, 0.05);
96
- color: var(--text-secondary);
97
- border: 1px solid var(--border-color);
98
  display: flex;
99
  align-items: center;
100
- gap: 6px;
 
101
  }
102
 
103
- .status-dot {
104
- width: 8px;
105
- height: 8px;
106
- border-radius: 50%;
107
- background-color: var(--text-muted);
108
  }
109
- .status-dot.active { background-color: var(--success); box-shadow: 0 0 8px var(--success); }
110
 
111
- /* --- Main Layout --- */
112
- main {
113
- flex: 1;
114
- padding: var(--spacing-lg);
115
- max-width: 1200px;
116
- margin: 0 auto;
117
- width: 100%;
 
118
  }
119
 
120
- /* --- Controls Area --- */
 
 
 
 
 
 
 
 
 
 
121
  .controls {
122
  display: flex;
123
  flex-wrap: wrap;
124
- gap: var(--spacing-md);
125
- margin-bottom: var(--spacing-lg);
126
- align-items: center;
127
- justify-content: space-between;
128
  }
129
 
130
- .date-display {
131
- font-size: 0.9rem;
132
- color: var(--text-secondary);
 
133
  }
134
 
135
- .btn-group {
136
- display: flex;
137
- gap: var(--spacing-sm);
 
138
  }
139
 
140
- button {
141
- cursor: pointer;
142
- border: none;
143
- border-radius: var(--radius-sm);
144
- padding: 8px 16px;
145
- font-size: 0.9rem;
146
- font-weight: 600;
147
- display: flex;
 
 
 
 
 
 
 
 
 
 
148
  align-items: center;
 
149
  gap: 8px;
150
- transition: all 0.2s ease;
 
 
 
 
 
 
 
 
 
 
 
 
151
  }
152
 
153
  .btn-primary {
154
- background-color: var(--text-primary);
155
- color: var(--bg-body);
156
  }
157
- .btn-primary:hover {
158
- background-color: #e5e7eb;
159
- transform: translateY(-1px);
160
  }
161
 
162
- .btn-outline {
163
- background-color: transparent;
164
- border: 1px solid var(--border-color);
165
- color: var(--text-primary);
166
  }
167
- .btn-outline:hover {
168
- background-color: var(--bg-hover);
169
- border-color: var(--text-secondary);
 
170
  }
171
 
172
- /* --- Snapshot / Delta Section --- */
173
- .delta-card {
174
- background-color: var(--metric-card-bg);
175
- border: 1px solid var(--metric-card-border);
176
- border-radius: var(--radius-md);
177
- padding: var(--spacing-md);
178
- margin-bottom: var(--spacing-lg);
179
- display: none; /* Hidden by default */
180
- animation: slideDown 0.3s ease-out;
181
  }
182
- .delta-card.visible { display: block; }
183
 
184
- @keyframes slideDown {
185
- from { opacity: 0; transform: translateY(-10px); }
186
- to { opacity: 1; transform: translateY(0); }
 
187
  }
188
 
189
- .delta-header {
190
- display: flex;
191
- align-items: center;
192
- gap: var(--spacing-sm);
193
- color: var(--text-secondary);
194
- font-size: 0.85rem;
195
- margin-bottom: var(--spacing-sm);
196
- text-transform: uppercase;
197
- letter-spacing: 0.5px;
198
  }
199
 
200
- .delta-main {
201
- display: grid;
202
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
203
- gap: var(--spacing-md);
204
  }
205
 
206
- .delta-item {
207
- background: rgba(255,255,255,0.03);
208
- padding: var(--spacing-sm);
209
- border-radius: var(--radius-sm);
210
- border-left: 3px solid var(--text-muted);
211
  }
212
- .delta-item.highlight { border-left-color: var(--success); }
213
-
214
- .delta-label { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 2px; }
215
- .delta-value { font-size: 1.25rem; font-weight: 700; color: var(--text-primary); }
216
- .delta-sub { font-size: 0.7rem; color: var(--text-muted); margin-top: 2px; }
217
 
218
- /* --- Metrics Overview --- */
219
  .metrics-grid {
220
  display: grid;
221
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
222
- gap: var(--spacing-md);
223
- margin-bottom: var(--spacing-lg);
224
  }
225
 
226
  .metric-card {
227
- background-color: var(--metric-card-bg);
228
- border: 1px solid var(--metric-card-border);
229
- border-radius: var(--radius-md);
230
- padding: var(--spacing-md);
231
- position: relative;
232
- overflow: hidden;
233
  }
234
 
235
- .metric-card::before {
236
- content: '';
237
- position: absolute;
238
- top: 0; left: 0; width: 4px; height: 100%;
239
- background-color: var(--text-muted);
240
  }
241
 
242
- .metric-card.orders::before { background-color: var(--info); }
243
- .metric-card.revenue::before { background-color: var(--success); }
244
- .metric-card.paid::before { background-color: var(--warning); }
 
 
245
 
246
- .metric-label { font-size: 0.85rem; color: var(--text-secondary); margin-bottom: var(--spacing-xs); }
247
- .metric-value { font-size: 1.5rem; font-weight: 700; color: var(--text-primary); }
248
- .metric-trend { font-size: 0.8rem; margin-top: 4px; display: flex; align-items: center; gap: 4px; }
249
- .trend-up { color: var(--success); }
250
- .trend-neutral { color: var(--text-secondary); }
251
 
252
- /* --- Data Table --- */
253
  .table-container {
254
- background-color: var(--bg-card);
255
- border: 1px solid var(--border-color);
256
- border-radius: var(--radius-md);
257
- overflow: hidden;
258
  overflow-x: auto;
 
 
259
  }
260
 
261
  table {
262
  width: 100%;
263
  border-collapse: collapse;
264
- font-size: 0.9rem;
265
- text-align: left;
266
- }
267
-
268
- thead {
269
- background-color: rgba(0,0,0,0.2);
270
  }
271
 
272
  th {
 
 
273
  padding: 12px 16px;
274
  font-weight: 600;
275
- color: var(--text-secondary);
276
- border-bottom: 1px solid var(--border-color);
277
  white-space: nowrap;
278
  cursor: pointer;
279
  user-select: none;
280
  }
281
- th:hover { color: var(--text-primary); background-color: rgba(255,255,255,0.02); }
282
- th i { margin-left: 4px; font-size: 0.8em; opacity: 0.5; }
 
 
283
 
284
  td {
285
  padding: 12px 16px;
286
- border-bottom: 1px solid var(--border-color);
287
- color: var(--text-primary);
288
  }
289
 
290
- tr:last-child td { border-bottom: none; }
291
- tr:hover { background-color: var(--bg-hover); }
 
292
 
293
- /* Table Specifics */
294
- .utm-content-cell {
295
- display: flex;
296
- flex-direction: column;
297
- gap: 2px;
298
  }
299
-
300
- .utm-badge {
 
301
  display: inline-flex;
302
  align-items: center;
303
- justify-content: center;
304
- min-width: 40px;
305
- height: 28px;
306
- padding: 0 8px;
307
- border-radius: 4px;
308
- font-size: 0.85rem;
309
- font-weight: 700;
310
- color: var(--text-primary);
311
- background: linear-gradient(135deg, var(--info), #2563eb);
312
- box-shadow: 0 2px 4px rgba(0,0,0,0.3);
313
- text-transform: uppercase;
314
- }
315
-
316
- .utm-full-text {
317
  font-size: 0.75rem;
318
- color: var(--text-secondary);
319
- font-family: monospace;
320
- white-space: nowrap;
321
- overflow: hidden;
322
- text-overflow: ellipsis;
323
- max-width: 300px;
324
  }
325
 
326
- .progress-bar-bg {
327
- width: 100%;
328
- height: 6px;
329
- background-color: rgba(255,255,255,0.1);
330
- border-radius: 3px;
331
- margin-top: 6px;
332
- overflow: hidden;
333
- }
334
-
335
- .progress-bar-fill {
336
- height: 100%;
337
- border-radius: 3px;
338
- transition: width 0.5s ease;
339
- }
340
 
341
- .badge-status {
342
- font-size: 0.7rem;
343
- padding: 2px 6px;
 
344
  border-radius: 4px;
345
- font-weight: 600;
346
- text-transform: uppercase;
 
347
  }
348
- .status-paid { background-color: rgba(16, 185, 129, 0.15); color: var(--success); }
349
- .status-pending { background-color: rgba(239, 68, 68, 0.15); color: var(--danger); }
350
 
351
- .currency {
352
- font-family: 'Roboto Mono', monospace;
 
353
  }
354
 
355
- /* --- Empty State --- */
356
- .empty-state {
357
- text-align: center;
358
- padding: 60px 20px;
359
- color: var(--text-secondary);
360
- }
361
- .empty-state i {
362
- font-size: 3rem;
363
- margin-bottom: var(--spacing-md);
364
- opacity: 0.3;
365
  }
366
-
367
- /* --- Footer --- */
368
- footer {
369
- margin-top: auto;
370
- padding: var(--spacing-md);
371
- text-align: center;
372
- color: var(--text-muted);
373
- font-size: 0.8rem;
374
- border-top: 1px solid var(--border-color);
375
- background-color: var(--bg-card);
376
  }
377
 
378
- /* --- Utility Classes --- */
379
- .text-success { color: var(--success); }
380
- .text-danger { color: var(--danger); }
381
- .text-warning { color: var(--warning); }
382
-
383
  .hidden { display: none !important; }
384
-
385
  /* Loading Spinner */
386
  .spinner {
387
- width: 16px;
388
- height: 16px;
389
- border: 2px solid rgba(0,0,0,0.1);
390
- border-left-color: var(--text-primary);
391
  border-radius: 50%;
392
- animation: spin 0.8s linear infinite;
 
393
  }
394
- @keyframes spin { to { transform: rotate(360deg); } }
395
 
 
 
 
 
 
 
 
396
  </style>
397
  </head>
398
  <body>
399
 
400
- <!-- Header -->
401
  <header>
402
- <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="brand">
403
- <i class="fa-solid fa-chart-pie"></i>
404
- <span>UTM Content Analytics</span>
405
- </a>
406
- <div class="status-badge">
407
- <div class="status-dot" id="statusDot"></div>
408
- <span id="statusText">Sistema Pronto</span>
409
  </div>
410
  </header>
411
 
412
- <main>
413
- <!-- Controls -->
414
- <div class="controls">
415
- <div class="date-display" id="dateDisplay">
416
- <!-- JS will populate -->
417
- </div>
418
- <div class="btn-group">
419
- <button class="btn-primary" id="btnUpdate" onclick="handleUpdate()">
420
- <i class="fa-solid fa-rotate"></i> <span>Atualizar Dados</span>
421
- </button>
422
- <button class="btn-outline" onclick="exportToCSV()">
423
- <i class="fa-solid fa-download"></i> <span>CSV</span>
424
- </button>
425
  </div>
426
  </div>
427
 
428
- <!-- Delta / Changes Card -->
429
- <div class="delta-card" id="deltaCard">
430
- <div class="delta-header">
431
- <i class="fa-solid fa-clock-rotate-left"></i> Novas vendas desde a última atualização
432
- </div>
433
- <div class="delta-main">
434
- <div class="delta-item highlight">
435
- <div class="delta-label">Novos Pedidos</div>
436
- <div class="delta-value" id="deltaOrders">0</div>
437
- </div>
438
- <div class="delta-item">
439
- <div class="delta-label">Novo Total (R$)</div>
440
- <div class="delta-value text-success" id="deltaRevenue">R$ 0,00</div>
441
  </div>
442
- <div class="delta-item">
443
- <div class="delta-label">Novos Pagos (R$)</div>
444
- <div class="delta-value text-success" id="deltaPaid">R$ 0,00</div>
 
 
445
  </div>
446
- <div class="delta-item">
447
- <div class="delta-label">Tempo Decorrido</div>
448
- <div class="delta-value" id="deltaTime">0m</div>
 
 
 
 
 
449
  </div>
450
  </div>
451
- <div style="margin-top: 12px; text-align: right;">
452
- <button class="btn-outline" style="font-size: 0.8rem; padding: 4px 10px;" onclick="resetSnapshot()">
453
- <i class="fa-solid fa-rotate-right"></i> Resetar Ponto de Partida
454
- </button>
455
- </div>
 
456
  </div>
457
 
458
- <!-- Metrics Summary -->
459
- <div class="metrics-grid">
460
- <div class="metric-card orders">
461
- <div class="metric-label">Total de Pedidos</div>
462
- <div class="metric-value" id="metricTotalOrders">0</div>
463
- <div class="metric-trend trend-neutral">Todos os pedidos</div>
464
- </div>
465
- <div class="metric-card revenue">
466
- <div class="metric-label">Vendas Totais (R$)</div>
467
- <div class="metric-value" id="metricTotalRevenue">R$ 0,00</div>
468
- </div>
469
- <div class="metric-card paid">
470
- <div class="metric-label">Vendas Pagas (R$)</div>
471
- <div class="metric-value" id="metricPaidRevenue">R$ 0,00</div>
472
- </div>
473
- <div class="metric-card paid">
474
- <div class="metric-label">Taxa de Pagamento</div>
475
- <div class="metric-value" id="metricConversionRate">0%</div>
476
- <div class="metric-trend trend-neutral" id="metricConversionBadge">Aguardando dados</div>
477
  </div>
478
  </div>
479
 
480
- <!-- Data Table -->
481
- <div class="table-container">
482
- <table id="dataTable">
483
- <thead>
484
- <tr>
485
- <th onclick="sortTable('utm')">UTM Content <i class="fa-solid fa-sort"></i></th>
486
- <th onclick="sortTable('orders')">Total Pedidos <i class="fa-solid fa-sort"></i></th>
487
- <th onclick="sortTable('paid')">Pagos / Total <i class="fa-solid fa-sort"></i></th>
488
- <th onclick="sortTable('customers')">Clientes <i class="fa-solid fa-sort"></i></th>
489
- <th onclick="sortTable('revenue')">Vendas (R$) <i class="fa-solid fa-sort"></i></th>
490
- <th onclick="sortTable('paidRev')">Vendas Pagas (R$) <i class="fa-solid fa-sort"></i></th>
491
- </tr>
492
- </thead>
493
- <tbody id="tableBody">
494
- <!-- Rows generated by JS -->
495
- </tbody>
496
- </table>
497
-
498
- <!-- Empty State -->
499
- <div id="emptyState" class="empty-state">
500
- <i class="fa-solid fa-box-open"></i>
501
- <h3>Nenhum dado encontrado</h3>
502
- <p>Clique em "Atualizar Dados" para buscar os pedidos.</p>
503
  </div>
504
  </div>
505
- </main>
506
-
507
- <footer>
508
- <p>Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" style="color: var(--text-primary); text-decoration: none;">anycoder</a></p>
509
- </footer>
510
-
511
- <script>
512
- // --- Configuration ---
513
- const CONFIG = {
514
- shopUrl: 'usevenzara.myshopify.com',
515
- apiVersion: '2024-01',
516
- // Note: Since this is a client-side demo, we cannot securely store a real token.
517
- // We will simulate the API call structure but return mock data for demonstration
518
- // unless a user provides a token in a real environment.
519
- // For this specific request, we will use the provided mock data from the prompt
520
- // to ensure the UI works perfectly as requested.
521
- };
522
-
523
- // --- State Management ---
524
- const STATE = {
525
- data: [],
526
- snapshot: null, // { timestamp, orderIds: [] }
527
- isLoading: false,
528
- sortBy: 'orders', // orders, paid, revenue
529
- sortAsc: false
530
- };
531
-
532
- // --- Mock Data (Based on User Prompt) ---
533
- // Simulating the result of the GraphQL query for today (25/01/2026)
534
- const MOCK_DB = [
535
- { id: 2000, name: "#2000", total_price: 97.90, status: "PENDING", utm_content: null },
536
- { id: 2001, name: "#2001", total_price: 116.57, status: "PAID", utm_content: "[ABO] [COMPRADORES] [38 ] [E002]" },
537
- { id: 2002, name: "#2002", total_price: 110.44, status: "PAID", utm_content: "[ABO] [ACESSORIOS] [38 ] [E005]" },
538
- { id: 2003, name: "#2003", total_price: 97.90, status: "PAID", utm_content: "[ABO] [VESTUARIO] [38 ] [E004]" },
539
- { id: 2004, name: "#2004", total_price: 97.90, status: "PAID", utm_content: "[ABO] [ACESSORIOS] [38 ] [E020]" },
540
- { id: 2005, name: "#2005", total_price: 97.90, status: "PAID", utm_content: "[ABO] [COMPRADORES] [38 ] [E018]" },
541
- { id: 2006, name: "#2006", total_price: 97.90, status: "PAID", utm_content: "[ABO] [ACESSORIOS] [38 ] [E014]" },
542
- { id: 2007, name: "#2007", total_price: 97.90, status: "PAID", utm_content: "[CBO] [ABERTO] [38 ] [E007]" },
543
- { id: 2008, name: "#2008", total_price: 97.90, status: "PAID", utm_content: "[ABO] [ACESSORIOS] [38 ] [E011]" },
544
- { id: 2009, name: "#2009", total_price: 110.44, status: "PAID", utm_content: "[ABO] [VESTUARIO] [38 ] [E004]" }, // Duplicate ID for realism
545
- { id: 2010, name: "#2010", total_price: 89.90, status: "PAID", utm_content: "[CBO] [VESTUARIO] [38 ] [I007]" },
546
- { id: 2011, name: "#2011", total_price: 97.90, status: "PAID", utm_content: "[ABO] [ACESSORIOS] [38 ] [E012]" },
547
- { id: 2012, name: "#2012", total_price: 94.90, status: "PAID", utm_content: "[ABO] [VESTUARIO] [38 ] [A122]" },
548
- { id: 2013, name: "#2013", total_price: 94.90, status: "PAID", utm_content: "[ABO] [VESTUARIO] [38 ] [A116]" },
549
- { id: 2014, name: "#2014", total_price: 97.90, status: "PAID", utm_content: "[CBO] [VESTUARIO] [38 ] [H006]" },
550
- { id: 2015, name: "#2015", total_price: 97.90, status: "PAID", utm_content: "[ABO] [ABERTO] [38 ] [H004]" },
551
- { id: 2016, name: "#2016", total_price: 195.80, status: "PAID", utm_content: "[CBO] [MIX] [38 ] [H008]" },
552
- { id: 2017, name: "#2017", total_price: 195.80, status: "PAID", utm_content: "[ABO] [MIX] [38 ] [H005]" },
553
- { id: 2018, name: "#2018", total_price: 195.80, status: "PAID", utm_content: "[CBO] [ABERTO] [38 ] [H007]" },
554
- { id: 2019, name: "#2019", total_price: 195.80, status: "PAID", utm_content: "[ABO] [COMPRADORES] [38 ] [E010]" },
555
- { id: 2020, name: "#2020", total_price: 195.80, status: "PAID", utm_content: "[ABO] [ACESSORIOS] [38 ] [E015]" },
556
- { id: 2021, name: "#2021", total_price: 195.80, status: "PAID", utm_content: "[ABO] [COMPRADORES] [38 ] [E021]" },
557
- { id: 2022, name: "#2022", total_price: 195.80, status: "PAID", utm_content: "[ABO] [SALAO] [38 ] [H001]" },
558
- { id: 2023, name: "#2023", total_price: 291.70, status: "PAID", utm_content: "[ABO] [COMPRADORES] [38 ] [E013]" },
559
- { id: 2024, name: "#2024", total_price: 291.70, status: "PAID", utm_content: "[CBO] [VESTUARIO] [38 ] [E006]" },
560
- { id: 2025, name: "#2025", total_price: 291.70, status: "PAID", utm_content: "[CBO] [MIX] [38 ] [H008]" },
561
- { id: 2026, name: "#2026", total_price: 291.70, status: "PAID", utm_content: "[ABO] [COMPRADORES] [38 ] [E008]" },
562
- { id: 2027, name: "#2027", total_price: 291.70, status: "PAID", utm_content: "[ABO] [MIX] [38 ] [E008]" },
563
- { id: 2028, name: "#2028", total_price: 291.70, status: "PAID", utm_content: "[ABO] [COMPRADORES] [38 ] [E008]" },
564
- { id: 2029, name: "#2029", total_price: 291.70, status: "PAID", utm_content: "[ABO] [MIX] [38 ] [E008]" },
565
- { id: 2030, name: "#2030", total_price: 388.70, status: "PAID", utm_content: "[ABO] [COMPRADORES] [38 ] [E008]" },
566
- { id: 2031, name: "#2031", total_price: 388.70, status: "PAID", utm_content: "[ABO] [MIX] [38 ] [E008]" },
567
- { id: 2032, name: "#2032", total_price: 388.70, status: "PAID", utm_content: "[ABO] [COMPRADORES] [38 ] [E008]" },
568
- { id: 2033, name: "#2033", total_price: 388.70, status: "PAID", utm_content: "[ABO] [MIX] [38 ] [E008]" },
569
- { id: 2034, name: "#2034", total_price: 485.70, status: "PAID", utm_content: "Sem UTM" },
570
- { id: 2035, name: "#2035", total_price: 485.70, status: "PAID", utm_content: "Sem UTM" },
571
- { id: 2036, name: "#2036", total_price: 485.70, status: "PAID", utm_content: "Sem UTM" },
572
- { id: 2037, name: "#2037", total_price: 485.70, status: "PAID", utm_content: "Sem UTM" },
573
- { id: 2038, name: "#2038", total_price: 485.70, status: "PAID", utm_content: "Sem UTM" },
574
- { id: 2039, name: "#2039", total_price: 485.70, status: "PAID", utm_content: "Sem UTM" },
575
- { id: 2040, name: "#2040", total_price: 485.70, status: "PAID", utm_content: "Sem UTM" }
576
- ];
577
-
578
- // --- Core Functions ---
579
-
580
- function init() {
581
- // Set Date
582
- const today = new Date();
583
- const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
584
- document.getElementById('dateDisplay').textContent = today.toLocaleDateString('pt-BR', options);
585
-
586
- // Check if we have a snapshot (simulated persistence)
587
- const savedSnapshot = localStorage.getItem('utm_report_snapshot');
588
- if (savedSnapshot) {
589
- try {
590
- STATE.snapshot = JSON.parse(savedSnapshot);
591
- } catch (e) {
592
- console.error("Failed to load snapshot", e);
593
- STATE.snapshot = null;
594
- }
595
- }
596
- }
597
 
598
- async function handleUpdate() {
599
- if (STATE.isLoading) return;
600
-
601
- setLoading(true);
602
- updateStatus("Buscando dados...", "loading");
603
-
604
- // Simulate API Network Delay
605
- await new Promise(r => setTimeout(r, 1500));
606
-
607
- // Process Data (Simulating GraphQL extraction)
608
- const rawData = MOCK_DB;
609
-
610
- // Calculate Aggregations
611
- const aggregated = processAggregations(rawData);
612
-
613
- // Compare with Snapshot
614
- const delta = calculateDelta(rawData);
615
-
616
- // Update State
617
- STATE.data = aggregated;
618
- STATE.snapshot = {
619
- timestamp: new Date().toISOString(),
620
- orderIds: rawData.map(o => o.id)
621
- };
622
-
623
- // Save Snapshot
624
- try {
625
- localStorage.setItem('utm_report_snapshot', JSON.stringify(STATE.snapshot));
626
- } catch (e) {
627
- console.warn("LocalStorage not available (Sidekick limitation)");
628
- }
629
-
630
- // Render UI
631
- renderTable();
632
- renderMetrics();
633
- renderDelta(delta);
634
-
635
- setLoading(false);
636
- updateStatus("Sincronizado", "active");
637
- }
638
-
639
- function processAggregations(orders) {
640
- const groups = {};
641
-
642
- orders.forEach(order => {
643
- const utm = order.utm_content || "Sem UTM Content";
644
- const isPaid = order.status === 'PAID';
645
- const price = parseFloat(order.total_price);
646
 
647
- if (!groups[utm]) {
648
- groups[utm] = {
649
- utm_content: utm,
650
- total_orders: 0,
651
- paid_orders: 0,
652
- unique_customers: new Set(),
653
- total_revenue: 0,
654
- paid_revenue: 0
655
- };
656
- }
657
 
658
- groups[utm].total_orders++;
659
- groups[utm].total_revenue += price;
 
 
660
 
661
- if (isPaid) {
662
- groups[utm].paid_orders++;
663
- groups[utm].paid_revenue += price;
664
- }
665
-
666
- // In a real app, we'd have a customer ID here.
667
- // For this demo, we'll use the Order ID as a proxy for uniqueness if no email exists,
668
- // or just assume 1 customer per order for simplicity unless specified.
669
- // Let's assume 1 unique customer per order for this specific dataset structure.
670
- groups[utm].unique_customers.add(order.id);
671
- });
672
-
673
- // Convert to array and sort
674
- let result = Object.values(groups);
675
-
676
- // Sort by Primary Metric
677
- result.sort((a, b) => {
678
- let valA, valB;
679
- if (STATE.sortBy === 'orders') { valA = a.total_orders; valB = b.total_orders; }
680
- else if (STATE.sortBy === 'paid') { valA = a.paid_orders; valB = b.paid_orders; }
681
- else if (STATE.sortBy === 'revenue') { valA = a.total_revenue; valB = b.total_revenue; }
682
- else if (STATE.sortBy === 'paidRev') { valA = a.paid_revenue; valB = b.paid_revenue; }
683
- else { valA = a.utm_content.localeCompare(b.utm_content); }
684
-
685
- return STATE.sortAsc ? valA - valB : valB - valA;
686
- });
687
-
688
- return result;
689
- }
690
-
691
- function calculateDelta(newOrders) {
692
- if (!STATE.snapshot || !STATE.snapshot.orderIds) {
693
- return { hasChanges: false };
694
- }
695
-
696
- const newIds = new Set(newOrders.map(o => o.id));
697
- const oldIds = new Set(STATE.snapshot.orderIds);
698
-
699
- const newOrdersList = newOrders.filter(o => !oldIds.has(o.id));
700
-
701
- if (newOrdersList.length === 0) {
702
- return { hasChanges: false };
703
- }
704
-
705
- const totalRevenue = newOrdersList.reduce((sum, o) => sum + o.total_price, 0);
706
- const paidRevenue = newOrdersList.filter(o => o.status === 'PAID').reduce((sum, o) => sum + o.total_price, 0);
707
-
708
- const timeDiff = Math.floor((new Date() - new Date(STATE.snapshot.timestamp)) / 60000);
709
 
710
- return {
711
- hasChanges: true,
712
- count: newOrdersList.length,
713
- revenue: totalRevenue,
714
- paidRevenue: paidRevenue,
715
- timeDiff: timeDiff
716
- };
717
- }
718
 
719
- // --- Rendering ---
720
 
721
- function renderTable() {
722
- const tbody = document.getElementById('tableBody');
723
- const emptyState = document.getElementById('emptyState');
724
- const table = document.getElementById('dataTable');
725
 
726
- if (STATE.data.length === 0) {
727
- table.classList.add('hidden');
728
- emptyState.classList.remove('hidden');
729
- return;
730
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
 
732
- table.classList.remove('hidden');
733
- emptyState.classList.add('hidden');
 
 
 
 
 
 
 
734
 
735
- tbody.innerHTML = '';
 
 
 
736
 
737
- STATE.data.forEach(group => {
738
- const conversion = group.total_orders > 0
739
- ? (group.paid_orders / group.total_orders) * 100
740
- : 0;
741
-
742
- // Determine Color
743
- let colorClass = 'text-success';
744
- let barColor = 'var(--success)';
745
- if (conversion < 50) { colorClass = 'text-danger'; barColor = 'var(--danger)'; }
746
- else if (conversion < 70) { colorClass = 'text-warning'; barColor = 'var(--warning)'; }
747
-
748
- // Extract Last Parameter for Badge
749
- const lastParam = group.utm_content.split(']').pop().trim();
750
-
751
- const tr = document.createElement('tr');
752
- tr.innerHTML = `
753
- <td>
754
- <div class="utm-content-cell">
755
- <span class="utm-badge">${lastParam}</span>
756
- <span class="utm-full-text" title="${group.utm_content}">${group.utm_content}</span>
757
- </div>
758
- </td>
759
- <td>${group.total_orders}</td>
760
- <td>
761
- <div style="display:flex; flex-direction:column; gap:4px;">
762
- <div style="display:flex; justify-content:space-between; font-size:0.8rem;">
763
- <span>${group.paid_orders}</span>
764
- <span class="${colorClass}">${conversion.toFixed(0)}%</span>
765
- </div>
766
- <div class="progress-bar-bg">
767
- <div class="progress-bar-fill" style="width: ${conversion}%; background-color: ${barColor};"></div>
768
- </div>
769
- </div>
770
- </td>
771
- <td>${group.unique_customers.size}</td>
772
- <td class="currency">R$ ${group.total_revenue.toFixed(2).replace('.', ',')}</td>
773
- <td class="currency text-success">R$ ${group.paid_revenue.toFixed(2).replace('.', ',')}</td>
774
- `;
775
- tbody.appendChild(tr);
776
- });
777
- }
778
-
779
- function renderMetrics() {
780
- const totalOrders = STATE.data.reduce((sum, g) => sum + g.total_orders, 0);
781
- const totalRev = STATE.data.reduce((sum, g) => sum + g.total_revenue, 0);
782
- const paidRev = STATE.data.reduce((sum, g) => sum + g.paid_revenue, 0);
783
- const convRate = totalOrders > 0 ? (paidRev / totalRev) * 100 : 0;
784
-
785
- document.getElementById('metricTotalOrders').textContent = totalOrders;
786
- document.getElementById('metricTotalRevenue').textContent = formatCurrency(totalRev);
787
- document.getElementById('metricPaidRevenue').textContent = formatCurrency(paidRev);
788
-
789
- const convEl = document.getElementById('metricConversionRate');
790
- const badgeEl = document.getElementById('metricConversionBadge');
791
-
792
- convEl.textContent = convRate.toFixed(1) + '%';
793
- convEl.className = 'metric-value ' + (convRate >= 70 ? 'text-success' : (convRate >= 50 ? 'text-warning' : 'text-danger'));
794
-
795
- badgeEl.textContent = convRate >= 70 ? 'Ótima Taxa' : (convRate >= 50 ? 'Média' : 'Baixa');
796
- badgeEl.className = 'metric-trend ' + (convRate >= 70 ? 'trend-up' : 'trend-neutral');
797
- }
798
 
799
- function renderDelta(delta) {
800
- const card = document.getElementById('deltaCard');
801
-
802
- if (!delta.hasChanges) {
803
- card.classList.remove('visible');
804
- return;
805
- }
806
 
807
- card.classList.add('visible');
808
- document.getElementById('deltaOrders').textContent = delta.count;
809
- document.getElementById('deltaRevenue').textContent = formatCurrency(delta.revenue);
810
- document.getElementById('deltaPaid').textContent = formatCurrency(delta.paidRevenue);
 
 
 
 
 
 
 
811
 
812
- let timeStr = '';
813
- if (delta.timeDiff < 60) timeStr = `${delta.timeDiff} min`;
814
- else if (delta.timeDiff < 1440) timeStr = `${Math.floor(delta.timeDiff/60)}h ${delta.timeDiff%60}m`;
815
- else timeStr = `${Math.floor(delta.timeDiff/1440)}d`;
816
 
817
- document.getElementById('deltaTime').textContent = timeStr;
818
- }
819
-
820
- // --- Utilities ---
821
-
822
- function formatCurrency(value) {
823
- return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
824
- }
825
-
826
- function setLoading(isLoading) {
827
- STATE.isLoading = isLoading;
828
- const btn = document.getElementById('btnUpdate');
829
- const icon = btn.querySelector('i');
830
- const span = btn.querySelector('span');
831
-
832
- if (isLoading) {
833
- icon.className = 'fa-solid fa-spinner spinner';
834
- span.textContent = 'Carregando...';
835
- btn.disabled = true;
836
- } else {
837
- icon.className = 'fa-solid fa-rotate';
838
- span.textContent = 'Atualizar Dados';
839
- btn.disabled = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
840
  }
841
- }
842
 
843
- function updateStatus(text, type) {
844
- const dot = document.getElementById('statusDot');
845
- const txt = document.getElementById('statusText');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
846
 
847
- txt.textContent = text;
848
- dot.className = 'status-dot ' + (type === 'active' ? 'active' : '');
849
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
850
 
851
- function sortTable(key) {
852
- if (STATE.sortBy === key) {
853
- STATE.sortAsc = !STATE.sortAsc;
854
- } else {
855
- STATE.sortBy = key;
856
- STATE.sortAsc = false; // Default to Descending for numbers
857
- }
858
- // Re-process and render
859
- STATE.data = processAggregations(MOCK_DB); // Re-sorts
860
- renderTable();
861
- }
862
 
863
- function resetSnapshot() {
864
- if(confirm("Deseja resetar o ponto de partida? Os dados anteriores serão considerados como base.")) {
865
- STATE.snapshot = null;
866
- localStorage.removeItem('utm_report_snapshot');
867
- document.getElementById('deltaCard').classList.remove('visible');
868
- updateStatus("Resetado", "neutral");
869
- }
870
- }
871
 
872
- function exportToCSV() {
873
- if (STATE.data.length === 0) {
874
- alert("Não há dados para exportar.");
875
- return;
876
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
877
 
878
- let csvContent = "data:text/csv;charset=utf-8,";
879
- csvContent += "UTM Content;Total Pedidos;Pedidos Pagos;Clientes;Total Vendas (R$);Vendas Pagas (R$)\r\n";
880
-
881
- STATE.data.forEach(row => {
882
- const rowStr = [
883
- `"${row.utm_content}"`,
884
- row.total_orders,
885
- row.paid_orders,
886
- row.unique_customers.size,
887
- row.total_revenue.toFixed(2).replace('.', ','),
888
- row.paid_revenue.toFixed(2).replace('.', ',')
889
- ].join(";");
890
- csvContent += rowStr + "\r\n";
891
- });
892
-
893
- const encodedUri = encodeURI(csvContent);
894
- const link = document.createElement("a");
895
- link.setAttribute("href", encodedUri);
896
- link.setAttribute("download", `relatorio_utm_${new Date().toISOString().split('T')[0]}.csv`);
897
- document.body.appendChild(link);
898
- link.click();
899
- document.body.removeChild(link);
900
- }
901
-
902
- // Initialize App
903
- init();
904
-
905
- </script>
906
- </body>
907
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Relatório de Vendas por UTM</title>
7
+ <!-- Importing Remix Icon for modern UI icons -->
8
+ <link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
9
 
10
  <style>
11
  :root {
12
+ /* Shopify Polaris-inspired Color Palette */
13
+ --color-bg-surface: #ffffff;
14
+ --color-bg-surface-hover: #f6f6f7;
15
+ --color-bg-surface-subdued: #f1f2f3;
16
+ --color-text-primary: #202223;
17
+ --color-text-secondary: #6d7175;
18
+ --color-border: #e1e3e5;
19
+ --color-border-hover: #babfc3;
20
 
21
+ --color-primary: #008060;
22
+ --color-primary-hover: #004c3f;
23
+ --color-bg-primary: #008060;
24
+ --color-text-on-primary: #ffffff;
25
+
26
+ --color-critical: #d82c0d;
27
+ --color-warning: #ffc453;
28
+ --color-warning-bg: #fffaed;
29
+ --color-success: #008060;
30
+ --color-success-bg: #e4f5ec;
31
+ --color-info: #202223;
32
+
33
+ --font-family: -apple-system, BlinkMacSystemFont, "San Francisco", "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
34
+ --border-radius: 8px;
35
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
36
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
37
 
38
+ --spacing-base: 16px;
39
+ --spacing-large: 24px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
 
42
  * {
 
46
  }
47
 
48
  body {
 
 
49
  font-family: var(--font-family);
50
+ background-color: var(--color-bg-surface-subdued);
51
+ color: var(--color-text-primary);
52
  line-height: 1.5;
53
+ -webkit-font-smoothing: antialiased;
54
+ padding-bottom: 40px;
55
+ }
56
+
57
+ /* Layout & Containers */
58
+ .container {
59
+ max-width: 1200px;
60
+ margin: 0 auto;
61
+ padding: 0 var(--spacing-base);
62
  }
63
 
 
64
  header {
65
+ background-color: var(--color-bg-surface);
66
+ border-bottom: 1px solid var(--color-border);
67
+ padding: var(--spacing-base) 0;
68
+ margin-bottom: var(--spacing-base);
 
 
69
  position: sticky;
70
  top: 0;
71
  z-index: 100;
 
 
72
  }
73
 
74
+ .header-content {
75
  display: flex;
76
+ justify-content: space-between;
77
  align-items: center;
 
 
 
 
 
78
  }
79
 
80
+ h1 {
81
+ font-size: 1.25rem;
82
+ font-weight: 600;
83
  }
84
 
85
+ .anycoder-link {
86
+ font-size: 0.875rem;
87
+ color: var(--color-text-secondary);
88
+ text-decoration: none;
 
 
 
89
  display: flex;
90
  align-items: center;
91
+ gap: 4px;
92
+ transition: color 0.2s;
93
  }
94
 
95
+ .anycoder-link:hover {
96
+ color: var(--color-primary);
 
 
 
97
  }
 
98
 
99
+ /* Sections */
100
+ section {
101
+ background: var(--color-bg-surface);
102
+ border-radius: var(--border-radius);
103
+ box-shadow: var(--shadow-sm);
104
+ padding: var(--spacing-large);
105
+ margin-bottom: var(--spacing-base);
106
+ border: 1px solid var(--color-border);
107
  }
108
 
109
+ .section-header {
110
+ margin-bottom: var(--spacing-base);
111
+ }
112
+
113
+ h2 {
114
+ font-size: 1.125rem;
115
+ font-weight: 600;
116
+ margin-bottom: 0.5rem;
117
+ }
118
+
119
+ /* Controls */
120
  .controls {
121
  display: flex;
122
  flex-wrap: wrap;
123
+ gap: var(--spacing-base);
124
+ align-items: flex-end;
 
 
125
  }
126
 
127
+ .control-group {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 6px;
131
  }
132
 
133
+ label {
134
+ font-size: 0.875rem;
135
+ font-weight: 500;
136
+ color: var(--color-text-primary);
137
  }
138
 
139
+ select, input[type="date"] {
140
+ padding: 8px 12px;
141
+ border: 1px solid var(--color-border);
142
+ border-radius: 4px;
143
+ font-size: 1rem;
144
+ background-color: var(--color-bg-surface);
145
+ min-width: 200px;
146
+ }
147
+
148
+ select:focus, input:focus {
149
+ outline: 2px solid var(--color-primary);
150
+ outline-offset: -1px;
151
+ border-color: transparent;
152
+ }
153
+
154
+ /* Buttons */
155
+ .btn {
156
+ display: inline-flex;
157
  align-items: center;
158
+ justify-content: center;
159
  gap: 8px;
160
+ padding: 8px 16px;
161
+ border-radius: 4px;
162
+ font-weight: 500;
163
+ font-size: 1rem;
164
+ cursor: pointer;
165
+ transition: all 0.2s;
166
+ border: 1px solid transparent;
167
+ text-decoration: none;
168
+ }
169
+
170
+ .btn:disabled {
171
+ opacity: 0.6;
172
+ cursor: not-allowed;
173
  }
174
 
175
  .btn-primary {
176
+ background-color: var(--color-bg-primary);
177
+ color: var(--color-text-on-primary);
178
  }
179
+
180
+ .btn-primary:hover:not(:disabled) {
181
+ background-color: var(--color-primary-hover);
182
  }
183
 
184
+ .btn-secondary {
185
+ background-color: var(--color-bg-surface);
186
+ border-color: var(--color-border);
187
+ color: var(--color-text-primary);
188
  }
189
+
190
+ .btn-secondary:hover:not(:disabled) {
191
+ background-color: var(--color-bg-surface-hover);
192
+ border-color: var(--color-border-hover);
193
  }
194
 
195
+ /* Banners */
196
+ .banner {
197
+ padding: 12px 16px;
198
+ border-radius: var(--border-radius);
199
+ margin-bottom: var(--spacing-base);
200
+ display: flex;
201
+ gap: 12px;
202
+ align-items: flex-start;
203
+ font-size: 0.9375rem;
204
  }
 
205
 
206
+ .banner-warning {
207
+ background-color: var(--color-warning-bg);
208
+ color: #5c4508;
209
+ border: 1px solid #f0a83b;
210
  }
211
 
212
+ .banner-critical {
213
+ background-color: #fff1f0;
214
+ color: #840505;
215
+ border: 1px solid #d82c0d;
 
 
 
 
 
216
  }
217
 
218
+ .banner-info {
219
+ background-color: #edf7fe;
220
+ color: #042a46;
221
+ border: 1px solid #007ace;
222
  }
223
 
224
+ .banner-icon {
225
+ font-size: 1.25rem;
 
 
 
226
  }
 
 
 
 
 
227
 
228
+ /* Metrics Grid */
229
  .metrics-grid {
230
  display: grid;
231
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
232
+ gap: var(--spacing-base);
 
233
  }
234
 
235
  .metric-card {
236
+ padding: var(--spacing-base);
237
+ background-color: var(--color-bg-surface);
238
+ border: 1px solid var(--color-border);
239
+ border-radius: var(--border-radius);
 
 
240
  }
241
 
242
+ .metric-label {
243
+ font-size: 0.875rem;
244
+ color: var(--color-text-secondary);
245
+ margin-bottom: 4px;
 
246
  }
247
 
248
+ .metric-value {
249
+ font-size: 1.5rem;
250
+ font-weight: 600;
251
+ color: var(--color-text-primary);
252
+ }
253
 
254
+ .metric-sub {
255
+ font-size: 0.8125rem;
256
+ color: var(--color-text-secondary);
257
+ margin-top: 4px;
258
+ }
259
 
260
+ /* Table */
261
  .table-container {
262
+ width: 100%;
 
 
 
263
  overflow-x: auto;
264
+ border: 1px solid var(--color-border);
265
+ border-radius: var(--border-radius);
266
  }
267
 
268
  table {
269
  width: 100%;
270
  border-collapse: collapse;
271
+ font-size: 0.9375rem;
 
 
 
 
 
272
  }
273
 
274
  th {
275
+ background-color: var(--color-bg-surface-subdued);
276
+ text-align: left;
277
  padding: 12px 16px;
278
  font-weight: 600;
279
+ color: var(--color-text-secondary);
280
+ border-bottom: 1px solid var(--color-border);
281
  white-space: nowrap;
282
  cursor: pointer;
283
  user-select: none;
284
  }
285
+
286
+ th:hover {
287
+ background-color: #e3e4e5;
288
+ }
289
 
290
  td {
291
  padding: 12px 16px;
292
+ border-bottom: 1px solid var(--color-border);
293
+ vertical-align: middle;
294
  }
295
 
296
+ tr:last-child td {
297
+ border-bottom: none;
298
+ }
299
 
300
+ tr.total-row td {
301
+ background-color: var(--color-bg-surface-subdued);
302
+ font-weight: 600;
303
+ border-top: 2px solid var(--color-border);
 
304
  }
305
+
306
+ /* Badges & Indicators */
307
+ .badge {
308
  display: inline-flex;
309
  align-items: center;
310
+ padding: 2px 8px;
311
+ border-radius: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
312
  font-size: 0.75rem;
313
+ font-weight: 600;
314
+ text-transform: uppercase;
 
 
 
 
315
  }
316
 
317
+ .badge-success { background-color: var(--color-success-bg); color: var(--color-success); }
318
+ .badge-warning { background-color: var(--color-warning-bg); color: #805b00; }
319
+ .badge-critical { background-color: #fff1f0; color: var(--color-critical); }
 
 
 
 
 
 
 
 
 
 
 
320
 
321
+ .status-bar-container {
322
+ width: 120px;
323
+ height: 8px;
324
+ background-color: #e1e3e5;
325
  border-radius: 4px;
326
+ overflow: hidden;
327
+ display: inline-block;
328
+ vertical-align: middle;
329
  }
 
 
330
 
331
+ .status-bar-fill {
332
+ height: 100%;
333
+ background-color: var(--color-success);
334
  }
335
 
336
+ /* New Orders Card */
337
+ .new-orders-grid {
338
+ display: grid;
339
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
340
+ gap: 12px;
341
+ margin-top: 12px;
 
 
 
 
342
  }
343
+
344
+ .campaign-card {
345
+ border: 1px solid var(--color-border);
346
+ border-radius: 4px;
347
+ padding: 12px;
348
+ background: #fafafa;
 
 
 
 
349
  }
350
 
351
+ /* Utility */
352
+ .text-subdued { color: var(--color-text-secondary); }
353
+ .text-strong { font-weight: 600; }
 
 
354
  .hidden { display: none !important; }
355
+
356
  /* Loading Spinner */
357
  .spinner {
358
+ border: 3px solid rgba(0, 0, 0, 0.1);
359
+ width: 24px;
360
+ height: 24px;
 
361
  border-radius: 50%;
362
+ border-left-color: var(--color-primary);
363
+ animation: spin 1s linear infinite;
364
  }
 
365
 
366
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
367
+
368
+ /* Responsive adjustments */
369
+ @media (max-width: 640px) {
370
+ .controls { flex-direction: column; align-items: stretch; }
371
+ .header-content { flex-direction: column; gap: 10px; text-align: center; }
372
+ }
373
  </style>
374
  </head>
375
  <body>
376
 
 
377
  <header>
378
+ <div class="container header-content">
379
+ <h1>Relatório Detalhado: Vendas vs. Pagamentos por UTM</h1>
380
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
381
+ Built with anycoder <i class="ri-external-link-line"></i>
382
+ </a>
 
 
383
  </div>
384
  </header>
385
 
386
+ <main class="container">
387
+
388
+ <!-- Banner Limitação -->
389
+ <div id="limitation-banner" class="banner banner-warning">
390
+ <i class="ri-alert-fill banner-icon"></i>
391
+ <div>
392
+ <p class="text-strong">⚠️ Limitação: Os dados não persistem entre sessões neste ambiente.</p>
393
+ <p>💡 Dica: Mantenha esta aba aberta para não perder os dados, ou exporte para CSV regularmente.</p>
 
 
 
 
 
394
  </div>
395
  </div>
396
 
397
+ <!-- Controles -->
398
+ <section id="filters-section">
399
+ <div class="controls">
400
+ <div class="control-group">
401
+ <label for="date-range-select">Período</label>
402
+ <select id="date-range-select">
403
+ <option value="hoje">Hoje</option>
404
+ <option value="ontem">Ontem</option>
405
+ <option value="ultimos7">Últimos 7 dias</option>
406
+ <option value="ultimos30">Últimos 30 dias</option>
407
+ <option value="customizado">Período customizado</option>
408
+ </select>
 
409
  </div>
410
+
411
+ <div class="control-group hidden" id="custom-date-group">
412
+ <label for="custom-date-picker">Data Início - Fim</label>
413
+ <input type="date" id="custom-date-start">
414
+ <input type="date" id="custom-date-end" style="margin-top: 5px;">
415
  </div>
416
+
417
+ <div style="display: flex; gap: 12px; margin-top: auto;">
418
+ <button id="refresh-button" class="btn btn-primary">
419
+ <i class="ri-refresh-line"></i> Atualizar Dados
420
+ </button>
421
+ <button id="export-csv-button" class="btn btn-secondary" disabled>
422
+ <i class="ri-download-line"></i> Exportar CSV
423
+ </button>
424
  </div>
425
  </div>
426
+ </section>
427
+
428
+ <!-- Erro -->
429
+ <div id="error-banner" class="banner banner-critical hidden">
430
+ <i class="ri-error-warning-fill banner-icon"></i>
431
+ <span id="error-message"></span>
432
  </div>
433
 
434
+ <!-- Info Snapshot -->
435
+ <div id="last-update-info" class="hidden">
436
+ <div style="background: var(--color-bg-surface-subdued); padding: 12px; border-radius: 8px; border: 1px solid var(--color-border);">
437
+ <p class="text-strong">📅 Dados de: <span id="snapshot-time"></span></p>
438
+ <p class="text-subdued">Última atualização manual</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
  </div>
440
  </div>
441
 
442
+ <!-- Dia Mudou -->
443
+ <div id="day-changed-banner" class="banner banner-info hidden">
444
+ <i class="ri-sun-fill banner-icon"></i>
445
+ <div>
446
+ <p class="text-strong">🌅 Novo dia detectado</p>
447
+ <p>Clique em 'Atualizar Dados' para iniciar novo ciclo e ver as vendas do novo dia.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  </div>
449
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
 
451
+ <!-- Estado Vazio Inicial -->
452
+ <div id="no-data-section" class="hidden" style="text-align: center; padding: 40px;">
453
+ <h2>Aguardando primeira atualização</h2>
454
+ <p class="text-subdued" style="margin-bottom: 20px;">Nenhum dado carregado nesta sessão. Clique em 'Atualizar Dados' para começar.</p>
455
+ <button id="initial-load-button" class="btn btn-primary">🔄 Atualizar Dados</button>
456
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
 
458
+ <!-- Primeira Load Info -->
459
+ <div id="first-load-section" class="hidden">
460
+ <div style="background: var(--color-bg-surface-subdued); padding: 12px; border-radius: 8px; border: 1px solid var(--color-border);">
461
+ <p class="text-strong">ℹ️ Primeira atualização - estabelecendo ponto de partida</p>
462
+ <p class="text-subdued">Os dados foram carregados. Na próxima atualização, você verá as novas vendas desde este momento.</p>
463
+ </div>
464
+ </div>
 
 
 
465
 
466
+ <!-- Novos Pedidos (Delta) -->
467
+ <div id="new-orders-section" class="hidden">
468
+ <div style="background: var(--color-bg-surface-subdued); padding: 16px; border-radius: 8px; border: 1px solid var(--color-border);">
469
+ <h2>🔔 Novas Vendas desde a Última Atualização</h2>
470
 
471
+ <div style="margin-top: 12px;">
472
+ <p><span class="text-strong">⏱️ Tempo desde última atualização:</span> <span id="diff-time"></span></p>
473
+
474
+ <div id="new-orders-content">
475
+ <!-- Populated via JS -->
476
+ </div>
477
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
479
+ <div id="campaign-breakdown" style="margin-top: 16px;">
480
+ <!-- Populated via JS -->
481
+ </div>
 
 
 
 
 
482
 
483
+ <hr style="border: 0; border-top: 1px solid var(--color-border); margin: 16px 0;">
484
 
485
+ <button id="reset-snapshot-button" class="btn btn-secondary" style="font-size: 0.875rem;">🔄 Resetar Ponto de Partida</button>
486
+ </div>
487
+ </div>
 
488
 
489
+ <!-- Métricas -->
490
+ <section id="summary-metrics-section" class="hidden">
491
+ <h2>Resumo Geral</h2>
492
+ <div class="metrics-grid">
493
+ <div class="metric-card">
494
+ <p class="metric-label">📦 Total de Pedidos</p>
495
+ <p class="metric-value" id="metric-total-pedidos">0</p>
496
+ <p class="metric-sub" id="metric-unique-utms">0 UTMs únicos</p>
497
+ </div>
498
+ <div class="metric-card">
499
+ <p class="metric-label">📊 Taxa de Pagamento Geral</p>
500
+ <p class="metric-value" id="metric-taxa-geral">0%</p>
501
+ <span class="badge" id="badge-taxa-geral">-</span>
502
+ </div>
503
+ <div class="metric-card">
504
+ <p class="metric-label">💰 Total de Vendas</p>
505
+ <p class="metric-value" id="metric-total-vendas">R$ 0,00</p>
506
+ <p class="metric-sub">Bruto</p>
507
+ </div>
508
+ <div class="metric-card">
509
+ <p class="metric-label">✅ Vendas Pagas</p>
510
+ <p class="metric-value" id="metric-vendas-pagas">R$ 0,00</p>
511
+ <p class="metric-sub" id="metric-count-pagas">0 pedidos pagos</p>
512
+ </div>
513
+ </div>
514
+ </section>
515
 
516
+ <!-- Tabela -->
517
+ <section id="table-section">
518
+ <div id="loading-state" class="hidden" style="text-align: center; padding: 40px;">
519
+ <div style="display: inline-block; vertical-align: middle;">
520
+ <div class="spinner"></div>
521
+ </div>
522
+ <h2 style="margin-top: 10px; display: inline-block; margin-left: 10px;">Carregando dados...</h2>
523
+ <p class="text-subdued">Buscando pedidos e processando informações de UTM</p>
524
+ </div>
525
 
526
+ <div id="empty-state" class="hidden" style="text-align: center; padding: 40px;">
527
+ <h2>Nenhum pedido encontrado</h2>
528
+ <p class="text-subdued">Não há pedidos para o período selecionado.</p>
529
+ </div>
530
 
531
+ <div id="table-wrapper" class="hidden">
532
+ <div class="table-container">
533
+ <table id="utm-report-table">
534
+ <thead>
535
+ <tr>
536
+ <th onclick="app.handleSort('lastParameter')">Campanha ↕</th>
537
+ <th>Pagos / Total</th>
538
+ <th onclick="app.handleSort('clientesUnicos')">Clientes ↕</th>
539
+ <th onclick="app.handleSort('totalVendas')">Total Vendas ↕</th>
540
+ <th onclick="app.handleSort('vendasPagas')">Vendas Pagas ↕</th>
541
+ <th onclick="app.handleSort('taxaPagamento')">Taxa ↕</th>
542
+ </tr>
543
+ </thead>
544
+ <tbody id="table-body">
545
+ <!-- Rows injected here -->
546
+ </tbody>
547
+ </table>
548
+ </div>
549
+ </div>
550
+ </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
 
552
+ </main>
 
 
 
 
 
 
553
 
554
+ <script>
555
+ /**
556
+ * Mock Data Generator
557
+ * Simulates Shopify API behavior for demonstration purposes
558
+ */
559
+ const MockDataGenerator = {
560
+ utmSources: [
561
+ 'instagram_stories_offer1', 'google_search_brand', 'facebook_feed_retargeting',
562
+ 'tiktok_influencer_john', 'email_newsletter_may', 'whatsapp_broadcast_launch',
563
+ 'google_display_generic', 'instagram_reels_video_a'
564
+ ],
565
 
566
+ generateId: () => Math.random().toString(36).substr(2, 9),
 
 
 
567
 
568
+ generateOrder(date, isNew = false) {
569
+ const isPaid = Math.random() > 0.3; // 70% chance of being paid
570
+ const utmContent = this.utmSources[Math.floor(Math.random() * this.utmSources.length)];
571
+ const price = (Math.random() * 500 + 50).toFixed(2);
572
+
573
+ // If "isNew", time is closer to "now", otherwise random within the day
574
+ const hours = isNew ? Math.floor(Math.random() * 2) : Math.floor(Math.random() * 24);
575
+ const minutes = Math.floor(Math.random() * 60);
576
+
577
+ const orderDate = new Date(date);
578
+ orderDate.setHours(hours, minutes, 0, 0);
579
+
580
+ return {
581
+ id: `gid://shopify/Order/${this.generateId()}`,
582
+ name: `#${Math.floor(Math.random() * 10000) + 1000}`,
583
+ createdAt: orderDate.toISOString(),
584
+ displayFinancialStatus: isPaid ? 'PAID' : 'PENDING',
585
+ totalPriceSet: { shopMoney: { amount: price } },
586
+ customer: { email: `customer${Math.floor(Math.random()*1000)}@example.com` },
587
+ customAttributes: [
588
+ { key: 'utm_content', value: utmContent }
589
+ ]
590
+ };
591
+ },
592
+
593
+ getOrders(startDate, endDate, existingIds = new Set()) {
594
+ return new Promise((resolve) => {
595
+ setTimeout(() => {
596
+ const start = new Date(startDate);
597
+ const end = new Date(endDate);
598
+ const orders = [];
599
+ const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
600
+
601
+ // Generate roughly 5-15 orders per day in range
602
+ const countPerDay = Math.floor(Math.random() * 10) + 5;
603
+
604
+ for (let i = 0; i < daysDiff * countPerDay; i++) {
605
+ // Random date in range
606
+ const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
607
+ const order = this.generateOrder(date);
608
+
609
+ // Ensure unique ID for this session
610
+ if (!existingIds.has(order.id)) {
611
+ orders.push(order);
612
+ existingIds.add(order.id);
613
+ }
614
+ }
615
+
616
+ // Sort by date desc
617
+ orders.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
618
+ resolve(orders);
619
+ }, 800); // Simulate network delay
620
+ });
621
+ },
622
+
623
+ // Simulate "New" orders appearing on refresh
624
+ getNewOrders(existingOrders) {
625
+ return new Promise((resolve) => {
626
+ setTimeout(() => {
627
+ const count = Math.floor(Math.random() * 4) + 1; // 1 to 4 new orders
628
+ const newOrders = [];
629
+ const now = new Date();
630
+
631
+ for(let i=0; i<count; i++) {
632
+ const order = this.generateOrder(now, true);
633
+ newOrders.push(order);
634
+ }
635
+ resolve(newOrders);
636
+ }, 800);
637
+ });
638
  }
639
+ };
640
 
641
+ /**
642
+ * Main Application Logic
643
+ */
644
+ const app = {
645
+ // State
646
+ dateRangeOption: 'hoje',
647
+ customDateRange: '',
648
+ loading: false,
649
+ error: null,
650
+ utmData: [],
651
+ totalOrders: 0,
652
+ lastUpdate: '',
653
+ sortColumn: 'totalPedidos',
654
+ sortDirection: 'desc',
655
+ snapshot: null, // { timestamp, orderIds: Set }
656
+ newOrdersReport: null,
657
+ isFirstLoad: true,
658
+ hasLoadedData: false,
659
+ dayChanged: false,
660
 
661
+ // In-memory store of all fetched orders to simulate persistence during session
662
+ allSessionOrders: [],
663
+ sessionOrderIds: new Set(),
664
+
665
+ init() {
666
+ this.cacheDOM();
667
+ this.bindEvents();
668
+ this.checkInitialView();
669
+ },
670
+
671
+ cacheDOM() {
672
+ this.dom = {
673
+ dateSelect: document.getElementById('date-range-select'),
674
+ customDateGroup: document.getElementById('custom-date-group'),
675
+ customStart: document.getElementById('custom-date-start'),
676
+ customEnd: document.getElementById('custom-date-end'),
677
+ refreshBtn: document.getElementById('refresh-button'),
678
+ exportBtn: document.getElementById('export-csv-button'),
679
+ initialLoadBtn: document.getElementById('initial-load-button'),
680
+ resetSnapshotBtn: document.getElementById('reset-snapshot-button'),
681
+
682
+ limitationBanner: document.getElementById('limitation-banner'),
683
+ errorBanner: document.getElementById('error-banner'),
684
+ errorMsg: document.getElementById('error-message'),
685
+
686
+ lastUpdateInfo: document.getElementById('last-update-info'),
687
+ snapshotTime: document.getElementById('snapshot-time'),
688
+ dayChangedBanner: document.getElementById('day-changed-banner'),
689
+
690
+ noDataSection: document.getElementById('no-data-section'),
691
+ firstLoadSection: document.getElementById('first-load-section'),
692
+ newOrdersSection: document.getElementById('new-orders-section'),
693
+ newOrdersContent: document.getElementById('new-orders-content'),
694
+ campaignBreakdown: document.getElementById('campaign-breakdown'),
695
+ diffTime: document.getElementById('diff-time'),
696
+
697
+ summarySection: document.getElementById('summary-metrics-section'),
698
+ metricTotalPedidos: document.getElementById('metric-total-pedidos'),
699
+ metricUniqueUtms: document.getElementById('metric-unique-utms'),
700
+ metricTaxaGeral: document.getElementById('metric-taxa-geral'),
701
+ badgeTaxaGeral: document.getElementById('badge-taxa-geral'),
702
+ metricTotalVendas: document.getElementById('metric-total-vendas'),
703
+ metricVendasPagas: document.getElementById('metric-vendas-pagas'),
704
+ metricCountPagas: document.getElementById('metric-count-pagas'),
705
+
706
+ tableSection: document.getElementById('table-section'),
707
+ loadingState: document.getElementById('loading-state'),
708
+ emptyState: document.getElementById('empty-state'),
709
+ tableWrapper: document.getElementById('table-wrapper'),
710
+ tableBody: document.getElementById('table-body')
711
+ };
712
+ },
713
+
714
+ bindEvents() {
715
+ this.dom.dateSelect.addEventListener('change', (e) => {
716
+ this.dateRangeOption = e.target.value;
717
+ this.toggleCustomDateInput();
718
+ });
719
+
720
+ this.dom.refreshBtn.addEventListener('click', () => this.fetchOrders());
721
+ this.dom.initialLoadBtn.addEventListener('click', () => this.fetchOrders());
722
+ this.dom.resetSnapshotBtn.addEventListener('click', () => this.resetSnapshot());
723
+ this.dom.exportBtn.addEventListener('click', () => this.exportCSV());
724
+ },
725
+
726
+ toggleCustomDateInput() {
727
+ if (this.dateRangeOption === 'customizado') {
728
+ this.dom.customDateGroup.classList.remove('hidden');
729
+ } else {
730
+ this.dom.customDateGroup.classList.add('hidden');
731
+ }
732
+ },
733
+
734
+ checkInitialView() {
735
+ // On first load, show the "No Data" section
736
+ this.dom.noDataSection.classList.remove('hidden');
737
+ this.dom.loadingState.classList.add('hidden');
738
+ this.dom.tableWrapper.classList.add('hidden');
739
+ this.dom.emptyState.classList.add('hidden');
740
+ },
741
+
742
+ getDateRange() {
743
+ const now = new Date();
744
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
745
+ let startDate, endDate;
746
+
747
+ switch (this.dateRangeOption) {
748
+ case 'hoje':
749
+ startDate = today;
750
+ endDate = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1);
751
+ break;
752
+ case 'ontem':
753
+ startDate = new Date(today.getTime() - 24 * 60 * 60 * 1000);
754
+ endDate = new Date(today.getTime() - 1);
755
+ break;
756
+ case 'ultimos7':
757
+ startDate = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
758
+ endDate = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1);
759
+ break;
760
+ case 'ultimos30':
761
+ startDate = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
762
+ endDate = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1);
763
+ break;
764
+ case 'customizado':
765
+ if (this.dom.customStart.value && this.dom.customEnd.value) {
766
+ startDate = new Date(this.dom.customStart.value);
767
+ endDate = new Date(this.dom.customEnd.value);
768
+ endDate.setHours(23, 59, 59, 999);
769
+ } else {
770
+ // Default to today if invalid
771
+ startDate = today;
772
+ endDate = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1);
773
+ }
774
+ break;
775
+ default:
776
+ startDate = today;
777
+ endDate = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1);
778
+ }
779
+ return { startDate: startDate.toISOString(), endDate: endDate.toISOString() };
780
+ },
781
 
782
+ async fetchOrders() {
783
+ this.setLoading(true);
784
+ this.error = null;
785
+ this.dom.errorBanner.classList.add('hidden');
 
 
 
 
 
 
 
786
 
787
+ const { startDate, endDate } = this.getDateRange();
 
 
 
 
 
 
 
788
 
789
+ try {
790
+ let fetchedOrders = [];
791
+
792
+ if (this.isFirstLoad || this.dateRangeOption !== 'hoje') {
793
+ // Full fetch
794
+ fetchedOrders = await MockDataGenerator.getOrders(startDate, endDate, this.sessionOrderIds);
795
+ this.allSessionOrders = fetchedOrders;
796
+ } else {
797
+ // Simulate fetching new orders only if "Hoje" and not first load
798
+ const newOrders = await MockDataGenerator.getNewOrders(this.allSessionOrders);
799
+ this.allSessionOrders = [...newOrders, ...this.allSessionOrders];
800
+ fetchedOrders = this.allSessionOrders;
801
+ }
802
+
803
+ // Logic from original React code
804
+ const report = this.analyzeNewOrders(this.allSessionOrders, this.snapshot);
805
+ this.newOrdersReport = report;
806
+
807
+ const newSnapshot = this.createSnapshot(this.allSessionOrders);
808
+ this.snapshot = newSnapshot;
809
+ this.isFirstLoad = false;
810
+ this.hasLoadedData = true;
811
+ this.dayChanged = false;
812
+
813
+ this.processOrders(this.allSessionOrders);
814
+ this.totalOrders = this.allSessionOrders.length;
815
+
816
+ const now = new Date();
817
+ this.lastUpdate = now.toLocaleString('pt-BR');
818
+
819
+ this.updateUI();
820
+
821
+ } catch (err) {
822
+ this.error = err.message || 'Erro ao carregar pedidos.';
823
+ this.dom.errorMsg.textContent = this.error;
824
+ this.dom.errorBanner.classList.remove('hidden');
825
+ } finally {
826
+ this.setLoading(false);
827
+ }
828
+ },
829
+
830
+ analyzeNewOrders(currentOrders, previousSnapshot) {
831
+ if (!previousSnapshot) return null;
832
+
833
+ const previousOrderIds = previousSnapshot.orderIds;
834
+ const newOrders = currentOrders.filter((order) => !previousOrderIds.has(order.id));
835
+
836
+ if (newOrders.length === 0) {
837
+ return {
838
+ newOrderCount: 0,
839
+ newOrdersTotal: 0,
840
+ newOrdersPaid: 0,
841
+ newOrdersPaidCount: 0,
842
+ newOrderNumbers: [],
843
+ timeDifference: this.calculateTimeDifference(previousSnapshot.timestamp),
844
+ campaignBreakdown: []
845
+ };
846
+ }
847
 
848
+ const newOrdersTotal = newOrders.reduce((sum, order) => sum + parseFloat(order.totalPriceSet.shopMoney.amount), 0);
849
+ const paidOrders = newOrders.filter((o) => o.displayFinancialStatus === 'PAID');
850
+ const newOrdersPaid = paidOrders.reduce((sum, order) => sum + parseFloat(order.totalPriceSet.shopMoney.amount), 0);
851
+ const newOrderNumbers = newOrders.map((order) => order.name);
852
+
853
+ const campaignGroups = new Map();
854
+ newOrders.forEach((order) => {
855
+ const utmContent = this.getUtmContent(order);
856
+ if (!campaignGroups.has(utmContent)) campaignGroups.set(utmContent, []);
857
+ campaignGroups.get(utmContent).push(order);
858
+ });
859
+
860
+ const campaignBreakdown = [];
861
+ campaignGroups.forEach((orders, utmContent) => {
862
+ const totalValue = orders.reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount), 0);
863
+ const paid = orders.filter((o) => o.displayFinancialStatus === 'PAID');
864
+ const paidValue = paid.reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount), 0);
865
+
866
+ campaignBreakdown.push({
867
+ utmContent,
868
+ newOrders: orders.length,
869
+ totalValue,
870
+ paidOrders: paid.length,
871
+ paidValue
872
+ });
873
+ });
874
+
875
+ return {
876
+ newOrderCount: newOrders.length,
877
+ newOrdersTotal,
878
+ newOrdersPaid,
879
+ newOrdersPaidCount: paidOrders.length,
880
+ newOrderNumbers,
881
+ timeDifference: this.calculateTimeDifference(previousSnapshot.timestamp),
882
+ campaignBreakdown
883
+ };
884
+ },
885
+
886
+ createSnapshot(orders) {
887
+ return {
888
+ timestamp: new Date().toISOString(),
889
+ orderIds: new Set(orders.map(o => o.id))
890
+ };
891
+ },
892
+
893
+ resetSnapshot() {
894
+ this.snapshot = null;
895
+ this.newOrdersReport = null;
896
+ this.isFirstLoad = true;
897
+ this.hasLoadedData = false;
898
+ this.dayChanged = false;
899
+ this.utmData = [];
900
+ this.totalOrders = 0;
901
+ this.lastUpdate = '';
902
+ this.checkInitialView();
903
+ this.dom.summarySection.classList.add('hidden');
904
+ this.dom.newOrdersSection.classList.add('hidden');
905
+ this.dom.firstLoadSection.classList.add('hidden');
906
+ this.dom.lastUpdateInfo.classList.add('hidden');
907
+ this.dom.exportBtn.disabled = true;
908
+ },
909
+
910
+ calculateTimeDifference(snapshotTime) {
911
+ const now = new Date();
912
+ const then = new Date(snapshotTime);
913
+ const diffMs = now.getTime() - then.getTime();
914
+ const diffMinutes = Math.floor(diffMs / (1000 * 60));
915
+
916
+ if (diffMinutes < 1) return 'menos de 1 minuto';
917
+ const hours = Math.floor(diffMinutes / 60);
918
+ const minutes = diffMinutes % 60;
919
+ return hours === 0 ? `${minutes}m` : `${hours}h e ${minutes}m`;
920
+ },
921
+
922
+ getUtmContent(order) {
923
+ if (!order.customAttributes) return 'Sem UTM Content';
924
+ const attr = order.customAttributes.find(a => a.key === 'utm_content');
925
+ return (attr && attr.value) ? attr.value.trim() : 'Sem UTM Content';
926
+ },
927
+
928
+ extractLastParameter(utmContent) {
929
+ if (utmContent === 'Sem UTM Content') return 'N/A';
930
+ const matches = utmContent.match(/\[([^\]]+)\]/g);
931
+ if (!matches || matches.length === 0) return utmContent;
932
+ return matches[matches.length - 1].replace(/[\[\]]/g, '').trim();
933
+ },
934
+
935
+ processOrders(orders) {
936
+ const utmGroups = new Map();
937
+ orders.forEach(order => {
938
+ const utm = this.getUtmContent(order);
939
+ if (!utmGroups.has(utm)) utmGroups.set(utm, []);
940
+ utmGroups.get(utm).push(order);
941
+ });
942
+
943
+ this.utmData = [];
944
+ utmGroups.forEach((groupOrders, utmContent) => {
945
+ const totalPedidos = groupOrders.length;
946
+ const pedidosPagos = groupOrders.filter(o => o.displayFinancialStatus === 'PAID').length;
947
+ const pedidosPendentes = totalPedidos - pedidosPagos;
948
+
949
+ const uniqueEmails = new Set(groupOrders.map(o => o.customer?.email || o.id));
950
+ const clientesUnicos = uniqueEmails.size;
951
+
952
+ const totalVendas = groupOrders.reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount), 0);
953
+ const vendasPagas = groupOrders
954
+ .filter(o => o.displayFinancialStatus === 'PAID')
955
+ .reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount), 0);
956
+
957
+ const taxaPagamento = totalPedidos > 0 ? (pedidosPagos / totalPedidos) * 100 : 0;
958
+
959
+ this.utmData.push({
960
+ utmContent,
961
+ totalPedidos,
962
+ pedidosPagos,
963
+ pedidosPendentes,
964
+ clientesUnicos,
965
+ totalVendas,
966
+ vendasPagas,
967
+ taxaPagamento
968
+ });
969
+ });
970
+ },
971
+
972
+ getPerformanceTone(taxa) {
973
+ if (taxa >= 70) return 'success';
974
+ if (taxa >= 50) return 'warning';
975
+ return 'critical';
976
+ },
977
+
978
+ formatCurrency(val) {
979
+ return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(val);
980
+ },
981
+
982
+ handleSort(column) {
983
+ if (this.sortColumn === column) {
984
+ this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
985
+ } else {
986
+ this.sortColumn = column;
987
+ this.sortDirection = 'desc';
988
+ }