/** * UTM Analytics Pro - Main Application Logic */ // --- Shopify Configuration --- const SHOPIFY_CONFIG = { storeName: 'sua-loja', // Substitua pelo nome da sua loja (ex: minhaloja em minhaloja.myshopify.com) accessToken: 'seu_access_token_aqui', // Token de Acesso da API Admin do Shopify apiVersion: '2024-01' }; // --- Mock Data & API Simulation --- const UTMS = [ "facebook_feed_summer", "instagram_stories_launch", "google_search_brand", "tiktok_viral_video", "email_newsletter_oct", "influencer_beauty_guru", "youtube_pre_roll", "facebook_retargeting_abandoned" ]; const NAMES = ["Ana Silva", "Bruno Souza", "Carla Dias", "Daniel Rocha", "Eduarda Lima"]; // Generates random orders based on date range function generateMockOrders(startDate, endDate, count = 20) { const orders = []; const start = new Date(startDate).getTime(); const end = new Date(endDate).getTime(); for (let i = 0; i < count; i++) { const randomTime = start + Math.random() * (end - start); const isPaid = Math.random() > 0.3; // 70% paid rate const utm = UTMS[Math.floor(Math.random() * UTMS.length)]; orders.push({ id: `gid://shopify/Order/${Math.floor(Math.random() * 1000000000)}`, name: `#${Math.floor(1000 + Math.random() * 9000)}`, createdAt: new Date(randomTime).toISOString(), displayFinancialStatus: isPaid ? 'PAID' : 'PENDING', totalPriceSet: { shopMoney: { amount: (Math.random() * 500 + 50).toFixed(2) // Random price between 50 and 550 } }, customer: { email: Math.random() > 0.5 ? `cliente${i}@email.com` : null }, customAttributes: [ { key: 'utm_content', value: utm } ] }); } return orders.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } // --- Application State --- const app = { state: { dateRangeOption: 'hoje', customDateRange: null, loading: false, error: null, utmData: [], allOrders: [], snapshot: null, // { timestamp, orderIds } lastUpdate: null, sortColumn: 'totalVendas', sortDirection: 'desc' }, init() { this.cacheDOM(); this.bindEvents(); // Initial load isn't automatic to match the "No data" state, // but let's pre-load some empty states this.updateUI(); }, cacheDOM() { this.dom = { dateSelect: document.getElementById('date-range-select'), customDateContainer: document.getElementById('custom-date-container'), dateStart: document.getElementById('date-start'), dateEnd: document.getElementById('date-end'), refreshBtn: document.getElementById('refresh-btn'), exportBtn: document.getElementById('export-btn'), alertsArea: document.getElementById('alerts-area'), kpiSection: document.getElementById('kpi-section'), tableLoading: document.getElementById('table-loading'), tableEmpty: document.getElementById('table-empty'), dataTable: document.getElementById('data-table'), tableBody: document.getElementById('table-body'), tableFooter: document.getElementById('table-footer'), sortSelect: document.getElementById('sort-select'), // KPIs kpiTotalOrders: document.getElementById('kpi-total-orders'), kpiPaidSales: document.getElementById('kpi-paid-sales'), kpiConversion: document.getElementById('kpi-conversion-rate'), kpiUniqueUtms: document.getElementById('kpi-unique-utms'), }; }, bindEvents() { this.dom.dateSelect.addEventListener('change', (e) => { this.state.dateRangeOption = e.target.value; if (this.state.dateRangeOption === 'customizado') { this.dom.customDateContainer.classList.remove('hidden'); } else { this.dom.customDateContainer.classList.add('hidden'); } }); this.dom.refreshBtn.addEventListener('click', () => this.fetchOrders()); this.dom.exportBtn.addEventListener('click', () => this.exportCSV()); this.dom.sortSelect.addEventListener('change', (e) => { this.state.sortColumn = e.target.value; this.state.sortDirection = 'desc'; this.renderTable(); }); }, // --- Logic & Helpers --- getDateRange() { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); let startDate, endDate = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1); switch (this.state.dateRangeOption) { case 'hoje': startDate = today; break; case 'ontem': startDate = new Date(today.getTime() - 24 * 60 * 60 * 1000); endDate = new Date(today.getTime() - 1); break; case 'ultimos7': startDate = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); break; case 'ultimos30': startDate = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000); break; case 'customizado': if (this.dom.dateStart.value && this.dom.dateEnd.value) { return { startDate: new Date(this.dom.dateStart.value).toISOString(), endDate: new Date(this.dom.dateEnd.value).toISOString() }; } // Fallback if dates not picked yet startDate = today; break; default: startDate = today; } return { startDate: startDate.toISOString(), endDate: endDate.toISOString() }; }, calculateTimeDiff(timestamp) { const now = new Date(); const then = new Date(timestamp); const diffMinutes = Math.floor((now - then) / 60000); if (diffMinutes < 1) return "menos de 1 minuto"; const hours = Math.floor(diffMinutes / 60); const mins = diffMinutes % 60; if (hours === 0) return `${mins}m`; return `${hours}h e ${mins}m`; }, // --- Core Actions --- async fetchShopifyData(startDate, endDate) { const query = ` { orders(first: 250, query: "created_at:>='${startDate}' AND created_at:<='${endDate}'") { edges { node { id name createdAt displayFinancialStatus totalPriceSet { shopMoney { amount currencyCode } } customer { email } customAttributes { key value } } } } }`; const response = await fetch(`https://${SHOPIFY_CONFIG.storeName}.myshopify.com/admin/api/${SHOPIFY_CONFIG.apiVersion}/graphql.json`, { method: 'POST', headers: { 'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken, 'Content-Type': 'application/json' }, body: JSON.stringify({ query }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Shopify API Error: ${response.status} - ${errorText}`); } const data = await response.json(); return data.data.orders.edges.map(edge => edge.node); }, async fetchOrders() { this.state.loading = true; this.state.error = null; this.updateUI(); try { const { startDate, endDate } = this.getDateRange(); let allOrders = []; // Tenta buscar dados reais do Shopify se as credenciais estiverem configuradas if (SHOPIFY_CONFIG.storeName !== 'sua-loja' && SHOPIFY_CONFIG.accessToken !== 'seu_access_token_aqui') { try { console.log("Conectando ao Shopify..."); allOrders = await this.fetchShopifyData(startDate.split('T')[0], endDate.split('T')[0]); } catch (apiError) { console.warn("Falha ao buscar dados da API Shopify, usando dados mockados:", apiError); this.state.error = "Erro na API Shopify: " + apiError.message + ". Exibindo dados simulados."; // Fallback para mock const orderCount = Math.floor(Math.random() * 30) + 10; allOrders = generateMockOrders(startDate, endDate, orderCount); } } else { // Dados Mockados await new Promise(r => setTimeout(r, 1500)); // Simulate Network Delay const orderCount = Math.floor(Math.random() * 30) + 10; allOrders = generateMockOrders(startDate, endDate, orderCount); } // 2. Compare with Snapshot for "New Orders" let newOrdersReport = null; if (this.state.snapshot) { const previousIds = new Set(this.state.snapshot.orderIds); const newOrders = allOrders.filter(o => !previousIds.has(o.id)); if (newOrders.length > 0) { const totalVal = newOrders.reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount), 0); const paidVal = newOrders.filter(o => o.displayFinancialStatus === 'PAID') .reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount), 0); newOrdersReport = { count: newOrders.length, total: totalVal, paid: paidVal, timeDiff: this.calculateTimeDiff(this.state.snapshot.timestamp) }; } else { newOrdersReport = { count: 0, timeDiff: this.calculateTimeDiff(this.state.snapshot.timestamp) }; } } // 3. Update Snapshot this.state.snapshot = { timestamp: new Date().toISOString(), orderIds: allOrders.map(o => o.id) }; this.state.lastUpdate = new Date().toLocaleTimeString('pt-BR', {hour: '2-digit', minute:'2-digit'}); // 4. Process Data this.processOrders(allOrders); this.state.newOrdersReport = newOrdersReport; } catch (err) { console.error(err); this.state.error = "Erro ao carregar dados. Tente novamente."; console.error(err); } finally { this.state.loading = false; this.updateUI(); } }, processOrders(orders) { const utmGroups = new Map(); orders.forEach(order => { // Find UTM Content let utm = 'Sem UTM Content'; const attr = order.customAttributes.find(a => a.key === 'utm_content'); if (attr && attr.value) utm = attr.value.trim(); if (!utmGroups.has(utm)) utmGroups.set(utm, []); utmGroups.get(utm).push(order); }); const processed = []; utmGroups.forEach((group, utmName) => { const total = group.length; const paidCount = group.filter(o => o.displayFinancialStatus === 'PAID').length; const uniqueClients = new Set(group.map(o => o.customer?.email || o.id)).size; const totalSales = group.reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount), 0); const paidSales = group.filter(o => o.displayFinancialStatus === 'PAID') .reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount), 0); const rate = total > 0 ? (paidCount / total) * 100 : 0; processed.push({ utmContent: utmName, totalPedidos: total, pedidosPagos: paidCount, pedidosPendentes: total - paidCount, clientesUnicos: uniqueClients, totalVendas: totalSales, vendasPagas: paidSales, taxaPagamento: rate }); }); this.state.utmData = processed; }, handleSort(column) { if (this.state.sortColumn === column) { this.state.sortDirection = this.state.sortDirection === 'asc' ? 'desc' : 'asc'; } else { this.state.sortColumn = column; this.state.sortDirection = 'desc'; } this.renderTable(); }, getSortedData() { return [...this.state.utmData].sort((a, b) => { let valA = a[this.state.sortColumn]; let valB = b[this.state.sortColumn]; if (typeof valA === 'string') { valA = valA.toLowerCase(); valB = valB.toLowerCase(); } if (valA < valB) return this.state.sortDirection === 'asc' ? -1 : 1; if (valA > valB) return this.state.sortDirection === 'asc' ? 1 : -1; return 0; }); }, calculateTotals() { return this.state.utmData.reduce((acc, curr) => { acc.totalPedidos += curr.totalPedidos; acc.pedidosPagos += curr.pedidosPagos; acc.clientesUnicos += curr.clientesUnicos; acc.totalVendas += curr.totalVendas; acc.vendasPagas += curr.vendasPagas; return acc; }, { totalPedidos: 0, pedidosPagos: 0, clientesUnicos: 0, totalVendas: 0, vendasPagas: 0, utmContent: 'TOTAL GERAL' }); }, // --- UI Rendering --- updateUI() { // Loading State if (this.state.loading) { this.dom.tableLoading.classList.remove('hidden'); this.dom.dataTable.classList.add('hidden'); this.dom.tableEmpty.classList.add('hidden'); this.dom.kpiSection.classList.add('hidden'); this.dom.alertsArea.innerHTML = ''; this.dom.refreshBtn.disabled = true; this.dom.refreshBtn.classList.add('opacity-75'); return; } this.dom.refreshBtn.disabled = false; this.dom.refreshBtn.classList.remove('opacity-75'); this.dom.tableLoading.classList.add('hidden'); // Error State if (this.state.error) { this.dom.alertsArea.innerHTML = `
${r.count} novos pedidos desde a última atualização (${r.timeDiff} atrás). Total de R$ ${r.total.toLocaleString('pt-BR', {minimumFractionDigits: 2})}.