Mercado Livre - Calculadora de Margem de Lucro

Mostra a margem de lucro em cada venda com detalhamento em pop-up

Size

20.8 KB

Version

1.0.1

Created

Feb 17, 2026

Updated

about 1 month ago

1// ==UserScript==
2// @name		Mercado Livre - Calculadora de Margem de Lucro
3// @description		Mostra a margem de lucro em cada venda com detalhamento em pop-up
4// @version		1.0.1
5// @match		https://www.mercadolivre.com.br/*
6// @match		https://www.mercadolivre.com/*
7// @icon		https://robomonkey.io/favicon.ico
8// ==/UserScript==
9(function() {
10    'use strict';
11
12    console.log('Calculadora de Margem de Lucro - Mercado Livre iniciada');
13
14    // Configurações padrão de custos (podem ser ajustadas pelo usuário)
15    const DEFAULT_COSTS = {
16        comissaoML: 16, // % comissão Mercado Livre
17        tarifaFrete: 0, // R$ tarifa fixa de frete
18        custoEmbalagem: 2, // R$ custo de embalagem
19        custoOperacional: 3, // R$ custo operacional por venda
20        margemDesejada: 30 // % margem de lucro desejada
21    };
22
23    // Função para adicionar estilos CSS
24    function addStyles() {
25        const styles = `
26            .profit-margin-badge {
27                display: inline-flex;
28                align-items: center;
29                padding: 4px 8px;
30                border-radius: 4px;
31                font-size: 12px;
32                font-weight: 600;
33                margin-left: 8px;
34                cursor: pointer;
35                transition: all 0.2s ease;
36            }
37            
38            .profit-margin-badge:hover {
39                transform: scale(1.05);
40                box-shadow: 0 2px 8px rgba(0,0,0,0.15);
41            }
42            
43            .profit-positive {
44                background-color: #e8f5e9;
45                color: #2e7d32;
46                border: 1px solid #4caf50;
47            }
48            
49            .profit-negative {
50                background-color: #ffebee;
51                color: #c62828;
52                border: 1px solid #f44336;
53            }
54            
55            .profit-neutral {
56                background-color: #fff3e0;
57                color: #e65100;
58                border: 1px solid #ff9800;
59            }
60            
61            .profit-popup {
62                position: fixed;
63                top: 50%;
64                left: 50%;
65                transform: translate(-50%, -50%);
66                background: white;
67                border-radius: 12px;
68                box-shadow: 0 8px 32px rgba(0,0,0,0.2);
69                padding: 24px;
70                z-index: 10000;
71                min-width: 400px;
72                max-width: 500px;
73                animation: slideIn 0.3s ease;
74            }
75            
76            @keyframes slideIn {
77                from {
78                    opacity: 0;
79                    transform: translate(-50%, -45%);
80                }
81                to {
82                    opacity: 1;
83                    transform: translate(-50%, -50%);
84                }
85            }
86            
87            .profit-popup-overlay {
88                position: fixed;
89                top: 0;
90                left: 0;
91                right: 0;
92                bottom: 0;
93                background: rgba(0,0,0,0.5);
94                z-index: 9999;
95                animation: fadeIn 0.3s ease;
96            }
97            
98            @keyframes fadeIn {
99                from { opacity: 0; }
100                to { opacity: 1; }
101            }
102            
103            .profit-popup-header {
104                display: flex;
105                justify-content: space-between;
106                align-items: center;
107                margin-bottom: 20px;
108                padding-bottom: 12px;
109                border-bottom: 2px solid #e0e0e0;
110            }
111            
112            .profit-popup-title {
113                font-size: 20px;
114                font-weight: 700;
115                color: #333;
116            }
117            
118            .profit-popup-close {
119                background: none;
120                border: none;
121                font-size: 24px;
122                cursor: pointer;
123                color: #666;
124                padding: 0;
125                width: 32px;
126                height: 32px;
127                display: flex;
128                align-items: center;
129                justify-content: center;
130                border-radius: 50%;
131                transition: all 0.2s ease;
132            }
133            
134            .profit-popup-close:hover {
135                background-color: #f5f5f5;
136                color: #333;
137            }
138            
139            .profit-detail-row {
140                display: flex;
141                justify-content: space-between;
142                padding: 10px 0;
143                border-bottom: 1px solid #f0f0f0;
144            }
145            
146            .profit-detail-label {
147                color: #666;
148                font-size: 14px;
149            }
150            
151            .profit-detail-value {
152                font-weight: 600;
153                font-size: 14px;
154                color: #333;
155            }
156            
157            .profit-detail-total {
158                margin-top: 16px;
159                padding-top: 16px;
160                border-top: 2px solid #e0e0e0;
161                display: flex;
162                justify-content: space-between;
163                align-items: center;
164            }
165            
166            .profit-detail-total-label {
167                font-size: 16px;
168                font-weight: 700;
169                color: #333;
170            }
171            
172            .profit-detail-total-value {
173                font-size: 20px;
174                font-weight: 700;
175            }
176            
177            .profit-settings-btn {
178                margin-top: 16px;
179                width: 100%;
180                padding: 10px;
181                background-color: #3483fa;
182                color: white;
183                border: none;
184                border-radius: 6px;
185                font-size: 14px;
186                font-weight: 600;
187                cursor: pointer;
188                transition: background-color 0.2s ease;
189            }
190            
191            .profit-settings-btn:hover {
192                background-color: #2968c8;
193            }
194        `;
195        
196        TM_addStyle(styles);
197    }
198
199    // Função para calcular margem de lucro
200    async function calculateProfit(salePrice, costPrice = null) {
201        const costs = await getCosts();
202        
203        // Se não tiver custo, estima baseado em 50% do preço de venda
204        const estimatedCost = costPrice || (salePrice * 0.5);
205        
206        const comissao = salePrice * (costs.comissaoML / 100);
207        const totalCosts = estimatedCost + comissao + costs.tarifaFrete + costs.custoEmbalagem + costs.custoOperacional;
208        const profit = salePrice - totalCosts;
209        const profitMargin = (profit / salePrice) * 100;
210        
211        return {
212            salePrice,
213            costPrice: estimatedCost,
214            comissao,
215            tarifaFrete: costs.tarifaFrete,
216            custoEmbalagem: costs.custoEmbalagem,
217            custoOperacional: costs.custoOperacional,
218            totalCosts,
219            profit,
220            profitMargin,
221            margemDesejada: costs.margemDesejada
222        };
223    }
224
225    // Função para obter custos salvos
226    async function getCosts() {
227        const savedCosts = await GM.getValue('profitCalculatorCosts', null);
228        return savedCosts || DEFAULT_COSTS;
229    }
230
231    // Função para salvar custos
232    async function saveCosts(costs) {
233        await GM.setValue('profitCalculatorCosts', costs);
234    }
235
236    // Função para formatar moeda
237    function formatCurrency(value) {
238        return new Intl.NumberFormat('pt-BR', {
239            style: 'currency',
240            currency: 'BRL'
241        }).format(value);
242    }
243
244    // Função para criar badge de margem de lucro
245    function createProfitBadge(profitData) {
246        const badge = document.createElement('span');
247        badge.className = 'profit-margin-badge';
248        
249        let badgeClass = 'profit-neutral';
250        if (profitData.profitMargin >= profitData.margemDesejada) {
251            badgeClass = 'profit-positive';
252        } else if (profitData.profitMargin < 10) {
253            badgeClass = 'profit-negative';
254        }
255        
256        badge.classList.add(badgeClass);
257        badge.textContent = `${profitData.profitMargin.toFixed(1)}% lucro`;
258        badge.title = 'Clique para ver detalhes';
259        
260        badge.addEventListener('click', (e) => {
261            e.stopPropagation();
262            showProfitPopup(profitData);
263        });
264        
265        return badge;
266    }
267
268    // Função para mostrar popup detalhado
269    function showProfitPopup(profitData) {
270        // Remove popup existente se houver
271        const existingPopup = document.querySelector('.profit-popup-overlay');
272        if (existingPopup) {
273            existingPopup.remove();
274        }
275        
276        const overlay = document.createElement('div');
277        overlay.className = 'profit-popup-overlay';
278        
279        const popup = document.createElement('div');
280        popup.className = 'profit-popup';
281        
282        const profitColor = profitData.profit >= 0 ? '#2e7d32' : '#c62828';
283        
284        popup.innerHTML = `
285            <div class="profit-popup-header">
286                <div class="profit-popup-title">💰 Detalhamento da Venda</div>
287                <button class="profit-popup-close">×</button>
288            </div>
289            
290            <div class="profit-detail-row">
291                <span class="profit-detail-label">Preço de Venda</span>
292                <span class="profit-detail-value">${formatCurrency(profitData.salePrice)}</span>
293            </div>
294            
295            <div class="profit-detail-row">
296                <span class="profit-detail-label">Custo do Produto</span>
297                <span class="profit-detail-value" style="color: #c62828;">- ${formatCurrency(profitData.costPrice)}</span>
298            </div>
299            
300            <div class="profit-detail-row">
301                <span class="profit-detail-label">Comissão ML (${(profitData.comissao / profitData.salePrice * 100).toFixed(1)}%)</span>
302                <span class="profit-detail-value" style="color: #c62828;">- ${formatCurrency(profitData.comissao)}</span>
303            </div>
304            
305            <div class="profit-detail-row">
306                <span class="profit-detail-label">Tarifa de Frete</span>
307                <span class="profit-detail-value" style="color: #c62828;">- ${formatCurrency(profitData.tarifaFrete)}</span>
308            </div>
309            
310            <div class="profit-detail-row">
311                <span class="profit-detail-label">Custo de Embalagem</span>
312                <span class="profit-detail-value" style="color: #c62828;">- ${formatCurrency(profitData.custoEmbalagem)}</span>
313            </div>
314            
315            <div class="profit-detail-row">
316                <span class="profit-detail-label">Custo Operacional</span>
317                <span class="profit-detail-value" style="color: #c62828;">- ${formatCurrency(profitData.custoOperacional)}</span>
318            </div>
319            
320            <div class="profit-detail-total">
321                <div>
322                    <div class="profit-detail-total-label">Lucro Líquido</div>
323                    <div style="font-size: 12px; color: #666; margin-top: 4px;">
324                        Margem: ${profitData.profitMargin.toFixed(1)}%
325                    </div>
326                </div>
327                <div class="profit-detail-total-value" style="color: ${profitColor};">
328                    ${formatCurrency(profitData.profit)}
329                </div>
330            </div>
331            
332            <button class="profit-settings-btn">⚙️ Configurar Custos</button>
333        `;
334        
335        overlay.appendChild(popup);
336        document.body.appendChild(overlay);
337        
338        // Event listeners
339        const closeBtn = popup.querySelector('.profit-popup-close');
340        closeBtn.addEventListener('click', () => overlay.remove());
341        
342        overlay.addEventListener('click', (e) => {
343            if (e.target === overlay) {
344                overlay.remove();
345            }
346        });
347        
348        const settingsBtn = popup.querySelector('.profit-settings-btn');
349        settingsBtn.addEventListener('click', () => {
350            overlay.remove();
351            showSettingsPopup();
352        });
353        
354        console.log('Popup de lucro exibido:', profitData);
355    }
356
357    // Função para mostrar popup de configurações
358    async function showSettingsPopup() {
359        const costs = await getCosts();
360        
361        const overlay = document.createElement('div');
362        overlay.className = 'profit-popup-overlay';
363        
364        const popup = document.createElement('div');
365        popup.className = 'profit-popup';
366        
367        popup.innerHTML = `
368            <div class="profit-popup-header">
369                <div class="profit-popup-title">⚙️ Configurar Custos</div>
370                <button class="profit-popup-close">×</button>
371            </div>
372            
373            <div style="margin-bottom: 16px;">
374                <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #666;">
375                    Comissão Mercado Livre (%)
376                </label>
377                <input type="number" id="comissaoML" value="${costs.comissaoML}" 
378                    style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
379            </div>
380            
381            <div style="margin-bottom: 16px;">
382                <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #666;">
383                    Tarifa de Frete (R$)
384                </label>
385                <input type="number" id="tarifaFrete" value="${costs.tarifaFrete}" 
386                    style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
387            </div>
388            
389            <div style="margin-bottom: 16px;">
390                <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #666;">
391                    Custo de Embalagem (R$)
392                </label>
393                <input type="number" id="custoEmbalagem" value="${costs.custoEmbalagem}" 
394                    style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
395            </div>
396            
397            <div style="margin-bottom: 16px;">
398                <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #666;">
399                    Custo Operacional (R$)
400                </label>
401                <input type="number" id="custoOperacional" value="${costs.custoOperacional}" 
402                    style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
403            </div>
404            
405            <div style="margin-bottom: 16px;">
406                <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #666;">
407                    Margem de Lucro Desejada (%)
408                </label>
409                <input type="number" id="margemDesejada" value="${costs.margemDesejada}" 
410                    style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
411            </div>
412            
413            <button class="profit-settings-btn" id="saveCostsBtn">💾 Salvar Configurações</button>
414        `;
415        
416        overlay.appendChild(popup);
417        document.body.appendChild(overlay);
418        
419        // Event listeners
420        const closeBtn = popup.querySelector('.profit-popup-close');
421        closeBtn.addEventListener('click', () => overlay.remove());
422        
423        overlay.addEventListener('click', (e) => {
424            if (e.target === overlay) {
425                overlay.remove();
426            }
427        });
428        
429        const saveBtn = popup.querySelector('#saveCostsBtn');
430        saveBtn.addEventListener('click', async () => {
431            const newCosts = {
432                comissaoML: parseFloat(document.getElementById('comissaoML').value),
433                tarifaFrete: parseFloat(document.getElementById('tarifaFrete').value),
434                custoEmbalagem: parseFloat(document.getElementById('custoEmbalagem').value),
435                custoOperacional: parseFloat(document.getElementById('custoOperacional').value),
436                margemDesejada: parseFloat(document.getElementById('margemDesejada').value)
437            };
438            
439            await saveCosts(newCosts);
440            overlay.remove();
441            
442            // Recarrega as badges com novos custos
443            processAllSales();
444            
445            console.log('Custos salvos:', newCosts);
446        });
447    }
448
449    // Função para extrair preço de venda de um elemento
450    function extractSalePrice(saleElement) {
451        // Procura por elementos de preço no card de venda
452        const priceSelectors = [
453            '.andes-money-amount__fraction',
454            '[class*="price"]',
455            '[class*="amount"]',
456            '[data-testid*="price"]'
457        ];
458        
459        for (const selector of priceSelectors) {
460            const priceElement = saleElement.querySelector(selector);
461            if (priceElement) {
462                const priceText = priceElement.textContent.trim();
463                const priceMatch = priceText.match(/[\d.,]+/);
464                if (priceMatch) {
465                    const price = parseFloat(priceMatch[0].replace(/\./g, '').replace(',', '.'));
466                    if (price > 0) {
467                        console.log('Preço encontrado:', price, 'no elemento:', priceElement);
468                        return price;
469                    }
470                }
471            }
472        }
473        
474        return null;
475    }
476
477    // Função para processar uma venda individual
478    async function processSale(saleElement) {
479        // Verifica se já foi processado
480        if (saleElement.dataset.profitProcessed) {
481            return;
482        }
483        
484        const price = extractSalePrice(saleElement);
485        
486        if (price) {
487            const profitData = await calculateProfit(price);
488            const badge = createProfitBadge(profitData);
489            
490            // Procura onde inserir o badge (próximo ao preço ou ao ID da venda)
491            const insertTargets = [
492                saleElement.querySelector('.left-column__pack-id'),
493                saleElement.querySelector('.right-column'),
494                saleElement.querySelector('.identification-row')
495            ];
496            
497            for (const target of insertTargets) {
498                if (target) {
499                    target.appendChild(badge);
500                    saleElement.dataset.profitProcessed = 'true';
501                    console.log('Badge de lucro adicionado para venda de', formatCurrency(price));
502                    break;
503                }
504            }
505        }
506    }
507
508    // Função para processar todas as vendas na página
509    async function processAllSales() {
510        console.log('Processando vendas na página...');
511        
512        // Remove badges existentes para reprocessar
513        document.querySelectorAll('.profit-margin-badge').forEach(badge => badge.remove());
514        document.querySelectorAll('[data-profit-processed]').forEach(el => {
515            delete el.dataset.profitProcessed;
516        });
517        
518        // Seleciona todos os cards de venda
519        const saleCards = document.querySelectorAll('.sc-row, [class*="row-card"], .andes-card');
520        
521        console.log(`Encontrados ${saleCards.length} cards de venda`);
522        
523        for (const card of saleCards) {
524            await processSale(card);
525        }
526    }
527
528    // Função para observar mudanças no DOM
529    function observeDOM() {
530        const observer = new MutationObserver((mutations) => {
531            let shouldProcess = false;
532            
533            for (const mutation of mutations) {
534                if (mutation.addedNodes.length > 0) {
535                    shouldProcess = true;
536                    break;
537                }
538            }
539            
540            if (shouldProcess) {
541                setTimeout(processAllSales, 500);
542            }
543        });
544        
545        observer.observe(document.body, {
546            childList: true,
547            subtree: true
548        });
549        
550        console.log('Observer de DOM iniciado');
551    }
552
553    // Função de inicialização
554    async function init() {
555        console.log('Inicializando Calculadora de Margem de Lucro...');
556        
557        addStyles();
558        
559        // Aguarda o carregamento da página
560        if (document.readyState === 'loading') {
561            document.addEventListener('DOMContentLoaded', () => {
562                setTimeout(processAllSales, 1000);
563                observeDOM();
564            });
565        } else {
566            setTimeout(processAllSales, 1000);
567            observeDOM();
568        }
569        
570        // Reprocessa quando a URL muda (navegação SPA)
571        let lastUrl = location.href;
572        new MutationObserver(() => {
573            const url = location.href;
574            if (url !== lastUrl) {
575                lastUrl = url;
576                setTimeout(processAllSales, 1500);
577            }
578        }).observe(document, { subtree: true, childList: true });
579        
580        console.log('Calculadora de Margem de Lucro inicializada com sucesso!');
581    }
582
583    // Inicia a extensão
584    init();
585
586})();