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})();