eubottura commited on
Commit
d00684d
·
verified ·
1 Parent(s): 3ec82c0

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +907 -19
index.html CHANGED
@@ -1,19 +1,907 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 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
+ * {
47
+ box-sizing: border-box;
48
+ margin: 0;
49
+ padding: 0;
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>