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

Upload folder using huggingface_hub

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