import React, { useState, useEffect, useMemo, useRef, Fragment } from 'react'; import { createRoot } from 'react-dom/client'; import { Plus, Trash2, ChevronLeft, ChevronRight, ChevronDown, ChevronUp, TrendingUp, TrendingDown, Wallet, BarChart3, Megaphone, Check, X, Receipt, Zap, Loader2, RefreshCw, PieChart, Users, Percent, Sparkles, Pencil, Package, Truck, Award, Calendar, Save, Download, Upload, Cloud, CloudOff, LogOut, LockKeyhole, Database, Search, CircleDollarSign, Copy, ClipboardList, Store, ShoppingCart, FileText, Link2, ShieldCheck, KeyRound, ExternalLink, Printer, } from 'https://esm.sh/lucide-react@0.468.0?external=react'; /* ---------- helpers ---------- */ const brl = (n) => new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(n) || 0); const brlMsg = (n) => brl(n).replace(/\s/g, ''); const brlMsgCurto = (n) => { const v = Number(n) || 0; const opts = Number.isInteger(v) ? { minimumFractionDigits: 0, maximumFractionDigits: 0 } : { minimumFractionDigits: 2, maximumFractionDigits: 2 }; return `R$${new Intl.NumberFormat('pt-BR', opts).format(v)}`; }; const num = (n, d = 1) => new Intl.NumberFormat('pt-BR', { minimumFractionDigits: d, maximumFractionDigits: d }).format(Number(n) || 0); const pct = (n) => new Intl.NumberFormat('pt-BR', { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(Number(n) || 0); const parseBRL = (s) => { if (typeof s === 'number') return s; if (!s) return 0; let t = String(s).trim().replace(/[^\d.,-]/g, ''); if (t.includes('.') && t.includes(',')) t = t.replace(/\./g, '').replace(',', '.'); else if (t.includes(',')) t = t.replace(',', '.'); const v = parseFloat(t); return isNaN(v) ? 0 : v; }; const MESES = ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro']; const monthLabel = (ym) => { const [y, m] = ym.split('-').map(Number); return `${MESES[m - 1]} ${y}`; }; const shiftMonth = (ym, d) => { let [y, m] = ym.split('-').map(Number); m += d; while (m < 1) { m += 12; y--; } while (m > 12) { m -= 12; y++; } return `${y}-${String(m).padStart(2, '0')}`; }; const lastDay = (ym) => { const [y, m] = ym.split('-').map(Number); return String(new Date(y, m, 0).getDate()).padStart(2, '0'); }; const todayISO = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; const yesterdayISO = () => { const d = new Date(); d.setDate(d.getDate() - 1); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; const currentMonth = () => todayISO().slice(0, 7); const dISO = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; const brDate = (iso) => iso ? `${iso.slice(8, 10)}/${iso.slice(5, 7)}/${iso.slice(0, 4)}` : ''; const uid = () => Math.random().toString(36).slice(2, 10); const norm = (s) => (s || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/\s+/g, ' ').trim(); const mesesEntre = (from, to) => { const out = []; let cur = from.slice(0, 7); const end = to.slice(0, 7); let guard = 0; while (cur <= end && guard < 240) { out.push(cur); cur = shiftMonth(cur, 1); guard++; } return out; }; const diasEntre = (from, to) => { const out = []; if (!from || !to) return out; const [fy,fm,fd] = from.split('-').map(Number); const [ty,tm,td] = to.split('-').map(Number); let d = new Date(fy, fm - 1, fd); const end = new Date(ty, tm - 1, td); let guard = 0; while (d <= end && guard < 370) { out.push(dISO(d)); d.setDate(d.getDate() + 1); guard++; } return out; }; const LOJAS_SUGERIDAS = ['Essential Estofados', 'Luna Comfort', 'Empório Sofás']; // Loja principal usada como padrão para dados antigos (o painel nasceu só da Essential). const LOJA_PADRAO = { id: 'essential', nome: 'Essential Estofados' }; const LOJAS_SEED = [ LOJA_PADRAO, { id: 'emporio', nome: 'Empório Sofás' }, { id: 'luna', nome: 'Luna Comfort' }, ]; // Regras operacionais por loja. A Essential mantém exatamente a lógica anterior. const TAXAS_LOJA = { debito: 1.09, 1: 3.49, 2: 4.49, 3: 5.29, 4: 5.99, 5: 6.49, 6: 7.49, 7: 8.49, 8: 8.99, 9: 9.99, 10: 10.49, 11: 10.99, 12: 10.99, }; const TAXAS_VENDEDOR = { debito: 2.69, 1: 2.69, 2: 4.59, 3: 5.71, 4: 6.83, 5: 7.95, 6: 9.07, 7: 10.19, 8: 11.31, 9: 12.43, 10: 13.55, 11: 14.67, 12: 15.79, }; const EMPORIO_MODELOS = [ { id: 'emp-new-york-180', nome: 'New York 1.80m', custo: 680, comissao: 85, fornecedor: 'Fornecedor Empório' }, { id: 'emp-new-york-2m', nome: 'New York 2m', custo: 680, comissao: 85, precoBaseNatan: 1000, fornecedor: 'Fornecedor Empório' }, // New York Premium: custo do fornecedor = R$750 e comissão do Natan = R$200 fixa. // A taxa da maquininha é calculada e descontada separadamente no financeiro da loja. { id: 'emp-new-york-premium', nome: 'New York Premium', custo: 750, comissao: 200, comissaoFixa: true, fornecedor: 'Fornecedor Empório' }, { id: 'emp-texas-170', nome: 'Texas 1.70m', custo: 550, comissao: 50, fornecedor: 'Fornecedor Empório' }, { id: 'emp-texas-2m', nome: 'Texas 2m', custo: 550, comissao: 100, fornecedor: 'Fornecedor Empório' }, ]; const LUNA_MODELOS = [ { id: 'luna-cama-casal-7', nome: 'Cama Casal 7cm', custo: 200, fornecedor: 'FG Colchões' }, { id: 'luna-cama-casal-12', nome: 'Cama Casal 12cm', custo: 300, fornecedor: 'FG Colchões' }, { id: 'luna-cama-solteiro-7', nome: 'Cama Solteiro 7cm', custo: 190, fornecedor: 'FG Colchões' }, { id: 'luna-cama-solteiro-aux-7', nome: 'Cama Solteiro com auxiliar 7cm', custo: 360, fornecedor: 'FG Colchões' }, { id: 'luna-base-casal', nome: 'Base Casal', custo: 170, fornecedor: 'FG Colchões' }, { id: 'luna-base-solteiro', nome: 'Base Solteiro', custo: 140, fornecedor: 'FG Colchões' }, { id: 'luna-base-casal-bau', nome: 'Base Casal Baú', custo: 400, fornecedor: 'FG Colchões' }, { id: 'luna-puff-casal', nome: 'Puff Casal', custo: 160, fornecedor: 'FG Colchões' }, { id: 'luna-cabeceira', nome: 'Cabeceira', custo: 110, fornecedor: 'FG Colchões' }, { id: 'luna-colchao-bonnel-solteiro', nome: 'Colchão Bonnel Solteiro', custo: 290, fornecedor: 'FG Colchões' }, { id: 'luna-colchao-pillow-casal', nome: 'Colchão Bonnel/Pillowtop Casal', custo: 400, fornecedor: 'FG Colchões' }, { id: 'luna-base-bau-casal-bip', nome: 'Base box baú casal bipartido', custo: 700, fornecedor: 'FG Colchões' }, { id: 'luna-base-bau-solteiro', nome: 'Base box baú solteiro', custo: 369, fornecedor: 'FG Colchões' }, { id: 'luna-base-bau-bip', nome: 'Base box baú bipartido', custo: 300, fornecedor: 'FG Colchões' }, { id: 'luna-cama-box-casal-avulsa', nome: 'Cama Box casal (base+colchão)', custo: 570, fornecedor: 'FG Colchões' }, ]; // Camas do fornecedor FG também podem ser vendidas pela Empório Sofás. // Para essas camas: comissão do Natan = R$50 fixa; frete segue a mesma Tabela 2 da Luna; // taxas de cartão do lucro geral seguem a tabela oficial já usada pela Luna/Empório. // Sofás da Empório continuam com as regras atuais, sem alteração. const EMPORIO_CAMA_FG_MODELOS = [ { id: 'emp-fg-cama-casal-7', nome: 'Cama Casal 7cm', custo: 200, comissao: 50, fornecedor: 'FG Colchões', tipoProduto: 'cama_fg' }, { id: 'emp-fg-cama-casal-12', nome: 'Cama Casal 12cm', custo: 300, comissao: 50, fornecedor: 'FG Colchões', tipoProduto: 'cama_fg' }, { id: 'emp-fg-cama-solteiro-7', nome: 'Cama Solteiro 7cm', custo: 190, comissao: 50, fornecedor: 'FG Colchões', tipoProduto: 'cama_fg' }, { id: 'emp-fg-cama-solteiro-aux-7', nome: 'Cama Solteiro com auxiliar 7cm', custo: 360, comissao: 50, fornecedor: 'FG Colchões', tipoProduto: 'cama_fg' }, // O custo da Cama Casal 14cm ainda não foi informado. O painel reconhece a ficha, // aplica frete/comissão corretamente e deixa o custo para conferência manual. { id: 'emp-fg-cama-casal-14', nome: 'Cama Casal 14cm', custo: 0, comissao: 50, fornecedor: 'FG Colchões', tipoProduto: 'cama_fg', custoPendente: true }, { id: 'emp-fg-cama-outro', nome: 'Cama FG (outro modelo)', custo: 0, comissao: 50, fornecedor: 'FG Colchões', tipoProduto: 'cama_fg', custoPendente: true }, ]; const FRETE_LUNA_T1 = [ [10,15,100],[20,25,100],[26,29,100],[30,39,100],[40,49,130],[50,55,110],[56,58,120],[60,62,110], [63,68,120],[70,73,100],[74,76,100],[77,79,120],[80,85,100],[86,89,100],[90,93,100],[94,99,100], ]; const FRETE_LUNA_T2 = [ [10,15,90],[20,25,80],[26,29,90],[30,39,80],[40,49,110],[50,55,80],[56,58,120],[60,62,110], [63,68,120],[70,73,80],[74,76,90],[77,79,120],[80,85,80],[86,89,100],[90,93,90],[94,99,100], ]; const HISTORICO_AGOSTO = [ ...[ [1,974.77,1821.95,910],[4,380,0,0],[5,200,0,0],[7,1467.93,0,0],[8,622.16,1451.88,406],[10,600,315.58,0], [11,200,0,0],[12,645.29,0,0],[13,600,481.71,0],[14,323,0,0],[15,466.86,0,0],[16,0,689.71,482], [17,740.41,0,0],[18,100,0,0],[19,420,0,0],[20,200,0,0],[22,1253.08,1092,755], ].map(([dia,lucroBase,ads,vendedor]) => ({ id:`hist-emporio-2026-08-${String(dia).padStart(2,'0')}`, lojaId:'emporio', data:`2026-08-${String(dia).padStart(2,'0')}`, lucroBase, ads, vendedor, resultado: lucroBase-ads-vendedor, origem:'Controle_Emporio_Sofas_Final_ATUALIZADO.xlsx' })), ...[ [1,342.65,0,0],[5,218,0,0],[6,119,0,0],[7,439.47,0,0],[8,386,0,0],[10,228,0,0],[11,160,0,0],[12,505,0,0], [13,89,0,0],[14,188,0,0],[15,0,1897,0],[18,189.17,0,0],[19,79,0,0],[20,119,0,0],[21,436.46,0,0],[22,416.46,0,0], ].map(([dia,lucroBase,ads,vendedor]) => ({ id:`hist-luna-2026-08-${String(dia).padStart(2,'0')}`, lojaId:'luna', data:`2026-08-${String(dia).padStart(2,'0')}`, lucroBase, ads, vendedor, resultado: lucroBase-ads-vendedor, origem:'Controle_Luna.Comfort_Final.xlsx' })), ]; const PAGAMENTO_PESSOA = { emporio: { vendedor: 'Natan', freteiro: 'Rogério' }, luna: { vendedor: 'Douglas', freteiro: 'Entregadores Luna' }, }; const LUNA_ENTREGADORES = ['Rogério', 'Tarcísio', 'Goldstar', 'FG']; const LUNA_ENTREGADORES_FRETE = ['Rogério', 'Tarcísio', 'Goldstar']; const LUNA_RECEBEDORES = [ { id: 'luna', nome: 'Luna Comfort' }, { id: 'fg', nome: 'FG' }, ]; const taxaPagamentoPct = (forma, parcelas, tabela = TAXAS_LOJA) => { if (forma === 'debito') return Number(tabela.debito) || 0; if (forma !== 'credito') return 0; const p = Math.min(12, Math.max(1, Number(parcelas) || 1)); return Number(tabela[p]) || 0; }; // A calculadora dos vendedores cobra a taxa arredondando o valor da taxa para cima // no centavo (ex.: R$1.134,16 × 6,83% = R$77,463... => R$77,47). // Isso reproduz os valores líquidos mostrados no app da maquininha. const valorTaxaVendedor = (bruto, pct) => { const taxaBruta = (Number(bruto) || 0) * (Number(pct) || 0) / 100; return Math.ceil((taxaBruta - 1e-9) * 100) / 100; }; // Empório Sofás — regra do Natan: // O valor cobrado do cliente no cartão já contém o acréscimo do parcelamento. // Para descobrir o extra real do Natan, primeiro retiramos a taxa da TABELA DO VENDEDOR // daquele parcelamento e só então comparamos o líquido restante com o preço normal do modelo. // Natan recebe: comissão-base + 50% do que SOBRAR acima do preço normal depois da taxa. // Ex.: New York 2m, R$1.182,73 em 10x -> líquido da tabela do vendedor ~R$1.022,47; // base normal R$1.000,00 -> extra líquido ~R$22,47 -> metade ~R$11,24 + R$85 = ~R$96,24. // O lucro/faturamento geral da loja continua usando exclusivamente TAXAS_LOJA (primeiro print). const calculoComissaoNatan = (bruto, forma, parcelas, comissaoBase = 0, valorNormalPagamento = 0) => { const valor = Number(bruto) || 0; const base = Number(comissaoBase) || 0; const normal = Number(valorNormalPagamento) || 0; const p = Math.min(12, Math.max(1, Number(parcelas) || 1)); const taxaAtual = forma === 'credito' ? taxaPagamentoPct('credito', p, TAXAS_VENDEDOR) : (forma === 'debito' ? taxaPagamentoPct('debito', 1, TAXAS_VENDEDOR) : 0); const taxaValor = taxaAtual > 0 ? valorTaxaVendedor(valor, taxaAtual) : 0; const valorLiquidoVendedor = Math.max(0, valor - taxaValor); const parcelado = forma === 'credito' && p > 1; const acrescimoParcelamento = parcelado && normal > 0 ? Math.max(0, valorLiquidoVendedor - normal) : 0; const extraNatan = acrescimoParcelamento / 2; const total = base + extraNatan; return { total, base, extraNatan, acrescimoParcelamento, valorNormalPagamento: normal, taxaAtual, taxaValor, valorLiquidoVendedor }; }; const calculoComissaoNatanModelo = (bruto, forma, parcelas, regra, valorNormalPagamento = 0) => { const base = Number(regra?.comissao) || 0; if (!regra?.comissaoFixa) return calculoComissaoNatan(bruto, forma, parcelas, base, valorNormalPagamento); const valor = Number(bruto) || 0; const p = Math.min(12, Math.max(1, Number(parcelas) || 1)); const taxaAtual = forma === 'credito' ? taxaPagamentoPct('credito', p, TAXAS_VENDEDOR) : (forma === 'debito' ? taxaPagamentoPct('debito', 1, TAXAS_VENDEDOR) : 0); const taxaValor = taxaAtual > 0 ? valorTaxaVendedor(valor, taxaAtual) : 0; return { total: base, base, extraNatan: 0, acrescimoParcelamento: 0, valorNormalPagamento: Number(valorNormalPagamento) || 0, taxaAtual, taxaValor, valorLiquidoVendedor: Math.max(0, valor - taxaValor), fixa: true, }; }; const prefixoCep = (cep) => { const d = String(cep || '').replace(/\D/g, ''); return d.length >= 3 ? Number(d.slice(0,3)) : null; }; const freteFaixa = (cep, tabela) => { const p = prefixoCep(cep); if (p === null) return null; const f = tabela.find(([a,b]) => p >= a && p <= b); return f ? f[2] : null; }; const normalizarNomeModelo = (s) => norm(String(s || '').replace(/\*/g,'').replace(/[_–—]/g,' ')); const regraModeloEmporio = (nome) => { const n = normalizarNomeModelo(nome); if (n.includes('new york') && n.includes('premium')) return EMPORIO_MODELOS[2]; if (n.includes('new york') && (n.includes('1.80') || n.includes('1,80') || n.includes('1 80'))) return EMPORIO_MODELOS[0]; if (n.includes('new york') && (n.includes('2m') || n.includes('2 m'))) return EMPORIO_MODELOS[1]; if (n.includes('texas') && (n.includes('1.70') || n.includes('1,70') || n.includes('1 70'))) return EMPORIO_MODELOS[3]; if (n.includes('texas')) return EMPORIO_MODELOS[4]; if (n.includes('cama') || n.includes('conjugada')) { if (n.includes('solteiro') && n.includes('auxiliar') && n.includes('7')) return EMPORIO_CAMA_FG_MODELOS[3]; if (n.includes('solteiro') && n.includes('7')) return EMPORIO_CAMA_FG_MODELOS[2]; if (n.includes('casal') && n.includes('14')) return EMPORIO_CAMA_FG_MODELOS[4]; if (n.includes('casal') && n.includes('12')) return EMPORIO_CAMA_FG_MODELOS[1]; if (n.includes('casal') && n.includes('7')) return EMPORIO_CAMA_FG_MODELOS[0]; return EMPORIO_CAMA_FG_MODELOS[5]; } return null; }; const modeloEhCamaFgEmporio = (m) => Boolean(m && (m.tipoProduto === 'cama_fg' || (norm(m.fornecedor).includes('fg') && /\bcama\b/.test(normalizarNomeModelo(m.nome))))); const regraModeloLuna = (nome) => { const n = normalizarNomeModelo(nome); if (n.includes('cama box') && n.includes('conjugada') && n.includes('solteiro') && n.includes('7')) return LUNA_MODELOS[2]; if (n.includes('conjugada') && n.includes('solteiro') && n.includes('7')) return LUNA_MODELOS[2]; if (n.includes('conjugada') && n.includes('casal') && n.includes('12')) return LUNA_MODELOS[1]; if (n.includes('conjugada') && n.includes('12')) return LUNA_MODELOS[1]; if ((n.includes('cama box') || n.includes('conjugada')) && n.includes('7')) return LUNA_MODELOS[0]; if (n.includes('ortopillow')) return LUNA_MODELOS[10]; if (n.includes('pillowtop') && n.includes('casal')) return LUNA_MODELOS[10]; if (n.includes('bonnel') && n.includes('solteiro')) return LUNA_MODELOS[9]; if (n.includes('base box bau casal bipartido') || (n.includes('base') && n.includes('bau') && n.includes('casal') && n.includes('bipartido'))) return LUNA_MODELOS[11]; if (n.includes('base box bau bipartido') || (n.includes('base') && n.includes('bau') && n.includes('bipartido'))) return LUNA_MODELOS[13]; if (n.includes('base box bau solteiro') || (n.includes('base') && n.includes('bau') && n.includes('solteiro'))) return LUNA_MODELOS[12]; if (n.includes('cama box casal') && (n.includes('base+colchao') || n.includes('base colchao'))) return LUNA_MODELOS[14]; if (n.includes('base casal bau') || (n.includes('base') && n.includes('casal') && n.includes('bau'))) return LUNA_MODELOS[6]; if (n.includes('base solteiro')) return LUNA_MODELOS[5]; if (n.includes('base casal')) return LUNA_MODELOS[4]; if (n.includes('puff') && n.includes('casal')) return LUNA_MODELOS[7]; if (n.includes('cabeceira')) return LUNA_MODELOS[8]; if (n.includes('solteiro') && n.includes('auxiliar') && n.includes('7')) return LUNA_MODELOS[3]; return null; }; const PLATAFORMAS = ['Instagram', 'Facebook', 'WhatsApp', 'Outro']; const ORIGENS = ['Indicação', 'Cliente antigo', 'Passou na loja', 'Outro']; const ADX = '__ad_nao_rastreado__'; const ORG = '__organico__'; const DEFAULT_SEC = ['desempenho', 'ajustes', 'windsor', 'comparativo', 'vendas', 'campanhas']; const MARKETPLACES = ['Magalu', 'Shopee']; const MARKETPLACE_STATUS = [ { value: 'pendente', label: 'Aguardando pagamento' }, { value: 'pago', label: 'Pago' }, { value: 'enviado', label: 'Enviado' }, { value: 'entregue', label: 'Entregue' }, { value: 'cancelado', label: 'Cancelado' }, { value: 'devolvido', label: 'Devolvido / reembolsado' }, ]; const MARKETPLACE_STATUS_VALIDOS = new Set(['pago', 'enviado', 'entregue']); const STATUS_PEDIDO = [ { value: 'pendente', label: 'Pedido recebido' }, { value: 'confirmado', label: 'Confirmado com cliente' }, { value: 'entregue', label: 'Entregue' }, { value: 'cancelado', label: 'Cancelado' }, { value: 'devolvido', label: 'Devolvido' }, ]; const STATUS_INFO = { pendente: { label: 'Pedido recebido', cls: 'bg-amber-50 text-amber-700 border-amber-200' }, confirmado: { label: 'Confirmado', cls: 'bg-sky-50 text-sky-700 border-sky-200' }, entregue: { label: 'Entregue', cls: 'bg-emerald-50 text-emerald-700 border-emerald-200' }, cancelado: { label: 'Cancelado', cls: 'bg-rose-50 text-rose-700 border-rose-200' }, devolvido: { label: 'Devolvido', cls: 'bg-slate-100 text-slate-600 border-slate-200' }, }; const statusVenda = (v) => { const raw = norm(String(v?.status || '')); if (!raw) return 'entregue'; // registros antigos continuam como entregues if (['em_rota', 'em rota'].includes(raw)) return 'confirmado'; if (['cancelado', 'cancelada', 'cancelled', 'canceled'].includes(raw)) return 'cancelado'; if (['devolvido', 'devolvida', 'devolucao', 'devolução'].includes(raw)) return 'devolvido'; if (['confirmado', 'confirmada', 'confirmado com cliente'].includes(raw)) return 'confirmado'; if (['pendente', 'pedido recebido', 'recebido'].includes(raw)) return 'pendente'; if (['entregue', 'entregues'].includes(raw)) return 'entregue'; return raw; }; const vendaEntregue = (v) => statusVenda(v) === 'entregue'; const vendaCancelada = (v) => ['cancelado', 'devolvido'].includes(statusVenda(v)); const vendaPendente = (v) => { if (vendaCancelada(v) || vendaEntregue(v)) return false; return ['pendente', 'confirmado'].includes(statusVenda(v)); }; // Mantém a mesma regra de pendência na contagem e na listagem. A data é // normalizada para evitar que valores vindos do banco com horário/espaços // sejam excluídos mesmo aparecendo corretamente no histórico de pedidos. const dataISO = (value) => { const match = String(value || '').trim().match(/^(\d{4}-\d{2}-\d{2})/); return match ? match[1] : ''; }; const vendaPendenteNoPeriodo = (v, from, to) => { if (!vendaPendente(v)) return false; const prevista = dataISO(v?.dataPrevista); return Boolean(prevista && prevista >= from && prevista <= to); }; const addDaysISO = (iso, days) => { const [y, m, d] = String(iso || todayISO()).split('-').map(Number); const dt = new Date(y, (m || 1) - 1, (d || 1) + days); return dISO(dt); }; const semanaISO = (ref = todayISO()) => { const [y, m, d] = String(ref || todayISO()).split('-').map(Number); const dt = new Date(y, (m || 1) - 1, d || 1); const desloc = (dt.getDay() + 6) % 7; // segunda = início da semana dt.setDate(dt.getDate() - desloc); const from = dISO(dt); dt.setDate(dt.getDate() + 6); return { from, to: dISO(dt) }; }; const brDateShort = (iso) => iso ? `${iso.slice(8, 10)}/${iso.slice(5, 7)}` : ''; const diaSemanaPt = (iso) => { if (!iso) return ''; const [y,m,d] = iso.split('-').map(Number); const nome = new Intl.DateTimeFormat('pt-BR', { weekday:'long' }).format(new Date(y, (m||1)-1, d||1)).replace('-feira',''); return nome ? nome.charAt(0).toUpperCase() + nome.slice(1) : ''; }; /* ---------- armazenamento local + API ---------- */ const KEY = 'cpa_whatsapp_v2'; const OLDKEY = 'cpa_whatsapp_v1'; async function readKey(k) { try { if (typeof window === 'undefined' || !window.localStorage) return null; const raw = window.localStorage.getItem(k); return raw ? JSON.parse(raw) : null; } catch { return null; } } const mergeById = (a, b) => { const m = new Map(); [...(b || []), ...(a || [])].forEach((it) => { if (it && it.id) m.set(it.id, it); }); return [...m.values()]; }; const mergeGastos = (a, b) => { const m = new Map(); [...(b || []), ...(a || [])].forEach((g) => { if (!g || !g.campanhaId) return; const periodo = g.date || g.mes; if (!periodo) return; m.set(`${g.campanhaId}|${periodo}`, g); }); return [...m.values()]; }; // Converte dados antigos (loja como texto na campanha) para lojas com id. // Regra: campanha sem loja definida vai para a loja padrão (Essential Estofados), já que o painel nasceu só dela. function migrarLojas(safe) { const slug = (s) => (s || '').toString().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || uid(); const lojas = Array.isArray(safe.lojas) && safe.lojas.length ? safe.lojas.map((l) => ({ id: l.id || slug(l.nome), nome: l.nome || 'Loja' })) : LOJAS_SEED.map((l) => ({ ...l })); const byId = new Map(lojas.map((l) => [l.id, l])); const byNome = new Map(lojas.map((l) => [norm(l.nome), l])); // As lojas principais precisam existir antes de resolver campanhas/vendas já marcadas com lojaId. LOJAS_SEED.forEach((seed) => { if (!byId.has(seed.id)) { const x={ ...seed }; lojas.push(x); byId.set(x.id, x); byNome.set(norm(x.nome), x); } }); const acharOuCriar = (nome) => { const n = norm(nome); if (!n) return LOJA_PADRAO.id; // Nomes antigos usados durante o desenvolvimento também pertencem à Essential. if (n === 'essenza' || n === 'essenza moveis' || n === 'essential' || n === 'essential estofados') return LOJA_PADRAO.id; if (byNome.has(n)) return byNome.get(n).id; const nova = { id: slug(nome), nome: nome.trim() }; lojas.push(nova); byId.set(nova.id, nova); byNome.set(n, nova); return nova.id; }; const campanhas = (safe.campanhas || []).map((c) => { if (c.lojaId && byId.has(c.lojaId)) return c; const lojaId = c.loja ? acharOuCriar(c.loja) : LOJA_PADRAO.id; return { ...c, lojaId }; }); const campLoja = new Map(campanhas.map((c) => [c.id, c.lojaId])); const vendasBase = (safe.vendas || []).map((v) => { if (v.lojaId && byId.has(v.lojaId)) return v; const lojaId = campLoja.get(v.campanhaId) || LOJA_PADRAO.id; return { ...v, lojaId }; }); // Garante que as três lojas principais existam, mesmo ao migrar um estado intermediário. LOJAS_SEED.forEach((seed) => { if (!byId.has(seed.id)) { lojas.push({ ...seed }); byId.set(seed.id, seed); byNome.set(norm(seed.nome), seed); } }); // Modelos antigos pertencem à Essential; modelos novos podem ser compartilhados explicitamente com lojaId=''. const modelosAntigos = (safe.modelos || []).map((m) => ({ ...m, lojaId: m.lojaId === undefined ? LOJA_PADRAO.id : m.lojaId })); const modelosSeed = [ ...EMPORIO_MODELOS.map((m) => ({ ...m, lojaId:'emporio', preco:0, frete:130 })), ...EMPORIO_CAMA_FG_MODELOS.map((m) => ({ ...m, lojaId:'emporio', preco:0, frete:0 })), ...LUNA_MODELOS.map((m) => ({ ...m, lojaId:'luna', preco:0, frete:0 })), ]; const modelosMesclados = mergeById(modelosAntigos, modelosSeed); // Migração V22: corrige uma única vez o cadastro já salvo do New York Premium. const premiumV22Aplicado = Boolean(safe.config?.migracoes?.newYorkPremiumV22); const premiumPedidosV22Aplicado = Boolean(safe.config?.migracoes?.newYorkPremiumPedidosV22); const modelos = premiumV22Aplicado ? modelosMesclados : modelosMesclados.map((m) => ( m.id === 'emp-new-york-premium' ? { ...m, custo:750, comissao:200, comissaoFixa:true } : m )); // Corrige também pedidos que ainda guardavam exatamente os padrões antigos. // Valores diferentes (ajustes manuais reais) permanecem intocados. const vendas = premiumPedidosV22Aplicado ? vendasBase : vendasBase.map((v) => { if ((v.lojaId || LOJA_PADRAO.id) !== 'emporio' || v.modeloId !== 'emp-new-york-premium') return v; const custoAtual = Number(v.custo) || 0; const manualAtual = v.comissaoVendedorManual; return { ...v, custo: (!custoAtual || Math.abs(custoAtual - 880) < 0.005) ? 750 : v.custo, vendedorDevido: 200, comissaoVendedorManual: manualAtual !== null && manualAtual !== undefined && Math.abs((Number(manualAtual) || 0) - 185) < 0.005 ? 200 : manualAtual, }; }); const config = premiumV22Aplicado && premiumPedidosV22Aplicado ? safe.config : { ...(safe.config || {}), migracoes: { ...(safe.config?.migracoes || {}), newYorkPremiumV22:true, newYorkPremiumPedidosV22:true, }, }; const historico = mergeById(safe.historico || [], HISTORICO_AGOSTO); return { ...safe, lojas, campanhas, vendas, modelos, historico, config, adsManuais: Array.isArray(safe.adsManuais) ? safe.adsManuais : [], pagamentos: Array.isArray(safe.pagamentos) ? safe.pagamentos : [], fgMovimentos: Array.isArray(safe.fgMovimentos) ? safe.fgMovimentos : [], marketplaceProdutos: Array.isArray(safe.marketplaceProdutos) ? safe.marketplaceProdutos : [], marketplacePedidos: Array.isArray(safe.marketplacePedidos) ? safe.marketplacePedidos : [], marketplaceAds: Array.isArray(safe.marketplaceAds) ? safe.marketplaceAds : [], }; } async function loadLocalData() { const v2 = await readKey(KEY); const v1 = await readKey(OLDKEY); if (!v2 && !v1) return null; const a = v2 || {}, b = v1 || {}; return { lojas: mergeById(a.lojas, b.lojas), campanhas: mergeById(a.campanhas, b.campanhas), gastos: mergeGastos(a.gastos, b.gastos), vendas: mergeById(a.vendas, b.vendas), modelos: mergeById(a.modelos, b.modelos), despesas: mergeById(a.despesas, b.despesas), adsManuais: mergeById(a.adsManuais, b.adsManuais), pagamentos: mergeById(a.pagamentos, b.pagamentos), fgMovimentos: mergeById(a.fgMovimentos, b.fgMovimentos), historico: mergeById(a.historico, b.historico), marketplaceProdutos: mergeById(a.marketplaceProdutos, b.marketplaceProdutos), marketplacePedidos: mergeById(a.marketplacePedidos, b.marketplacePedidos), marketplaceAds: mergeById(a.marketplaceAds, b.marketplaceAds), config: a.config || b.config || { impostoAdPct: 0 }, }; } async function saveLocalData(data) { try { if (typeof window === 'undefined' || !window.localStorage) return; window.localStorage.setItem(KEY, JSON.stringify(data)); } catch (e) { console.error('Erro ao salvar cache local:', e); } } async function apiRequest(url, options = {}) { const init = { credentials: 'same-origin', cache: 'no-store', ...options }; if (init.body && !init.headers) init.headers = { 'Content-Type': 'application/json' }; const response = await fetch(url, init); const payload = await response.json().catch(() => ({ error: `Resposta inválida do servidor (HTTP ${response.status}).` })); if (!response.ok) { const error = new Error(payload.error || `Erro HTTP ${response.status}`); error.status = response.status; error.payload = payload; throw error; } return payload; } function App() { const [campanhas, setCampanhas] = useState([]); const [gastos, setGastos] = useState([]); const [vendas, setVendas] = useState([]); const [modelos, setModelos] = useState([]); const [lojas, setLojas] = useState([]); const [despesas, setDespesas] = useState([]); const [adsManuais, setAdsManuais] = useState([]); const [pagamentos, setPagamentos] = useState([]); const [fgMovimentos, setFgMovimentos] = useState([]); const [historico, setHistorico] = useState([]); const [marketplaceProdutos, setMarketplaceProdutos] = useState([]); const [marketplacePedidos, setMarketplacePedidos] = useState([]); const [marketplaceAds, setMarketplaceAds] = useState([]); const [config, setConfig] = useState({ impostoAdPct: 0 }); const [loaded, setLoaded] = useState(false); const [periodo, setPeriodo] = useState({ tipo: 'mes', mesRef: currentMonth(), from: todayISO(), to: todayISO() }); const [lojaFiltro, setLojaFiltro] = useState('todas'); const [aba, setAba] = useState('anuncio'); const [paginaAtiva, setPaginaAtiva] = useState('painel'); // painel | ajustes const [mobileNavOpen, setMobileNavOpen] = useState(false); const [modelosView, setModelosView] = useState('modelos'); const [financeiroView, setFinanceiroView] = useState('comercial'); const [finData, setFinData] = useState(todayISO()); const [finDescricao, setFinDescricao] = useState(''); const [finCategoria, setFinCategoria] = useState('Software / assinaturas'); const [finValor, setFinValor] = useState(''); const [finRecorrencia, setFinRecorrencia] = useState('nao'); const [finLojaId, setFinLojaId] = useState('geral'); const [adManualData, setAdManualData] = useState(todayISO()); const [adManualLoja, setAdManualLoja] = useState('emporio'); const [adManualValor, setAdManualValor] = useState(''); const [adManualObs, setAdManualObs] = useState(''); const [pagData, setPagData] = useState(todayISO()); const [pagLoja, setPagLoja] = useState('emporio'); const [pagTipo, setPagTipo] = useState('vendedor'); const [pagValor, setPagValor] = useState(''); const [pagObs, setPagObs] = useState(''); const [ajustePeriodoAdsInput, setAjustePeriodoAdsInput] = useState(''); const [ajustePeriodoVendedorInput, setAjustePeriodoVendedorInput] = useState(''); const [ajustePeriodoMsg, setAjustePeriodoMsg] = useState(''); const [acertoSemanaRef, setAcertoSemanaRef] = useState(todayISO()); const [acertoSomentePendentes, setAcertoSomentePendentes] = useState(true); const [acertoMsg, setAcertoMsg] = useState(''); const [lunaFechamentoDia, setLunaFechamentoDia] = useState(todayISO()); const [lunaFreteEntregadorFiltro, setLunaFreteEntregadorFiltro] = useState('todos'); const [fgRetiradaData, setFgRetiradaData] = useState(todayISO()); const [fgRetiradaDescricao, setFgRetiradaDescricao] = useState(''); const [fgRetiradaValor, setFgRetiradaValor] = useState(''); const [fgSaldoAnteriorInput, setFgSaldoAnteriorInput] = useState(''); const [pedidoPaste, setPedidoPaste] = useState(''); const [pedidoParseMsg, setPedidoParseMsg] = useState(''); const [vendasBusca, setVendasBusca] = useState(''); const [pedidosDataFiltro, setPedidosDataFiltro] = useState(''); const [pedidosModoData, setPedidosModoData] = useState('pedido'); const [pedidosFiltroRapido, setPedidosFiltroRapido] = useState('hoje'); const [pedidosDataFrom, setPedidosDataFrom] = useState(todayISO()); const [pedidosDataTo, setPedidosDataTo] = useState(todayISO()); const [expandedCamp, setExpandedCamp] = useState(null); const [ordenacao, setOrdenacao] = useState('lucro'); // 'lucro' | 'manual' const dragCampIdx = useRef(null); const dragSecKey = useRef(null); const entregasPendentesRef = useRef(null); const [vData, setVData] = useState(todayISO()); const [vCampanha, setVCampanha] = useState(''); const [vOrigem, setVOrigem] = useState(ORIGENS[0]); const [vModelo, setVModelo] = useState(''); const [vCliente, setVCliente] = useState(''); const [vReceita, setVReceita] = useState(''); const [vCusto, setVCusto] = useState(''); const [vFrete, setVFrete] = useState(''); const [vFreteTabela1, setVFreteTabela1] = useState(''); const [vDataPrevista, setVDataPrevista] = useState(''); const [vStatus, setVStatus] = useState('confirmado'); const [vDataEntrega, setVDataEntrega] = useState(''); const [vFormaPagamento, setVFormaPagamento] = useState('nao_informado'); const [vParcelas, setVParcelas] = useState(1); const [vCep, setVCep] = useState(''); const [vQtdItens, setVQtdItens] = useState(1); const [vLojaId, setVLojaId] = useState(LOJA_PADRAO.id); const [vEntregadorLuna, setVEntregadorLuna] = useState('Tarcísio'); const [vEntregadorEmporioCama, setVEntregadorEmporioCama] = useState('Rogério'); const [vPagamentoRecebidoPorLuna, setVPagamentoRecebidoPorLuna] = useState('luna'); const [entregaFiltro, setEntregaFiltro] = useState('todas'); const [entregaDataFrom, setEntregaDataFrom] = useState(''); const [entregaDataTo, setEntregaDataTo] = useState(''); const [cNome, setCNome] = useState(''); const [cLojaId, setCLojaId] = useState(LOJA_PADRAO.id); const [cPlataforma, setCPlataforma] = useState('Instagram'); const [novaLoja, setNovaLoja] = useState(''); const [lojaMsg, setLojaMsg] = useState(''); const [mNome, setMNome] = useState(''); const [mPreco, setMPreco] = useState(''); const [mCusto, setMCusto] = useState(''); const [mFrete, setMFrete] = useState(''); const [mFornecedor, setMFornecedor] = useState(''); const [mLojaId, setMLojaId] = useState(''); const [editModelo, setEditModelo] = useState(null); const [confirmDel, setConfirmDel] = useState(null); const [mergePending, setMergePending] = useState(null); // { from, to } const [editId, setEditId] = useState(null); const [editDraft, setEditDraft] = useState(null); const [fetchingMeta, setFetchingMeta] = useState(false); const [metaRows, setMetaRows] = useState([]); // [{campanha, gastoTotal, mapId}] const [metaDetail, setMetaDetail] = useState([]); // [{campanha, mes, gasto}] const [metaError, setMetaError] = useState(''); const [metaPaste, setMetaPaste] = useState(''); const [metaMsg, setMetaMsg] = useState(''); const [backupMsg, setBackupMsg] = useState(''); const [authChecked, setAuthChecked] = useState(false); const [authenticated, setAuthenticated] = useState(false); const [loginPassword, setLoginPassword] = useState(''); const [loginError, setLoginError] = useState(''); const [bootError, setBootError] = useState(''); const [syncStatus, setSyncStatus] = useState('carregando'); // carregando | salvando | salvo | erro | conflito const [syncError, setSyncError] = useState(''); const serverRevisionRef = useRef(0); const lastSavedJsonRef = useRef(''); const queuedSaveRef = useRef(null); const saveTimerRef = useRef(null); const saveInFlightRef = useRef(false); const applyLoadedData = (d) => { const safe = d || {}; const migrated = migrarLojas(safe); setLojas(migrated.lojas); setCampanhas(migrated.campanhas); setGastos(migrated.gastos || []); setVendas(migrated.vendas); setModelos(migrated.modelos || []); setDespesas((migrated.despesas || []).map((x) => ({ ...x, lojaId: x.lojaId || 'geral' }))); setAdsManuais(migrated.adsManuais || []); setPagamentos(migrated.pagamentos || []); setFgMovimentos(migrated.fgMovimentos || []); setHistorico(migrated.historico || []); setMarketplaceProdutos(migrated.marketplaceProdutos || []); setMarketplacePedidos(migrated.marketplacePedidos || []); setMarketplaceAds(migrated.marketplaceAds || []); setConfig(migrated.config || { impostoAdPct: 0 }); }; useEffect(() => { let cancelled = false; (async () => { try { const session = await apiRequest('/api/session.php'); if (cancelled) return; setAuthChecked(true); if (!session.authenticated) { setAuthenticated(false); setSyncStatus('erro'); return; } setAuthenticated(true); let local = await loadLocalData(); try { const server = await apiRequest('/api/state.php'); serverRevisionRef.current = Number(server.revision) || 0; let data = server.data; // Primeira migração: se o banco estiver vazio, envia automaticamente os dados já existentes no navegador. if (!data && local) { const migrated = await apiRequest('/api/state.php', { method: 'POST', body: JSON.stringify({ data: local, revision: 0 }), }); serverRevisionRef.current = Number(migrated.revision) || 1; data = local; } const chosen = data || local || { lojas: [], campanhas: [], gastos: [], vendas: [], modelos: [], despesas: [], adsManuais: [], pagamentos: [], fgMovimentos: [], historico: [], marketplaceProdutos: [], marketplacePedidos: [], marketplaceAds: [], config: { impostoAdPct: 0 } }; if (cancelled) return; applyLoadedData(chosen); const json = JSON.stringify(chosen); lastSavedJsonRef.current = json; await saveLocalData(chosen); setSyncStatus('salvo'); setSyncError(''); setLoaded(true); } catch (error) { // Se o banco estiver temporariamente indisponível, mantém o painel utilizável com a cópia local. const chosen = local || { lojas: [], campanhas: [], gastos: [], vendas: [], modelos: [], despesas: [], adsManuais: [], pagamentos: [], fgMovimentos: [], historico: [], marketplaceProdutos: [], marketplacePedidos: [], marketplaceAds: [], config: { impostoAdPct: 0 } }; if (cancelled) return; applyLoadedData(chosen); lastSavedJsonRef.current = JSON.stringify(chosen); setBootError(error.message || 'Não foi possível carregar o banco.'); setSyncStatus('erro'); setSyncError('Banco indisponível; usando a cópia local.'); setLoaded(true); } } catch (error) { if (cancelled) return; setAuthChecked(true); setAuthenticated(false); setLoginError(error.message || 'Não foi possível acessar o servidor.'); setSyncStatus('erro'); } })(); return () => { cancelled = true; }; }, []); const flushServerSave = async () => { if (saveInFlightRef.current || !authenticated) return; const queued = queuedSaveRef.current; if (!queued || queued.json === lastSavedJsonRef.current) return; queuedSaveRef.current = null; saveInFlightRef.current = true; let savedSuccessfully = false; try { const response = await apiRequest('/api/state.php', { method: 'POST', body: JSON.stringify({ data: queued.data, revision: serverRevisionRef.current }), }); serverRevisionRef.current = Number(response.revision) || (serverRevisionRef.current + 1); lastSavedJsonRef.current = queued.json; savedSuccessfully = true; setSyncError(''); if (!queuedSaveRef.current || queuedSaveRef.current.json === queued.json) setSyncStatus('salvo'); } catch (error) { if (error.status === 401) { setAuthenticated(false); setAuthChecked(true); setLoginError('Sua sessão expirou. Entre novamente.'); } else if (error.status === 409) { setSyncStatus('conflito'); setSyncError(error.message || 'Dados alterados em outro dispositivo. Atualize a página.'); } else { setSyncStatus('erro'); setSyncError(error.message || 'Falha ao salvar no banco. A cópia local foi mantida.'); // Mantém a versão mais recente na fila; uma nova alteração fará outra tentativa. if (!queuedSaveRef.current) queuedSaveRef.current = queued; } } finally { saveInFlightRef.current = false; if (savedSuccessfully && queuedSaveRef.current) { window.setTimeout(() => flushServerSave(), 150); } } }; useEffect(() => { if (!loaded) return; const data = { lojas, campanhas, gastos, vendas, modelos, despesas, adsManuais, pagamentos, fgMovimentos, historico, marketplaceProdutos, marketplacePedidos, marketplaceAds, config }; const json = JSON.stringify(data); saveLocalData(data); if (!authenticated || json === lastSavedJsonRef.current) return; queuedSaveRef.current = { data, json }; setSyncStatus('salvando'); setSyncError(''); window.clearTimeout(saveTimerRef.current); saveTimerRef.current = window.setTimeout(() => flushServerSave(), 450); return () => window.clearTimeout(saveTimerRef.current); }, [lojas, campanhas, gastos, vendas, modelos, despesas, adsManuais, pagamentos, fgMovimentos, historico, marketplaceProdutos, marketplacePedidos, marketplaceAds, config, loaded, authenticated]); const doLogin = async (event) => { event.preventDefault(); if (!loginPassword) return; setLoginError(''); try { await apiRequest('/api/login.php', { method: 'POST', body: JSON.stringify({ password: loginPassword }) }); window.location.reload(); } catch (error) { setLoginError(error.message || 'Não foi possível entrar.'); } }; const doLogout = async () => { try { await apiRequest('/api/logout.php', { method: 'POST', body: '{}' }); } catch { /* ignora */ } window.location.reload(); }; /* ---------- período ---------- */ const per = useMemo(() => { const p = periodo; const now = new Date(); if (p.tipo === 'hoje') { const d = todayISO(); return { from: d, to: d, label: `Hoje · ${brDate(d)}` }; } if (p.tipo === 'ontem') { const d = yesterdayISO(); return { from: d, to: d, label: `Ontem · ${brDate(d)}` }; } if (p.tipo === 'amanha') { const d = addDaysISO(todayISO(), 1); return { from: d, to: d, label: `Amanhã · ${brDate(d)}` }; } if (p.tipo === 'mes') return { from: `${p.mesRef}-01`, to: `${p.mesRef}-${lastDay(p.mesRef)}`, label: monthLabel(p.mesRef) }; if (p.tipo === 'mes_passado') { const mr = shiftMonth(currentMonth(), -1); return { from: `${mr}-01`, to: `${mr}-${lastDay(mr)}`, label: monthLabel(mr) }; } if (p.tipo === 'ano') { const y = now.getFullYear(); return { from: `${y}-01-01`, to: `${y}-12-31`, label: `Ano de ${y}` }; } if (p.tipo === 'custom') return { from: p.from, to: p.to, label: `${brDate(p.from)} – ${brDate(p.to)}` }; return { from: `${currentMonth()}-01`, to: `${currentMonth()}-${lastDay(currentMonth())}`, label: monthLabel(currentMonth()) }; }, [periodo]); const mesesPeriodo = useMemo(() => mesesEntre(per.from, per.to), [per]); const mesUnico = per.from.slice(0, 7) === per.to.slice(0, 7) ? per.from.slice(0, 7) : null; const mesRefAtual = periodo.tipo === 'mes' ? periodo.mesRef : currentMonth(); const shiftPeriodo = (delta) => setPeriodo({ ...periodo, tipo: 'mes', mesRef: shiftMonth(mesRefAtual, delta) }); const impPct = Number(config.impostoAdPct) || 0; const comImp = (g) => g * (1 + impPct / 100); const nomeLoja = (id) => lojas.find((l) => l.id === id)?.nome || (id === 'geral' ? 'Geral da empresa' : id || '—'); const modeloPorId = (id) => modelos.find((m) => m.id === id); const lojaFormulario = vCampanha && ![ORG, ADX].includes(vCampanha) ? (campanhas.find((c) => c.id === vCampanha)?.lojaId || vLojaId || LOJA_PADRAO.id) : (vLojaId || (lojaFiltro !== 'todas' ? lojaFiltro : LOJA_PADRAO.id)); const modelosVisiveis = modelos.filter((m) => !m.lojaId || lojaFormulario === 'todas' || m.lojaId === lojaFormulario); const campanhasFormulario = campanhas.filter((c) => lojaFiltro === 'todas' || (c.lojaId || LOJA_PADRAO.id) === lojaFiltro); useEffect(() => { if (lojaFiltro !== 'todas' && lojas.some((l) => l.id === lojaFiltro)) { setVLojaId(lojaFiltro); setCLojaId(lojaFiltro); if (['emporio','luna'].includes(lojaFiltro)) setAdManualLoja(lojaFiltro); setFinLojaId(lojaFiltro); setPagLoja(['emporio','luna'].includes(lojaFiltro) ? lojaFiltro : 'emporio'); } }, [lojaFiltro, lojas]); const taxaCartaoPctVenda = (v, tabela = TAXAS_LOJA) => { const lojaId = v.lojaId || LOJA_PADRAO.id; if (!['emporio','luna'].includes(lojaId)) return 0; // A taxa oficial da loja é congelada no pedido para que alterações futuras da tabela // não mudem o resultado de vendas antigas. A tabela dos vendedores é calculada separadamente. if (tabela === TAXAS_LOJA && Number.isFinite(Number(v.taxaCartaoPct))) return Number(v.taxaCartaoPct) || 0; return taxaPagamentoPct(v.formaPagamento, v.parcelas, tabela); }; const taxaCartaoValorVenda = (v, tabela = TAXAS_LOJA) => { if (tabela === TAXAS_LOJA && Number.isFinite(Number(v.taxaCartaoValor))) return Number(v.taxaCartaoValor) || 0; return (Number(v.receita) || 0) * taxaCartaoPctVenda(v, tabela) / 100; }; const receitaLiquidaVenda = (v) => (Number(v.receita) || 0) - taxaCartaoValorVenda(v); const receitaFinanceiraVenda = (v) => receitaLiquidaVenda(v); // Resultado calculado pelas regras normais da loja, sem qualquer ajuste manual do fechamento FG. const lucroBaseVendaCalculado = (v) => receitaLiquidaVenda(v) - (Number(v.custo) || 0) - (Number(v.frete) || 0); // Quando o usuário corrige manualmente o crédito de uma entrega feita pelo FG, esse valor vira // a referência financeira daquele pedido em todo o painel (Lucro Loja, cards, anúncios e fechamentos). // "Usar sugerido" remove o override e volta para lucroBaseVendaCalculado. const lucroBaseVenda = (v) => { const over = fgMovimentos.find((x) => x.tipo === 'credito_entrega_override' && x.vendaId === v.id); if (over && Number.isFinite(Number(over.valor)) && vendaEntreguePeloFgOperacao(v)) return Math.max(0, Number(over.valor) || 0); return lucroBaseVendaCalculado(v); }; // Tabela específica usada no cálculo do Douglas/Luna. O lucro geral NÃO usa esta tabela. // Na Empório, o Natan NÃO é calculado comparando taxas: usa o acréscimo real sobre o valor normal da venda. const taxaVendedorPctVenda = (v) => taxaCartaoPctVenda(v, TAXAS_VENDEDOR); const taxaVendedorValorVenda = (v) => valorTaxaVendedor(Number(v.receita) || 0, taxaVendedorPctVenda(v)); // Douglas/Luna usa uma conta própria para o acerto: taxa dos vendedores + frete da planilha T1. // Essa base NÃO altera o lucro oficial da empresa, que continua usando TAXAS_LOJA + frete T2 (v.frete). const freteAcertoDouglasVenda = (v) => { if ((v.lojaId || LOJA_PADRAO.id) !== 'luna') return 0; if (Number.isFinite(Number(v.freteTabela1)) && Number(v.freteTabela1) > 0) return Number(v.freteTabela1); const calc = freteLuna(v.cep, FRETE_LUNA_T1, Math.max(1, Number(v.qtdItens) || 1)); return calc === null ? 0 : calc; }; const resultadoBaseVendedorLunaCalculado = (v) => (Number(v.receita) || 0) - taxaVendedorValorVenda(v) - (Number(v.custo) || 0) - freteAcertoDouglasVenda(v); const resultadoBaseVendedorLuna = (v) => { const over = fgMovimentos.find((x) => x.tipo === 'credito_entrega_override' && x.vendaId === v.id); // Em uma entrega Luna feita pelo FG, a correção digitada no Acerto FG passa a ser o número // cheio de referência também para o Acerto Douglas. A comissão paga continua sendo 50%. if (over && Number.isFinite(Number(over.valor)) && vendaEntreguePeloFgLuna(v)) return Math.max(0, Number(over.valor) || 0); return resultadoBaseVendedorLunaCalculado(v); }; const entregadorLunaVenda = (v) => (v.lojaId || LOJA_PADRAO.id) === 'luna' ? (v.entregadorLuna || 'Tarcísio') : ''; const pagamentoRecebidoPorLunaVenda = (v) => { if ((v.lojaId || LOJA_PADRAO.id) !== 'luna') return 'loja'; if (v.pagamentoRecebidoPor === 'fg' || v.pagamentoRecebidoPor === 'luna') return v.pagamentoRecebidoPor; return entregadorLunaVenda(v) === 'FG' ? 'fg' : 'luna'; }; // Luna Comfort: o que define se a venda entra no faturamento financeiro é o ENTREGADOR. // Somente pedidos com Entregador = FG ficam fora do faturamento/recebido pela loja. // Eles continuam entrando normalmente no Lucro Loja e no acerto do Douglas. // O fornecedor do produto NÃO interfere nessa regra: cama/colchão do FG entregue por Rogério, // Tarcísio ou Goldstar entra normalmente no faturamento da Luna. const vendaEntreguePeloFgLuna = (v) => (v.lojaId || LOJA_PADRAO.id) === 'luna' && entregadorLunaVenda(v) === 'FG'; const ehCamaFgEmporio = (v) => { if ((v.lojaId || LOJA_PADRAO.id) !== 'emporio') return false; if (v.tipoProduto === 'cama_fg') return true; return modeloEhCamaFgEmporio(modeloPorId(v.modeloId)); }; const entregadorEmporioCamaVenda = (v) => ehCamaFgEmporio(v) ? (v.entregadorEmporioCama || 'Rogério') : ''; // A mesma regra operacional do FG vale para Luna e para camas FG da Empório: // se o entregador for FG, a venda não entra no faturamento/recebido da loja, // mas continua no lucro da loja e nas obrigações do vendedor. const vendaEntreguePeloFgEmporioCama = (v) => ehCamaFgEmporio(v) && entregadorEmporioCamaVenda(v) === 'FG'; const vendaEntreguePeloFgOperacao = (v) => vendaEntreguePeloFgLuna(v) || vendaEntreguePeloFgEmporioCama(v); const vendaEntraFinanceiroLoja = (v) => !vendaEntreguePeloFgOperacao(v); // Empório: resolve automaticamente o preço normal usado como base do Natan. // Essa regra vale SOMENTE para os sofás. Camas FG têm comissão fixa de R$50. // Prioridade: preço cadastrado no modelo > venda recente em Pix/Dinheiro do mesmo modelo // > venda recente sem parcelamento > preço-base conhecido da regra. const valorNormalNatanAutomatico = (v) => { if ((v.lojaId || LOJA_PADRAO.id) !== 'emporio' || ehCamaFgEmporio(v)) return 0; const m = modeloPorId(v.modeloId); if (Number(m?.preco) > 0) return Number(m.preco); if (v.modeloId) { const historicas = vendas .filter((x) => x.id !== v.id && (x.lojaId || LOJA_PADRAO.id) === 'emporio' && x.modeloId === v.modeloId && statusVenda(x) !== 'cancelado' && statusVenda(x) !== 'devolvido') .filter((x) => Number(x.receita) > 0) .sort((a,b) => String(b.data || '').localeCompare(String(a.data || ''))); const semTaxa = historicas.find((x) => ['pix','dinheiro','nao_informado'].includes(x.formaPagamento || 'nao_informado')); if (semTaxa) return Number(semTaxa.receita) || 0; const naoParcelada = historicas.find((x) => x.formaPagamento !== 'credito' || Number(x.parcelas || 1) <= 1); if (naoParcelada) return Number(naoParcelada.receita) || 0; } const regra = regraModeloEmporio(m?.nome); if (Number(regra?.precoBaseNatan) > 0) return Number(regra.precoBaseNatan); return 0; }; const freteLuna = (cep, tabela, qtd = 1) => { const base = freteFaixa(cep, tabela); if (base === null) return null; return base + Math.max(0, (Number(qtd) || 1) - 1) * 10; }; // Configuração de comissão por loja. O modo “atual” preserva integralmente as regras // históricas já configuradas (Natan por modelo/parcelamento e Douglas pelo acerto próprio). // O usuário pode sobrescrever a regra globalmente ou definir um valor específico por modelo. const comissaoConfigLoja = (lojaId) => { const salvo = config?.comissoes?.[lojaId] || {}; return { tipo: salvo.tipo || 'atual', valorFixo: Number(salvo.valorFixo) || 0, porModelo: salvo.porModelo || {} }; }; const comissaoModeloConfigurada = (v) => { const lojaId = v.lojaId || LOJA_PADRAO.id; if (!['emporio','luna'].includes(lojaId) || !v.modeloId) return null; const cfg = comissaoConfigLoja(lojaId); const raw = cfg.porModelo?.[v.modeloId]; if (raw === '' || raw === null || raw === undefined) return null; const n = Number(raw); return Number.isFinite(n) ? Math.max(0, n) : null; }; const comissaoConfiguradaVenda = (v) => { const lojaId = v.lojaId || LOJA_PADRAO.id; if (!['emporio','luna'].includes(lojaId)) return null; const porModelo = comissaoModeloConfigurada(v); if (porModelo !== null) return porModelo; const cfg = comissaoConfigLoja(lojaId); if (cfg.tipo === 'fixo') return Math.max(0, Number(cfg.valorFixo) || 0); if (cfg.tipo === 'metade_lucro') return Math.max(0, lucroBaseVenda(v)) / 2; return null; }; const comissaoUsaRegraConfigurada = (v) => { const lojaId = v.lojaId || LOJA_PADRAO.id; if (!['emporio','luna'].includes(lojaId)) return false; return comissaoModeloConfigurada(v) !== null || comissaoConfigLoja(lojaId).tipo !== 'atual'; }; const comissaoVendedorManualVenda = (v) => { const raw = v?.comissaoVendedorManual; if (raw === null || raw === undefined || raw === '') return null; const n = Number(raw); return Number.isFinite(n) ? Math.max(0, n) : null; }; const vendedorDevidoVenda = (v) => { const lojaId = v.lojaId || LOJA_PADRAO.id; const manual = comissaoVendedorManualVenda(v); // Uma comissão ajustada manualmente na edição do pedido passa a ser a obrigação oficial // daquele vendedor em todas as contas. O usuário pode voltar ao cálculo automático. if (['emporio','luna'].includes(lojaId) && manual !== null) return manual; const configurada = comissaoConfiguradaVenda(v); if (configurada !== null) return configurada; const m = modeloPorId(v.modeloId); // Empório é recalculada dinamicamente para que pedidos antigos que estavam // congelados apenas com a comissão-base também recebam o ajuste do parcelamento. if (lojaId === 'emporio') { if (ehCamaFgEmporio(v)) return 50; const regra = regraModeloEmporio(m?.nome); return calculoComissaoNatanModelo(v.receita, v.formaPagamento, v.parcelas, regra, valorNormalNatanAutomatico(v)).total; } if (lojaId === 'luna') { // Luna Comfort: a comissão do Douglas usa a tabela específica dos vendedores. // Depois da taxa específica do acerto, fornecedor e frete T1, 50% do resultado positivo vai para Douglas. // O lucro geral da loja continua usando TAXAS_LOJA + frete T2, sem misturar as duas contas. return Math.max(0, resultadoBaseVendedorLuna(v)) / 2; } if (Number.isFinite(Number(v.vendedorDevido))) return Number(v.vendedorDevido) || 0; return 0; }; const vendedorNomeVenda = (v) => v.vendedorNome || PAGAMENTO_PESSOA[v.lojaId]?.vendedor || ''; const detalheNatanVenda = (v) => { const manual = comissaoVendedorManualVenda(v); if (manual !== null) return { total:manual, base:manual, extraNatan:0, acrescimoParcelamento:0, valorNormalPagamento:0, manual:true, camaFg:ehCamaFgEmporio(v) }; const configurada = comissaoConfiguradaVenda(v); if (configurada !== null) return { total:configurada, base:configurada, extraNatan:0, acrescimoParcelamento:0, valorNormalPagamento:0, configurada:true, camaFg:ehCamaFgEmporio(v) }; if (ehCamaFgEmporio(v)) return { total:50, base:50, extraNatan:0, acrescimoParcelamento:0, valorNormalPagamento:0, camaFg:true }; const m = modeloPorId(v.modeloId); const regra = regraModeloEmporio(m?.nome); return calculoComissaoNatanModelo(v.receita, v.formaPagamento, v.parcelas, regra, valorNormalNatanAutomatico(v)); }; const ajusteAdsPeriodoRegistro = (lojaId, from = per.from, to = per.to) => adsManuais.find((x) => x.origem === 'ajuste_periodo' && x.lojaId === lojaId && x.periodoFrom === from && x.periodoTo === to) || null; const ajusteVendedorPeriodoRegistro = (lojaId, from = per.from, to = per.to) => pagamentos.find((x) => x.origem === 'ajuste_periodo' && x.tipo === 'vendedor' && x.lojaId === lojaId && x.periodoFrom === from && x.periodoTo === to) || null; const pagamentoPeriodo = (lojaId, tipo, from = per.from, to = per.to) => pagamentos.filter((x) => x.origem !== 'ajuste_periodo' && x.lojaId === lojaId && x.tipo === tipo && x.data >= from && x.data <= to).reduce((s,x) => s + (Number(x.valor) || 0), 0); const vendedorPagoLojaNoPeriodo = (lojaId, from = per.from, to = per.to) => { const ajuste = ajusteVendedorPeriodoRegistro(lojaId, from, to); if (ajuste) return Math.max(0, Number(ajuste.valor) || 0); return pagamentos.filter((x) => x.origem !== 'ajuste_periodo' && x.tipo === 'vendedor' && x.lojaId === lojaId && x.data >= from && x.data <= to).reduce((s,x) => s + (Number(x.valor) || 0), 0); }; // Vendedor só vira despesa quando o pagamento é efetivamente marcado/registrado como pago. // Empório/Luna podem sobrescrever manualmente o TOTAL do período selecionado. O ajuste substitui // os lançamentos detalhados apenas naquele intervalo, sem apagar o histórico original. const vendedorPagoNoPeriodo = (from = per.from, to = per.to) => { const lojasAlvo = lojaFiltro === 'todas' ? ['emporio','luna'] : (['emporio','luna'].includes(lojaFiltro) ? [lojaFiltro] : []); if (!lojasAlvo.length) return 0; return lojasAlvo.reduce((s, lojaId) => { const ajusteAtual = ajusteVendedorPeriodoRegistro(lojaId, per.from, per.to); // Na tabela diária, um total manual do período aparece apenas no último dia, // sem inventar uma divisão por dia. O TOTAL continua exatamente igual ao digitado. if (ajusteAtual && from === to && (per.from !== per.to || from !== per.from)) { return s + (from === per.to ? Math.max(0, Number(ajusteAtual.valor) || 0) : 0); } return s + vendedorPagoLojaNoPeriodo(lojaId, from, to); }, 0); }; const acertoValorVenda = (v) => { const lojaId = v.lojaId || LOJA_PADRAO.id; // Luna: o valor mostrado/marcado no acerto e enviado ao Douglas é o resultado CHEIO // antes da divisão de 50%. A comissão efetiva do Douglas continua sendo metade dele. // Um valor digitado manualmente pelo usuário continua sendo respeitado. if (lojaId === 'luna' && !acertoPagoVenda(v) && !v.acertoValorManual) { const manual = comissaoVendedorManualVenda(v); // O número enviado ao Douglas é sempre o valor cheio. Se a comissão foi editada // no pedido ou configurada na página Ajustes, o acerto cheio vira 2x a comissão, // preservando a apresentação 50/50 do fechamento. if (manual !== null) return Math.max(0, manual * 2); if (comissaoUsaRegraConfigurada(v)) return Math.max(0, vendedorDevidoVenda(v) * 2); return Math.max(0, resultadoBaseVendedorLuna(v)); } if (lojaId === 'emporio' && !acertoPagoVenda(v) && !v.acertoValorManual) return Math.max(0, vendedorDevidoVenda(v)); if (Number.isFinite(Number(v.acertoValor))) return Number(v.acertoValor) || 0; return lojaId === 'luna' ? Math.max(0, resultadoBaseVendedorLuna(v)) : Math.max(0, vendedorDevidoVenda(v)); }; const valorPagamentoAcertoVenda = (v) => { const base = Math.max(0, acertoValorVenda(v)); // Na Luna, o número do acerto é enviado cheio no WhatsApp; somente 50% vira pagamento/despesa do Douglas. return (v.lojaId || LOJA_PADRAO.id) === 'luna' ? base / 2 : base; }; const acertoRecebedorVenda = (v) => v.acertoRecebedor || vendedorNomeVenda(v) || 'Recebedor'; const acertoTipoVenda = (v) => v.acertoTipo || 'vendedor'; const acertoPagoVenda = (v) => v.acertoStatus === 'pago'; const pagamentoAcertoId = (v) => `acerto-${v.id}`; const enriquecerVenda = (base, lojaId, modeloId) => { const m = modelos.find((x) => x.id === modeloId); const bruto = Number(base.receita) || 0; const parcelas = Math.min(12, Math.max(1, Number(base.parcelas) || 1)); const qtdItens = Math.max(1, Number(base.qtdItens) || 1); const formaPagamento = base.formaPagamento || 'nao_informado'; let custo = Number(base.custo) || 0; let frete = Number(base.frete) || 0; let freteTabela1 = Number(base.freteTabela1) || 0; let vendedorNome = ''; let vendedorDevido = 0; if (lojaId === 'emporio') { const regra = regraModeloEmporio(m?.nome); const camaFg = modeloEhCamaFgEmporio(m) || regra?.tipoProduto === 'cama_fg' || base.tipoProduto === 'cama_fg'; if (regra && !custo) custo = regra.custo; vendedorNome = 'Natan'; if (camaFg) { const calcT2 = freteLuna(base.cep, FRETE_LUNA_T2, qtdItens); if (!frete && calcT2 !== null) frete = calcT2; vendedorDevido = 50; base = { ...base, entregadorEmporioCama: base.entregadorEmporioCama || 'Rogério' }; } else { if (!frete) frete = 130; const normalAuto = valorNormalNatanAutomatico({ ...base, lojaId, modeloId }); vendedorDevido = calculoComissaoNatanModelo(bruto, formaPagamento, parcelas, regra, normalAuto).total; } base = { ...base, tipoProduto: camaFg ? 'cama_fg' : (base.tipoProduto || 'sofa') }; } else if (lojaId === 'luna') { const regra = regraModeloLuna(m?.nome) || LUNA_MODELOS.find((x) => x.id === modeloId); if (regra && !custo) custo = regra.custo; const calcT2 = freteLuna(base.cep, FRETE_LUNA_T2, qtdItens); const calcT1 = freteLuna(base.cep, FRETE_LUNA_T1, qtdItens); if (!frete && calcT2 !== null) frete = calcT2; // T1 é a planilha usada exclusivamente no acerto do Douglas; T2 (frete) segue no lucro oficial da loja. if (!freteTabela1 && calcT1 !== null) freteTabela1 = calcT1; vendedorNome = 'Douglas'; } const taxaPct = ['emporio','luna'].includes(lojaId) ? taxaPagamentoPct(formaPagamento, parcelas, TAXAS_LOJA) : 0; const taxaValor = bruto * taxaPct / 100; if (lojaId === 'luna') { const taxaVendedorPct = taxaPagamentoPct(formaPagamento, parcelas, TAXAS_VENDEDOR); const taxaVendedorValor = bruto * taxaVendedorPct / 100; vendedorDevido = Math.max(0, bruto - taxaVendedorValor - custo - freteTabela1) / 2; } const entregadorLuna = lojaId === 'luna' ? (base.entregadorLuna || 'Tarcísio') : ''; const pagamentoRecebidoPor = lojaId === 'luna' ? ((base.pagamentoRecebidoPor === 'fg' || base.pagamentoRecebidoPor === 'luna') ? base.pagamentoRecebidoPor : (entregadorLuna === 'FG' ? 'fg' : 'luna')) : ''; const entregadorEmporioCama = lojaId === 'emporio' && (base.tipoProduto === 'cama_fg' || modeloEhCamaFgEmporio(m)) ? (base.entregadorEmporioCama || 'Rogério') : ''; return { ...base, lojaId, custo, frete, parcelas, qtdItens, taxaCartaoPct: taxaPct, taxaCartaoValor: taxaValor, freteTabela1, vendedorNome, vendedorDevido, entregadorLuna, entregadorEmporioCama, pagamentoRecebidoPor }; }; const sugerirModeloPorTexto = (texto, lojaId) => { const regra = lojaId === 'emporio' ? regraModeloEmporio(texto) : lojaId === 'luna' ? regraModeloLuna(texto) : null; if (regra) return modelos.find((m) => m.id === regra.id) || regra; const n = normalizarNomeModelo(texto); return modelos.find((m) => (!m.lojaId || m.lojaId === lojaId) && (normalizarNomeModelo(m.nome) === n || n.includes(normalizarNomeModelo(m.nome)) || normalizarNomeModelo(m.nome).includes(n))); }; const interpretarFicha = () => { const txt = String(pedidoPaste || '').replace(/\r/g,''); if (!txt.trim()) return; const lojaId = lojaFiltro !== 'todas' ? lojaFiltro : vLojaId; const pick = (re) => (txt.match(re)?.[1] || '').trim(); const nome = pick(/NOME\s*:\s*([^\n]+)/i); const cep = (pick(/CEP\s*:\s*([0-9.\-]+)/i) || '').replace(/\D/g,''); const item = pick(/ITENS?\s*:\s*([^\n]+)/i).replace(/^[^A-Za-zÀ-ÿ0-9]+/,'').trim(); const dataRaw = pick(/(?:ENTREGA|PREVISÃO PARA)\s*:\s*[^0-9]*(\d{1,2}\/\d{1,2}\/\d{4})/i) || pick(/ENTREGA\s*:[^0-9]*(\d{1,2}\/\d{1,2}\/\d{4})/i); const dataPrev = dataRaw ? dataRaw.split('/').reverse().join('-') : ''; const linhaPag = pick(/(?:FORMA|CONDIÇÃO)\s*(?:DE\s*)?PAGAMENTO\s*:\s*([^\n]+)/i); const blocoPag = (txt.match(/(?:FORMA|CONDIÇÃO)\s*(?:DE\s*)?PAGAMENTO\s*:\s*([\s\S]{0,120})/i)?.[1] || linhaPag).split(/(?:ATENÇÃO|OBSERVAÇÃO|PONTO DE REFERÊNCIA)/i)[0]; let forma = 'nao_informado'; if (/pix/i.test(linhaPag)) forma = 'pix'; else if (/dinheiro/i.test(linhaPag)) forma = 'dinheiro'; else if (/d[eé]bito/i.test(linhaPag)) forma = 'debito'; else if (/cr[eé]dito/i.test(linhaPag)) forma = 'credito'; const parcelas = Number((linhaPag.match(/(\d{1,2})\s*x/i)||[])[1]) || 1; const valores = [...blocoPag.matchAll(/(?:R\$\s*)?([0-9]{1,3}(?:\.[0-9]{3})*,[0-9]{2}|[0-9]+,[0-9]{2}|[0-9]{1,3}(?:\.[0-9]{3})+)/g)].map((m) => parseBRL(m[1])); const valorProdutoFicha = parseBRL((txt.match(/(?:^|\n)\s*VALOR\s*:\s*(?:R\$\s*)?([0-9.]+(?:,[0-9]{1,2})?)/im)||[])[1]); const valorFreteFicha = parseBRL((txt.match(/VALOR\s+DO\s+FRETE\s*:\s*(?:R\$\s*)?([0-9.]+(?:,[0-9]{1,2})?)/i)||[])[1]); let receita = valorProdutoFicha > 0 ? valorProdutoFicha : (valores.length ? valores[valores.length-1] : 0); // Formatos comuns das fichas: “Crédito 10x de 126,00” / “5x R$ 228,18”. // Nesses casos o valor escrito é a parcela, então o faturamento é parcela × quantidade. if (valorProdutoFicha <= 0 && forma === 'credito' && parcelas > 1) { const pm = linhaPag.match(/(\d{1,2})\s*x\s*(?:de\s*)?(?:R\$\s*)?([0-9]{1,3}(?:\.[0-9]{3})*,[0-9]{2}|[0-9]+(?:,[0-9]{2})?)/i); if (pm) receita = Number(pm[1]) * parseBRL(pm[2]); } // Quantidade: nunca deixe \s* atravessar uma quebra de linha aqui. // Ex.: um telefone na linha anterior a "Itens:" estava sendo lido como quantidade. const qtdRotulo = (txt.match(/(?:^|\n)[ \t]*(?:QTD(?:\.|ADE)?|QUANTIDADE)[ \t]*:[ \t]*(\d+)/im) || [])[1]; const qtdNaMesmaLinha = (txt.match(/(?:^|\n)[^\n]*?\b(\d+)[ \t]+itens?\b[^\n]*$/im) || [])[1]; const qtdNoInicioDoItem = (item.match(/^[ \t]*(\d+)[ \t]*(?:x\b|un(?:id(?:ades?)?)?\b)?/i) || [])[1]; const qtd = Math.max(1, Number(qtdRotulo || qtdNaMesmaLinha || qtdNoInicioDoItem) || 1); const m = sugerirModeloPorTexto(item, lojaId); setVCliente(nome); setVCep(cep); setVQtdItens(qtd); setVFormaPagamento(forma); setVParcelas(parcelas); if (dataPrev) setVDataPrevista(dataPrev); if (receita) setVReceita(String(receita)); if (m) { setVModelo(m.id); setVCusto(m.custo ? String(m.custo) : ''); if (lojaId === 'emporio') { if (modeloEhCamaFgEmporio(m)) { const f2 = freteLuna(cep, FRETE_LUNA_T2, qtd); setVFrete(f2 === null ? '' : String(f2)); setVFreteTabela1(''); } else setVFrete('130'); } if (lojaId === 'luna') { const f2 = freteLuna(cep, FRETE_LUNA_T2, qtd); const f1 = freteLuna(cep, FRETE_LUNA_T1, qtd); setVFrete(f2 === null ? '' : String(f2)); setVFreteTabela1(f1 === null ? '' : String(f1)); } } setVLojaId(lojaId || LOJA_PADRAO.id); if (!vCampanha) setVCampanha(ADX); const avisoCusto = lojaId === 'emporio' && modeloEhCamaFgEmporio(m) && !(Number(m?.custo)>0) ? ' · ATENÇÃO: custo FG não cadastrado; preencha o custo do produto' : ''; const infoFreteFicha = valorFreteFicha > 0 ? ` · frete informado ao cliente ${brl(valorFreteFicha)} (o painel usa a tabela de frete da loja)` : ''; setPedidoParseMsg(`Ficha lida${m ? ` · ${m.nome}` : item ? ` · modelo não reconhecido: ${item}` : ''}${cep ? ` · CEP ${cep.slice(0,5)}-${cep.slice(5)}` : ''}${infoFreteFicha}${avisoCusto}. Confira os campos antes de registrar.`); }; /* ---------- ações ---------- */ const dailyMonths = useMemo(() => { const s = new Set(); for (const g of gastos) if (g.date) s.add(g.date.slice(0, 7)); return s; }, [gastos]); const getGastoMes = (cid, m) => { if (dailyMonths.has(m)) return gastos.filter((g) => g.campanhaId === cid && g.date && g.date.slice(0, 7) === m).reduce((s, g) => s + g.valor, 0); return gastos.filter((g) => g.campanhaId === cid && g.mes === m).reduce((s, g) => s + g.valor, 0); }; const getGastoPeriodo = (cid) => { let sum = 0; for (const g of gastos) { if (g.campanhaId !== cid) continue; if (g.date) { if (g.date >= per.from && g.date <= per.to) sum += g.valor; } else if (g.mes && !dailyMonths.has(g.mes)) { const ms = `${g.mes}-01`, me = `${g.mes}-${lastDay(g.mes)}`; if (per.from <= me && per.to >= ms) sum += g.valor; } } return sum; }; const upsertGasto = (cid, m, valor) => setGastos((prev) => { const rest = prev.filter((g) => !(g.campanhaId === cid && (g.mes === m || (g.date && g.date.slice(0, 7) === m)))); return valor > 0 ? [...rest, { campanhaId: cid, mes: m, valor }] : rest; }); const isOrg = vCampanha === ORG; const onModelo = (id) => { setVModelo(id); const m = modelos.find((x) => x.id === id); if (!m) return; setVReceita(m.preco ? String(m.preco) : ''); setVCusto(m.custo ? String(m.custo) : ''); if (lojaFormulario === 'emporio') { if (modeloEhCamaFgEmporio(m)) { const f2 = freteLuna(vCep, FRETE_LUNA_T2, vQtdItens); setVFrete(f2 === null ? '' : String(f2)); setVFreteTabela1(''); setVEntregadorEmporioCama('Rogério'); } else setVFrete(String(m.frete || 130)); } else if (lojaFormulario === 'luna') { const f2 = freteLuna(vCep, FRETE_LUNA_T2, vQtdItens); const f1 = freteLuna(vCep, FRETE_LUNA_T1, vQtdItens); setVFrete(f2 === null ? '' : String(f2)); setVFreteTabela1(f1 === null ? '' : String(f1)); } else setVFrete(m.frete ? String(m.frete) : ''); }; const addVenda = () => { const receita = parseBRL(vReceita); if (!vCampanha || receita <= 0) return; const base = { id: uid(), data: vData, cliente: vCliente.trim(), receita, custo: parseBRL(vCusto), frete: parseBRL(vFrete), modeloId: vModelo || null, status: vStatus === 'em_rota' ? 'confirmado' : vStatus, dataPrevista: vDataPrevista || '', formaPagamento: vFormaPagamento, parcelas: vParcelas, cep: vCep, qtdItens: vQtdItens, freteTabela1: parseBRL(vFreteTabela1), tipoProduto: lojaFormulario === 'emporio' && modeloEhCamaFgEmporio(modelos.find((x)=>x.id===vModelo)) ? 'cama_fg' : (lojaFormulario === 'emporio' ? 'sofa' : ''), entregadorLuna: lojaFormulario === 'luna' ? vEntregadorLuna : '', entregadorEmporioCama: lojaFormulario === 'emporio' && modeloEhCamaFgEmporio(modelos.find((x)=>x.id===vModelo)) ? vEntregadorEmporioCama : '', pagamentoRecebidoPor: lojaFormulario === 'luna' ? vPagamentoRecebidoPorLuna : '', dataEntrega: ['entregue', 'devolvido'].includes(vStatus) ? (vDataEntrega || todayISO()) : '', }; const lojaExplicita = (vLojaId && lojas.some((l) => l.id === vLojaId)) ? vLojaId : LOJA_PADRAO.id; let lojaId = lojaExplicita; if (![ORG, ADX].includes(vCampanha)) lojaId = campanhas.find((c) => c.id === vCampanha)?.lojaId || lojaExplicita; let nova = enriquecerVenda(base, lojaId, vModelo || null); if (vCampanha === ORG) nova = { ...nova, campanhaId: ORG, origem: vOrigem }; else if (vCampanha === ADX) nova = { ...nova, campanhaId: ADX }; else nova = { ...nova, campanhaId: vCampanha }; setVendas((p) => [...p, nova]); setVCliente(''); setVReceita(''); setVCusto(''); setVFrete(''); setVModelo(''); setVDataPrevista(''); setVCep(''); setVQtdItens(1); setVParcelas(1); setVFreteTabela1(''); setPedidoPaste(''); setPedidoParseMsg(''); setVStatus('confirmado'); setVDataEntrega(''); setVFormaPagamento('nao_informado'); setVEntregadorLuna('Tarcísio'); setVEntregadorEmporioCama('Rogério'); setVPagamentoRecebidoPorLuna('luna'); }; const atualizarStatusVenda = (id, status) => setVendas((prev) => prev.map((v) => { if (v.id !== id) return v; const dataEntrega = status === 'entregue' ? (v.dataEntrega || todayISO()) : (status === 'devolvido' ? (v.dataEntrega || todayISO()) : ''); return { ...v, status, dataEntrega }; })); const confirmarEntrega = (id) => atualizarStatusVenda(id, 'entregue'); const addDespesa = () => { const valor = parseBRL(finValor); if (!finDescricao.trim() || valor <= 0 || !finData) return; setDespesas((p) => [...p, { id: uid(), data: finData, descricao: finDescricao.trim(), categoria: finCategoria, valor, recorrencia: finRecorrencia, lojaId: finLojaId || 'geral' }]); setFinDescricao(''); setFinValor(''); setFinRecorrencia('nao'); }; const delDespesa = (id) => setDespesas((p) => p.filter((d) => d.id !== id)); const addAdManual = () => { const valor = parseBRL(adManualValor); if (!adManualData || !['emporio','luna'].includes(adManualLoja) || valor <= 0) return; setAdsManuais((p) => [...p, { id: uid(), data: adManualData, lojaId: adManualLoja, valor, observacao: adManualObs.trim(), origem:'manual' }]); setAdManualValor(''); setAdManualObs(''); }; const delAdManual = (id) => setAdsManuais((p) => p.filter((x) => x.id !== id)); const addPagamento = () => { const valor = parseBRL(pagValor); if (!pagData || !['emporio','luna'].includes(pagLoja) || valor <= 0) return; const pessoa = PAGAMENTO_PESSOA[pagLoja]?.[pagTipo] || (pagTipo === 'vendedor' ? 'Vendedor' : 'Freteiro'); setPagamentos((p) => [...p, { id:uid(), data:pagData, lojaId:pagLoja, tipo:pagTipo, pessoa, valor, observacao:pagObs.trim() }]); setPagValor(''); setPagObs(''); }; const delPagamento = (id) => { const rec = pagamentos.find((x) => x.id === id); setPagamentos((p) => p.filter((x) => x.id !== id)); if (rec?.origem === 'acerto_semanal' && rec.vendaId) { setVendas((p) => p.map((v) => v.id === rec.vendaId ? { ...v, acertoStatus:'pendente', acertoDataPagamento:'' } : v)); } }; const salvarAjustePeriodoFinanceiro = () => { if (!['emporio','luna'].includes(lojaFiltro)) return; const temAds = String(ajustePeriodoAdsInput).trim() !== ''; const temVendedor = String(ajustePeriodoVendedorInput).trim() !== ''; const adsValor = Math.max(0, parseBRL(ajustePeriodoAdsInput)); const vendedorValor = Math.max(0, parseBRL(ajustePeriodoVendedorInput)); const pessoa = PAGAMENTO_PESSOA[lojaFiltro]?.vendedor || 'Vendedor'; setAdsManuais((prev) => { const base = prev.filter((x) => !(x.origem === 'ajuste_periodo' && x.lojaId === lojaFiltro && x.periodoFrom === per.from && x.periodoTo === per.to)); if (!temAds) return base; return [...base, { id:uid(), data:per.to, lojaId:lojaFiltro, valor:adsValor, observacao:`Total manual do período ${brDate(per.from)} a ${brDate(per.to)}`, origem:'ajuste_periodo', periodoFrom:per.from, periodoTo:per.to }]; }); setPagamentos((prev) => { const base = prev.filter((x) => !(x.origem === 'ajuste_periodo' && x.tipo === 'vendedor' && x.lojaId === lojaFiltro && x.periodoFrom === per.from && x.periodoTo === per.to)); if (!temVendedor) return base; return [...base, { id:uid(), data:per.to, lojaId:lojaFiltro, tipo:'vendedor', pessoa, valor:vendedorValor, observacao:`Total manual pago no período ${brDate(per.from)} a ${brDate(per.to)}`, origem:'ajuste_periodo', periodoFrom:per.from, periodoTo:per.to }]; }); setAjustePeriodoMsg('Valores do período atualizados ✓'); }; const limparAjustePeriodoFinanceiro = () => { if (!['emporio','luna'].includes(lojaFiltro)) return; setAdsManuais((prev) => prev.filter((x) => !(x.origem === 'ajuste_periodo' && x.lojaId === lojaFiltro && x.periodoFrom === per.from && x.periodoTo === per.to))); setPagamentos((prev) => prev.filter((x) => !(x.origem === 'ajuste_periodo' && x.tipo === 'vendedor' && x.lojaId === lojaFiltro && x.periodoFrom === per.from && x.periodoTo === per.to))); setAjustePeriodoAdsInput(''); setAjustePeriodoVendedorInput(''); setAjustePeriodoMsg('Ajuste removido. O painel voltou a usar os lançamentos detalhados.'); }; const syncPagamentoAcerto = (v, patch = {}) => { const next = { ...v, ...patch }; if (!next.acertoAtivo || next.acertoStatus !== 'pago') return; const id = pagamentoAcertoId(next); const valor = valorPagamentoAcertoVenda(next); const pessoa = next.acertoRecebedor || acertoRecebedorVenda(next); const tipo = next.acertoTipo || 'vendedor'; const data = next.acertoDataPagamento || todayISO(); const rec = { id, data, lojaId: next.lojaId || LOJA_PADRAO.id, tipo, pessoa, valor, observacao: `Acerto semanal · ${next.cliente || 'pedido'}`, origem:'acerto_semanal', vendaId: next.id }; setPagamentos((p) => [...p.filter((x) => x.id !== id && !(x.origem === 'acerto_semanal' && x.vendaId === next.id)), rec]); }; const toggleAcertoVenda = (v) => { if (v.acertoAtivo && acertoPagoVenda(v)) { setAcertoMsg('Este pedido já está marcado como pago. Desmarque o pagamento antes de removê-lo do acerto.'); return; } const ativar = !v.acertoAtivo; setVendas((p) => p.map((x) => x.id !== v.id ? x : ativar ? { ...x, acertoAtivo:true, acertoValor:acertoValorVenda(x), acertoRecebedor:acertoRecebedorVenda(x), acertoTipo:acertoTipoVenda(x), acertoStatus:x.acertoStatus || 'pendente', acertoDataPagamento:x.acertoDataPagamento || '', acertoObservacao:x.acertoObservacao || '', } : { ...x, acertoAtivo:false })); setAcertoMsg(ativar ? 'Pedido incluído no acerto semanal ✓' : 'Pedido removido do acerto semanal.'); }; const atualizarAcertoVenda = (v, patch) => { const enrichedPatch = Object.prototype.hasOwnProperty.call(patch, 'acertoValor') ? { ...patch, acertoValorManual:true } : patch; const next = { ...v, ...enrichedPatch }; setVendas((p) => p.map((x) => x.id === v.id ? { ...x, ...enrichedPatch } : x)); syncPagamentoAcerto(next); }; const marcarAcertoPago = (v, pago) => { const id = pagamentoAcertoId(v); if (!pago) { setVendas((p) => p.map((x) => x.id === v.id ? { ...x, acertoAtivo:true, acertoStatus:'pendente', acertoDataPagamento:'' } : x)); setPagamentos((p) => p.filter((x) => x.id !== id && !(x.origem === 'acerto_semanal' && x.vendaId === v.id))); setAcertoMsg('Pagamento desmarcado; o pedido voltou para pendente.'); return; } const patch = { acertoAtivo:true, acertoStatus:'pago', acertoDataPagamento:v.acertoDataPagamento || todayISO(), acertoValor:acertoValorVenda(v), acertoRecebedor:acertoRecebedorVenda(v), acertoTipo:acertoTipoVenda(v), }; setVendas((p) => p.map((x) => x.id === v.id ? { ...x, ...patch } : x)); syncPagamentoAcerto({ ...v, ...patch }); setAcertoMsg('Pagamento marcado como pago e registrado em “A pagar” ✓'); }; const copiarAcerto = async (texto, label = 'Atualização copiada ✓') => { if (!texto) return; try { await navigator.clipboard.writeText(texto); setAcertoMsg(label); } catch { try { const ta = document.createElement('textarea'); ta.value = texto; ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); setAcertoMsg(label); } catch { setAcertoMsg('Não consegui copiar automaticamente.'); } } }; const baixarTexto = (nome, texto) => { if (!texto) return; try { const blob = new Blob([texto], { type:'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href=url; a.download=nome; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 500); setAcertoMsg('Arquivo TXT gerado ✓'); } catch { setAcertoMsg('Não consegui gerar o arquivo TXT.'); } }; const addCampanha = () => { if (!cNome.trim()) return; const lojaId = (cLojaId && lojas.some((l) => l.id === cLojaId)) ? cLojaId : (lojas[0]?.id || LOJA_PADRAO.id); setCampanhas((p) => [...p, { id: uid(), nome: cNome.trim(), lojaId, plataforma: cPlataforma }]); setCNome(''); }; const addLoja = () => { const nome = novaLoja.trim(); if (!nome) return; if (lojas.some((l) => norm(l.nome) === norm(nome))) { setNovaLoja(''); return; } const id = nome.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || uid(); const finalId = lojas.some((l) => l.id === id) ? `${id}-${uid().slice(0, 4)}` : id; setLojas((p) => [...p, { id: finalId, nome }]); setNovaLoja(''); }; const renameLoja = (id, nome) => setLojas((p) => p.map((l) => (l.id === id ? { ...l, nome } : l))); const delLoja = (id) => { if (LOJAS_SEED.some((l) => l.id === id)) { setLojaMsg('Essential, Empório e Luna são lojas principais e não podem ser excluídas.'); return; } const emUso = campanhas.some((c) => (c.lojaId || LOJA_PADRAO.id) === id) || vendas.some((v) => lojaDaVenda(v) === id) || despesas.some((d) => d.lojaId === id) || modelos.some((m) => m.lojaId === id) || adsManuais.some((a) => a.lojaId === id) || pagamentos.some((a) => a.lojaId === id); if (emUso) { setLojaMsg('Essa loja tem pedidos, campanhas, modelos ou lançamentos vinculados — mova-os antes de excluir.'); return; } setLojas((p) => p.filter((l) => l.id !== id)); if (lojaFiltro === id) setLojaFiltro('todas'); setLojaMsg(''); }; const delVenda = (id) => { setVendas((p) => p.filter((v) => v.id !== id)); setPagamentos((p) => p.filter((x) => !(x.origem === 'acerto_semanal' && x.vendaId === id))); }; const delCampanha = (id) => { setCampanhas((p) => p.filter((c) => c.id !== id)); setGastos((p) => p.filter((g) => g.campanhaId !== id)); setVendas((p) => p.filter((v) => v.campanhaId !== id)); if (vCampanha === id) setVCampanha(''); }; const mergeCampanha = (fromId, toId) => { if (!fromId || !toId || fromId === toId) { setMergePending(null); return; } const from = campanhas.find((c) => c.id === fromId), to = campanhas.find((c) => c.id === toId); if (!from || !to || (from.lojaId || LOJA_PADRAO.id) !== (to.lojaId || LOJA_PADRAO.id)) { setMergePending(null); return; } setGastos((prev) => { const reassigned = prev.map((g) => (g.campanhaId === fromId ? { ...g, campanhaId: toId } : g)); const map = new Map(); reassigned.forEach((g) => { const k = `${g.campanhaId}|${g.date || g.mes}`; if (map.has(k)) map.get(k).valor += g.valor; else map.set(k, { ...g }); }); return [...map.values()]; }); setVendas((prev) => prev.map((v) => (v.campanhaId === fromId ? { ...v, campanhaId: toId } : v))); setCampanhas((prev) => prev.filter((c) => c.id !== fromId)); if (vCampanha === fromId) setVCampanha(toId); setMergePending(null); }; const salvarModelo = () => { if (!mNome.trim()) return; const rec = { nome: mNome.trim(), preco: parseBRL(mPreco), custo: parseBRL(mCusto), frete: parseBRL(mFrete), fornecedor: mFornecedor.trim(), lojaId: mLojaId || '' }; if (editModelo) setModelos((p) => p.map((m) => (m.id === editModelo ? { ...m, ...rec } : m))); else setModelos((p) => [...p, { id: uid(), ...rec }]); setMNome(''); setMPreco(''); setMCusto(''); setMFrete(''); setMFornecedor(''); setMLojaId(''); setEditModelo(null); }; const startEditModelo = (m) => { setEditModelo(m.id); setMNome(m.nome); setMPreco(m.preco ? String(m.preco) : ''); setMCusto(m.custo ? String(m.custo) : ''); setMFrete(m.frete ? String(m.frete) : ''); setMFornecedor(m.fornecedor || ''); setMLojaId(m.lojaId || ''); }; const delModelo = (id) => { setModelos((p) => p.filter((m) => m.id !== id)); setVendas((p) => p.map((v) => (v.modeloId === id ? { ...v, modeloId: null } : v))); }; const exportBackup = () => { try { const dump = { app: 'metricas-campanhas-whatsapp', versao: 10, exportadoEm: new Date().toISOString(), lojas, campanhas, gastos, vendas, modelos, despesas, adsManuais, pagamentos, fgMovimentos, historico, marketplaceProdutos, marketplacePedidos, marketplaceAds, config }; const blob = new Blob([JSON.stringify(dump, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `metricas-backup-${todayISO()}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setBackupMsg('Backup baixado ✓'); } catch { setBackupMsg('Não consegui gerar o backup.'); } }; const importBackup = (e) => { const file = e.target.files && e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const d = migrarLojas(JSON.parse(String(reader.result))); setLojas(mergeById(d.lojas, lojas)); setCampanhas(mergeById(d.campanhas, campanhas)); setGastos(mergeGastos(d.gastos, gastos)); setVendas(mergeById(d.vendas, vendas)); setModelos(mergeById(d.modelos, modelos)); setDespesas(mergeById((d.despesas || []).map((x) => ({ ...x, lojaId: x.lojaId || 'geral' })), despesas)); setAdsManuais(mergeById(d.adsManuais || [], adsManuais)); setPagamentos(mergeById(d.pagamentos || [], pagamentos)); setFgMovimentos(mergeById(d.fgMovimentos || [], fgMovimentos)); setHistorico(mergeById(d.historico || [], historico)); setMarketplaceProdutos(mergeById(d.marketplaceProdutos || [], marketplaceProdutos)); setMarketplacePedidos(mergeById(d.marketplacePedidos || [], marketplacePedidos)); setMarketplaceAds(mergeById(d.marketplaceAds || [], marketplaceAds)); if (d.config) setConfig(d.config); setBackupMsg('Backup restaurado e mesclado ✓'); } catch { setBackupMsg('Arquivo inválido — não consegui ler o backup.'); } }; reader.readAsText(file); e.target.value = ''; }; const startEdit = (v) => { setEditId(v.id); setEditDraft({ data: v.data || todayISO(), tipo: v.campanhaId, origem: v.origem || ORIGENS[0], modeloId: v.modeloId || '', cliente: v.cliente || '', receita: String(v.receita ?? ''), custo: String(v.custo ?? ''), frete: String(v.frete ?? ''), status: statusVenda(v), dataPrevista: v.dataPrevista || '', dataEntrega: v.dataEntrega || '', formaPagamento: v.formaPagamento || 'nao_informado', lojaId: v.lojaId || LOJA_PADRAO.id, parcelas: v.parcelas || 1, cep: v.cep || '', qtdItens: v.qtdItens || 1, freteTabela1: String(v.freteTabela1 ?? ''), tipoProduto: v.tipoProduto || '', entregadorLuna: entregadorLunaVenda(v) || 'Tarcísio', entregadorEmporioCama: entregadorEmporioCamaVenda(v) || 'Rogério', pagamentoRecebidoPor: pagamentoRecebidoPorLunaVenda(v) === 'fg' ? 'fg' : 'luna', comissaoVendedorManual: String(comissaoVendedorManualVenda(v) ?? vendedorDevidoVenda(v) ?? 0), comissaoVendedorManualAtiva: comissaoVendedorManualVenda(v) !== null, }); }; const cancelEdit = () => { setEditId(null); setEditDraft(null); }; const saveEdit = () => { if (!editDraft) return; const receita = parseBRL(editDraft.receita); if (receita <= 0) return; setVendas((p) => p.map((v) => { if (v.id !== editId) return v; const status = editDraft.status === 'em_rota' ? 'confirmado' : (editDraft.status || statusVenda(v)); const base = { id: v.id, data: editDraft.data, cliente: editDraft.cliente.trim(), receita, custo: parseBRL(editDraft.custo), frete: parseBRL(editDraft.frete), modeloId: editDraft.modeloId || null, status, dataPrevista: editDraft.dataPrevista || '', formaPagamento: editDraft.formaPagamento || 'nao_informado', parcelas: editDraft.parcelas || 1, cep: editDraft.cep || '', qtdItens: editDraft.qtdItens || 1, freteTabela1: parseBRL(editDraft.freteTabela1), tipoProduto: editDraft.tipoProduto || '', entregadorLuna: editDraft.lojaId === 'luna' ? (editDraft.entregadorLuna || 'Tarcísio') : '', entregadorEmporioCama: editDraft.lojaId === 'emporio' && (editDraft.tipoProduto === 'cama_fg' || modeloEhCamaFgEmporio(modelos.find((x)=>x.id===editDraft.modeloId))) ? (editDraft.entregadorEmporioCama || 'Rogério') : '', pagamentoRecebidoPor: editDraft.lojaId === 'luna' ? (editDraft.pagamentoRecebidoPor || 'luna') : '', comissaoVendedorManual: ['emporio','luna'].includes(editDraft.lojaId) && editDraft.comissaoVendedorManualAtiva ? parseBRL(editDraft.comissaoVendedorManual) : null, dataEntrega: status === 'entregue' ? (editDraft.dataEntrega || todayISO()) : (status === 'devolvido' ? (editDraft.dataEntrega || v.dataEntrega || todayISO()) : ''), }; let lojaId = editDraft.lojaId || v.lojaId || LOJA_PADRAO.id; if (![ORG, ADX].includes(editDraft.tipo)) lojaId = campanhas.find((c) => c.id === editDraft.tipo)?.lojaId || lojaId; const rich = enriquecerVenda(base, lojaId, editDraft.modeloId || null); if (editDraft.tipo === ORG) return { ...v, ...rich, campanhaId: ORG, origem: editDraft.origem }; if (editDraft.tipo === ADX) return { ...v, ...rich, campanhaId: ADX }; return { ...v, ...rich, campanhaId: editDraft.tipo }; })); cancelEdit(); }; /* ---------- Windsor.ai ---------- */ const autoMatch = (name) => { const n = norm(name); if (!n) return ''; // Por enquanto o Windsor pertence somente à Essential Estofados. const pool = campanhas.filter((c) => (c.lojaId || LOJA_PADRAO.id) === LOJA_PADRAO.id); const exact = pool.find((c) => norm(c.nome) === n); if (exact) return exact.id; const part = pool.find((c) => norm(c.nome) && (n.includes(norm(c.nome)) || norm(c.nome).includes(n))); return part ? part.id : ''; }; const loadMetaArray = (arr) => { if (!Array.isArray(arr) || arr.length === 0) { setMetaError('Nenhum dado válido encontrado.'); return false; } const detail = arr.map((r) => ({ campanha: String(r.campanha ?? r.campaign ?? '—'), date: String(r.data ?? r.date ?? '').slice(0, 10), gasto: parseBRL(r.gasto ?? r.spend ?? 0) })).filter((d) => d.gasto > 0 && d.date.length === 10); if (detail.length === 0) { setMetaError('Nenhum registro válido (esperado: campanha, data YYYY-MM-DD e gasto).'); return false; } const byName = {}; detail.forEach((d) => { byName[d.campanha] = (byName[d.campanha] || 0) + d.gasto; }); setMetaError(''); setMetaMsg(''); setMetaDetail(detail); setMetaRows(Object.keys(byName).map((nome) => ({ campanha: nome, gastoTotal: byName[nome], mapId: autoMatch(nome) || '__new__' }))); return true; }; const carregarColado = () => { setMetaMsg(''); try { const t = (metaPaste || '').trim(); const a = t.indexOf('['), b = t.lastIndexOf(']'); if (a === -1 || b === -1) throw new Error('sem json'); const arr = JSON.parse(t.slice(a, b + 1)); if (loadMetaArray(arr)) setMetaPaste(''); } catch { setMetaError('Não consegui ler o texto colado. Cole o JSON exatamente como recebeu.'); } }; const fetchMeta = async () => { setFetchingMeta(true); setMetaError(''); setMetaMsg(''); setMetaRows([]); setMetaDetail([]); try { const qs = new URLSearchParams({ from: per.from, to: per.to }); const res = await fetch(`/api/windsor-meta.php?${qs.toString()}`, { headers: { Accept: 'application/json' } }); const payload = await res.json().catch(() => ({})); if (!res.ok) throw new Error(payload.error || `Falha HTTP ${res.status}`); const arr = Array.isArray(payload) ? payload : (payload.data || []); if (!Array.isArray(arr) || arr.length === 0) { setMetaError('Nenhum gasto retornado para este período. Confirme no Windsor.ai se a conta do Meta está conectada e com dados nas datas escolhidas.'); } else { loadMetaArray(arr); } } catch (e) { setMetaError(e?.message || 'Não consegui puxar do Windsor.ai agora. Use a opção de colar o gasto abaixo ou tente novamente.'); } finally { setFetchingMeta(false); } }; const applyMeta = () => { let newCamps = [...campanhas]; const nameToCid = {}; metaRows.forEach((r) => { let cid = r.mapId; if (!cid) { nameToCid[r.campanha] = null; return; } if (cid === '__new__') { cid = uid(); newCamps.push({ id: cid, nome: r.campanha, lojaId: LOJA_PADRAO.id, plataforma: 'Facebook' }); } nameToCid[r.campanha] = cid; }); const touched = new Set(Object.values(nameToCid).filter(Boolean)); const mesesPuxados = new Set(metaDetail.map((d) => d.date.slice(0, 7))); // remove registros antigos (diários no intervalo ou mensais legado dos meses puxados) das campanhas atualizadas let newGastos = gastos.filter((g) => { if (!touched.has(g.campanhaId)) return true; if (g.date && g.date >= per.from && g.date <= per.to) return false; if (g.mes && mesesPuxados.has(g.mes)) return false; return true; }); let n = 0; metaDetail.forEach((d) => { const cid = nameToCid[d.campanha]; if (!cid) return; newGastos.push({ campanhaId: cid, date: d.date, valor: d.gasto }); n++; }); setCampanhas(newCamps); setGastos(newGastos); setMetaRows([]); setMetaDetail([]); setMetaMsg(n > 0 ? `Gasto aplicado (${n} registro(s)) ✓` : 'Nada aplicado — tudo estava como "Ignorar".'); }; /* ---------- dados calculados ---------- */ // Resolve a loja de uma venda: própria > da campanha > padrão. const campLojaMap = useMemo(() => new Map(campanhas.map((c) => [c.id, c.lojaId || LOJA_PADRAO.id])), [campanhas]); const lojaDaVenda = (v) => v.lojaId || campLojaMap.get(v.campanhaId) || LOJA_PADRAO.id; const campNoEscopo = (c) => lojaFiltro === 'todas' || (c.lojaId || LOJA_PADRAO.id) === lojaFiltro; const vendaNoEscopo = (v) => lojaFiltro === 'todas' || lojaDaVenda(v) === lojaFiltro; const emPeriodo = (v) => (v.data || '') >= per.from && (v.data || '') <= per.to; // Fontes já filtradas pela loja selecionada — todos os cálculos financeiros consomem estas. const campanhasEsc = useMemo(() => campanhas.filter(campNoEscopo), [campanhas, lojaFiltro]); const vendasEsc = useMemo(() => vendas.filter(vendaNoEscopo), [vendas, lojaFiltro, campLojaMap]); const vendasPer = useMemo(() => vendas.filter((v) => emPeriodo(v) && vendaNoEscopo(v)), [vendas, per, lojaFiltro, campLojaMap]); const gastosEsc = useMemo(() => { if (lojaFiltro === 'todas') return gastos; const ids = new Set(campanhasEsc.map((c) => c.id)); return gastos.filter((g) => ids.has(g.campanhaId)); }, [gastos, campanhasEsc, lojaFiltro]); const hasOrg = useMemo(() => vendas.some((v) => v.campanhaId === ORG), [vendas]); const nomeModelo = (id) => modelos.find((m) => m.id === id)?.nome || null; const custoTot = (v) => receitaFinanceiraVenda(v) - lucroBaseVenda(v); const secOrder = useMemo(() => { const saved = Array.isArray(config.secOrder) ? config.secOrder.filter((k) => DEFAULT_SEC.includes(k)) : []; return [...saved, ...DEFAULT_SEC.filter((k) => !saved.includes(k))]; }, [config.secOrder]); const visibleSecKeys = () => secOrder; const moveSection = (key, dir) => { const vis = visibleSecKeys(); const target = vis[vis.indexOf(key) + dir]; if (target == null) return; const order = secOrder.slice(); const a = order.indexOf(key), b = order.indexOf(target); [order[a], order[b]] = [order[b], order[a]]; setConfig((c) => ({ ...c, secOrder: order })); }; const secUp = (key) => (visibleSecKeys().indexOf(key) > 0 ? () => moveSection(key, -1) : null); const secDown = (key) => { const vis = visibleSecKeys(); return vis.indexOf(key) < vis.length - 1 ? () => moveSection(key, 1) : null; }; const moveCamp = (i, dir) => { const j = i + dir; if (j < 0 || j >= campanhas.length) return; setCampanhas((prev) => { const a = [...prev]; [a[i], a[j]] = [a[j], a[i]]; return a; }); }; const nomeVenda = (v) => { if (v.campanhaId === ORG) return `Orgânico · ${v.origem || 'Outro'}`; if (v.campanhaId === ADX) return 'Anúncio (não rastreado)'; return campanhas.find((c) => c.id === v.campanhaId)?.nome || '—'; }; const todasPendentes = vendas.filter((v) => vendaPendente(v) && vendaNoEscopo(v)).slice().sort((a, b) => { const da = dataISO(a.dataPrevista) || '9999-12-31'; const db = dataISO(b.dataPrevista) || '9999-12-31'; return da.localeCompare(db) || String(a.data || '').localeCompare(String(b.data || '')); }); // Toda a área de entregas acompanha o período principal escolhido no topo. // Para pedidos ainda pendentes, a referência é a DATA PREVISTA DE ENTREGA. // Derivado em cada render para nunca manter uma lista antiga depois de // cadastrar, editar ou sincronizar um pedido. const entregasPendentes = todasPendentes.filter((v) => vendaPendenteNoPeriodo(v, per.from, per.to) ); const abrirPendentesDoPeriodo = () => { window.setTimeout(() => entregasPendentesRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50); }; const abrirTodosPendentes = abrirPendentesDoPeriodo; const abrirPendentesPrevistosPeriodo = abrirPendentesDoPeriodo; const campDetalhe = (cid) => { const vs = vendasPer.filter((v) => v.campanhaId === cid && !vendaCancelada(v)); const byModel = {}; vs.forEach((v) => { const k = v.modeloId || '__sem__'; if (!byModel[k]) byModel[k] = { id: k, nome: v.modeloId ? (nomeModelo(v.modeloId) || 'Modelo removido') : 'Sem modelo', n: 0, receita: 0, custo: 0 }; byModel[k].n++; byModel[k].receita += receitaFinanceiraVenda(v); byModel[k].custo += custoTot(v); }); const modelosList = Object.values(byModel).map((m) => ({ ...m, lucro: m.receita - m.custo })).sort((a, b) => b.n - a.n); return { vs: vs.slice().sort((a, b) => (b.data > a.data ? 1 : -1)), modelosList }; }; const detalheNaoRastreado = expandedCamp === ADX ? campDetalhe(ADX) : null; const rows = useMemo(() => { const filt = campanhasEsc; const out = filt.map((c) => { const vs = vendasPer.filter((v) => v.campanhaId === c.id && !vendaCancelada(v)); const nPedidos = vs.length; const receita = vs.reduce((s, v) => s + receitaFinanceiraVenda(v), 0); const custoProd = vs.reduce((s, v) => s + custoTot(v), 0); const gastoMidia = getGastoPeriodo(c.id); const gasto = comImp(gastoMidia); const cpaPedido = nPedidos > 0 ? gasto / nPedidos : null; const ticket = nPedidos > 0 ? receita / nPedidos : null; const roas = gasto > 0 ? receita / gasto : null; const lucro = receita - custoProd - gasto; return { c, nPedidos, receita, custoProd, gastoMidia, gasto, cpaPedido, ticket, roas, lucro }; }); // O comparativo mostra somente anúncios que realmente tiveram gasto no // período selecionado. O cadastro continua preservado para outros períodos. const comGastoNoPeriodo = out.filter((r) => Number(r.gastoMidia) > 0.005); return ordenacao === 'manual' ? comGastoNoPeriodo : comGastoNoPeriodo.sort((a, b) => b.lucro - a.lucro); }, [campanhas, vendasPer, gastos, per, lojaFiltro, impPct, ordenacao, fgMovimentos]); const adReal = useMemo(() => { // Pedidos são atribuídos à data em que foram feitos. const pedidosTracked = vendasPer.filter((v) => v.campanhaId !== ORG && v.campanhaId !== ADX); const pedidosUntrackedLista = vendasPer.filter((v) => v.campanhaId === ADX); const pedidosAd = [...pedidosTracked, ...pedidosUntrackedLista]; // Entregas são atribuídas à data real da entrega, mesmo quando o pedido foi feito em outro dia. const entreguesPeriodo = vendasEsc.filter((v) => { if (!vendaEntregue(v) || v.campanhaId === ORG) return false; const dataEntrega = v.dataEntrega || ''; return dataEntrega && dataEntrega >= per.from && dataEntrega <= per.to; }); const trackedEntregues = entreguesPeriodo.filter((v) => v.campanhaId !== ADX); const untrackedEntregues = entreguesPeriodo.filter((v) => v.campanhaId === ADX); const ativos = pedidosAd.filter((v) => !vendaCancelada(v)); const gastoMidiaCalculado = campanhasEsc.reduce((s, c) => s + getGastoPeriodo(c.id), 0); const ajusteAds = ['emporio','luna'].includes(lojaFiltro) ? ajusteAdsPeriodoRegistro(lojaFiltro, per.from, per.to) : null; const gastoMidia = ajusteAds ? Math.max(0, Number(ajusteAds.valor) || 0) : gastoMidiaCalculado; const imposto = ajusteAds ? 0 : gastoMidiaCalculado * impPct / 100; const gastoTotal = ajusteAds ? gastoMidia : gastoMidia + imposto; const receita = entreguesPeriodo.reduce((s, v) => s + receitaFinanceiraVenda(v), 0); const custo = entreguesPeriodo.reduce((s, v) => s + custoTot(v), 0); const receitaPrevista = ativos.reduce((s, v) => s + receitaFinanceiraVenda(v), 0); const custoPrevisto = ativos.reduce((s, v) => s + custoTot(v), 0); const nPedidos = pedidosAd.length; const nEntregues = entreguesPeriodo.length; return { gastoMidia, imposto, gastoTotal, receita, custo, receitaPrevista, custoPrevisto, nPedidos, nEntregues, nPendentes: pedidosAd.filter(vendaPendente).length, nCancelados: pedidosAd.filter(vendaCancelada).length, nTracked: trackedEntregues.length, nUntracked: untrackedEntregues.length, cpaPedido: nPedidos > 0 ? gastoTotal / nPedidos : null, cpaEntrega: nEntregues > 0 ? gastoTotal / nEntregues : null, roas: gastoTotal > 0 ? receita / gastoTotal : null, lucro: receita - custo - gastoTotal, lucroPrevisto: receitaPrevista - custoPrevisto - gastoTotal, recUntracked: untrackedEntregues.reduce((s, v) => s + receitaFinanceiraVenda(v), 0), custoUntracked: untrackedEntregues.reduce((s, v) => s + custoTot(v), 0), pedidosUntracked: pedidosUntrackedLista.length, }; }, [vendas, vendasEsc, vendasPer, campanhasEsc, gastosEsc, per, impPct, lojaFiltro, fgMovimentos]); const adComparativo = useMemo(() => { const ajusteAds = ['emporio','luna'].includes(lojaFiltro) ? ajusteAdsPeriodoRegistro(lojaFiltro, per.from, per.to) : null; const campanhasComGasto = new Set(rows.map((r) => r.c.id)); // Sem ajuste geral, os totais acompanham exatamente os anúncios visíveis. // Quando existe um ajuste agregado da loja, não há rateio confiável por // campanha; nesse caso preservamos todos os pedidos rastreados no total. const tracked = vendasPer.filter((v) => v.campanhaId !== ORG && v.campanhaId !== ADX && !vendaCancelada(v) && (ajusteAds || campanhasComGasto.has(v.campanhaId))); const untracked = vendasPer.filter((v) => v.campanhaId === ADX && !vendaCancelada(v)); const ad = [...tracked, ...untracked]; const gastoMidiaCalculado = rows.reduce((s, r) => s + r.gastoMidia, 0); const gastoMidia = ajusteAds ? Math.max(0, Number(ajusteAds.valor) || 0) : gastoMidiaCalculado; const imposto = ajusteAds ? 0 : gastoMidiaCalculado * impPct / 100; const gastoTotal = ajusteAds ? gastoMidia : gastoMidia + imposto; const receita = ad.reduce((s, v) => s + receitaFinanceiraVenda(v), 0); const custo = ad.reduce((s, v) => s + custoTot(v), 0); const nPedidos = ad.length; return { gastoMidia, imposto, gastoTotal, receita, custo, nPedidos, cpa: nPedidos > 0 ? gastoTotal / nPedidos : null, ticket: nPedidos > 0 ? receita / nPedidos : null, roas: gastoTotal > 0 ? receita / gastoTotal : null, lucro: receita - custo - gastoTotal, recUntracked: untracked.reduce((s, v) => s + receitaFinanceiraVenda(v), 0), custoUntracked: untracked.reduce((s, v) => s + custoTot(v), 0), pedidosUntracked: untracked.length, }; }, [vendasPer, rows, per, impPct, lojaFiltro, fgMovimentos]); const trackedTot = useMemo(() => rows.reduce((s, r) => ({ gasto: s.gasto + r.gasto, receita: s.receita + r.receita, nPedidos: s.nPedidos + r.nPedidos, lucro: s.lucro + r.lucro, }), { gasto: 0, receita: 0, nPedidos: 0, lucro: 0 }), [rows]); const vendasOrg = useMemo(() => vendasPer.filter((v) => v.campanhaId === ORG), [vendasPer]); const orgResumo = useMemo(() => { const entregues = vendasEsc.filter((v) => { if (v.campanhaId !== ORG || !vendaEntregue(v)) return false; const dataEntrega = v.dataEntrega || ''; return dataEntrega && dataEntrega >= per.from && dataEntrega <= per.to; }); const ativos = vendasOrg.filter((v) => !vendaCancelada(v)); const receita = entregues.reduce((s, v) => s + receitaFinanceiraVenda(v), 0); const custo = entregues.reduce((s, v) => s + custoTot(v), 0); const receitaPrevista = ativos.reduce((s, v) => s + receitaFinanceiraVenda(v), 0); const custoPrevisto = ativos.reduce((s, v) => s + custoTot(v), 0); return { nPedidos: vendasOrg.length, nEntregues: entregues.length, nPendentes: vendasOrg.filter(vendaPendente).length, receita, custo, lucro: receita - custo, receitaPrevista, lucroPrevisto: receitaPrevista - custoPrevisto, }; }, [vendas, vendasEsc, vendasOrg, per, lojaFiltro, fgMovimentos]); const orgPorOrigem = useMemo(() => { const map = {}; vendasOrg.forEach((v) => { const k = v.origem || 'Outro'; if (!map[k]) map[k] = { origem: k, pedidos: 0, entregues: 0, receita: 0, custo: 0 }; map[k].pedidos++; if (vendaEntregue(v)) { map[k].entregues++; map[k].receita += receitaFinanceiraVenda(v); map[k].custo += custoTot(v); } }); return Object.values(map).map((o) => ({ ...o, lucro: o.receita - o.custo })).sort((a, b) => b.lucro - a.lucro); }, [vendasOrg, fgMovimentos]); const geral = useMemo(() => { const receitaPrevista = adReal.receitaPrevista + orgResumo.receitaPrevista; const receitaRealizada = adReal.receita + orgResumo.receita; const lucroPrevisto = adReal.lucroPrevisto + orgResumo.lucroPrevisto; const lucroRealizado = adReal.lucro + orgResumo.lucro; return { receitaPrevista, receitaRealizada, gastoTotal: adReal.gastoTotal, lucroPrevisto, lucroRealizado, pendentes: vendasEsc.filter((v) => vendaPendenteNoPeriodo(v, per.from, per.to)).length, totalPedidos: vendasPer.filter((v) => vendaEntregue(v) || vendaPendente(v)).length, share: receitaRealizada > 0 ? adReal.receita / receitaRealizada : null, }; }, [adReal, orgResumo, vendasPer, vendasEsc, per.from, per.to]); const modelosRank = useMemo(() => { const map = {}; // Mais vendidos deve representar os pedidos válidos feitos no período, // mesmo que ainda estejam confirmados ou pendentes. Cancelados e // devolvidos não entram no ranking. const pedidosValidos = vendasPer.filter((v) => !vendaCancelada(v)); pedidosValidos.forEach((v) => { if (!v.modeloId) return; if (!map[v.modeloId]) map[v.modeloId] = { n: 0, receita: 0, custo: 0 }; map[v.modeloId].n++; map[v.modeloId].receita += receitaFinanceiraVenda(v); map[v.modeloId].custo += custoTot(v); }); const semModelo = pedidosValidos.filter((v) => !v.modeloId).length; const list = modelos.map((m) => { const a = map[m.id] || { n: 0, receita: 0, custo: 0 }; return { m, n: a.n, receita: a.receita, custo: a.custo, lucro: a.receita - a.custo }; }) .sort((a, b) => b.n - a.n || b.receita - a.receita); return { list, semModelo }; }, [vendasPer, modelos, fgMovimentos]); const fornecedoresRank = useMemo(() => { const pedidosValidos = vendasPer.filter((v) => !vendaCancelada(v) && v.modeloId); const map = {}; pedidosValidos.forEach((v) => { const modelo = modelos.find((m) => m.id === v.modeloId); if (!modelo) return; const fornecedor = (modelo.fornecedor || 'Sem fornecedor').trim() || 'Sem fornecedor'; if (!map[fornecedor]) map[fornecedor] = { fornecedor, pedidos: 0, faturamento: 0, gasto: 0, modelos: {} }; const item = map[fornecedor]; item.pedidos += 1; item.faturamento += receitaFinanceiraVenda(v); // Gasto com o fornecedor considera somente o custo do produto. // O frete fica fora deste ranking, conforme solicitado. item.gasto += Number(v.custo) || 0; if (!item.modelos[modelo.id]) item.modelos[modelo.id] = { nome: modelo.nome, n: 0, faturamento: 0 }; item.modelos[modelo.id].n += 1; item.modelos[modelo.id].faturamento += receitaFinanceiraVenda(v); }); return Object.values(map) .map((x) => ({ ...x, lucro: x.faturamento - x.gasto, modelosRank: Object.values(x.modelos).sort((a, b) => b.n - a.n || b.faturamento - a.faturamento), })) .sort((a, b) => b.faturamento - a.faturamento || b.pedidos - a.pedidos); }, [vendasPer, modelos, fgMovimentos]); const recentesAba = useMemo(() => { const src = aba === 'organico' ? vendasOrg : vendasPer.filter((v) => v.campanhaId !== ORG); return src.slice().sort((a, b) => (b.data > a.data ? 1 : b.data < a.data ? -1 : 0)).slice(0, 100); }, [aba, vendasPer, vendasOrg]); // O histórico do bloco de pedidos usa todos os pedidos de anúncio. // A data é aplicada depois conforme a aba: pedido, entrega ou confirmação. // Isso evita perder entregas feitas no dia quando o pedido foi criado em outra data. const pedidosAnuncioBase = useMemo(() => { const dataRef = (v) => pedidosModoData === 'entregue' ? (v.dataEntrega || v.data || '') : (v.data || ''); return vendas .filter((v) => vendaNoEscopo(v) && v.campanhaId !== ORG) .slice() .sort((a, b) => dataRef(b).localeCompare(dataRef(a))) .slice(0, 500); }, [vendas, pedidosModoData, lojaFiltro, campLojaMap]); // A lista de pedidos usa sempre o mesmo período principal do topo. // O modo abaixo define apenas qual data será comparada (pedido/entrega/confirmação). const pedidosRange = useMemo(() => ({ from: per.from, to: per.to }), [per.from, per.to]); const recentesFiltradas = useMemo(() => { const q = norm(vendasBusca); const { from, to } = pedidosRange; return pedidosAnuncioBase.filter((v) => { const stRaw = statusVenda(v); if (pedidosModoData === 'entregue' && stRaw !== 'entregue') return false; if (pedidosModoData === 'confirmado' && stRaw !== 'confirmado') return false; const dataRef = pedidosModoData === 'entregue' ? (v.dataEntrega || '') : (v.data || ''); if (from && (!dataRef || dataRef < from)) return false; if (to && (!dataRef || dataRef > to)) return false; if (!q) return true; const cliente = norm(v.cliente || ''); const modelo = norm(nomeModelo(v.modeloId) || ''); const anuncio = norm(nomeVenda(v) || ''); const status = norm(STATUS_INFO[stRaw]?.label || ''); return cliente.includes(q) || modelo.includes(q) || anuncio.includes(q) || status.includes(q); }); }, [pedidosAnuncioBase, vendasBusca, pedidosDataFiltro, pedidosModoData, pedidosRange, campanhas, modelos]); const despesaNoDia = (d, dia) => { if (!d || !dia) return 0; if ((d.recorrencia || 'nao') !== 'mensal') return d.data === dia ? (Number(d.valor) || 0) : 0; if (!d.data || dia < d.data) return 0; const originalDay = Number(d.data.slice(8, 10)) || 1; const ym = dia.slice(0, 7); const diaAlvo = Math.min(originalDay, Number(lastDay(ym))); return Number(dia.slice(8, 10)) === diaAlvo ? (Number(d.valor) || 0) : 0; }; const gastoAdsDia = (dia) => { const lojasAlvo = lojaFiltro === 'todas' ? lojas.map((l) => l.id) : [lojaFiltro]; return lojasAlvo.reduce((total, lojaId) => { const ajuste = ['emporio','luna'].includes(lojaId) ? ajusteAdsPeriodoRegistro(lojaId, per.from, per.to) : null; // Um total digitado para o período é aplicado uma única vez no último dia do intervalo, // apenas para fechar corretamente o total. Ele não é inventado/distribuído pelos dias. if (ajuste) return total + (dia === per.to ? Math.max(0, Number(ajuste.valor) || 0) : 0); const campIds = new Set(campanhas.filter((c) => (c.lojaId || LOJA_PADRAO.id) === lojaId).map((c) => c.id)); const midia = gastos.filter((g) => g.date === dia && campIds.has(g.campanhaId)).reduce((s, g) => s + (Number(g.valor) || 0), 0); const manual = adsManuais.filter((a) => a.origem !== 'ajuste_periodo' && a.data === dia && a.lojaId === lojaId).reduce((s,a) => s + (Number(a.valor) || 0), 0); return total + comImp(midia) + manual; }, 0); }; useEffect(() => { if (!['emporio','luna'].includes(lojaFiltro)) { setAjustePeriodoAdsInput(''); setAjustePeriodoVendedorInput(''); setAjustePeriodoMsg(''); return; } const a = ajusteAdsPeriodoRegistro(lojaFiltro, per.from, per.to); const p = ajusteVendedorPeriodoRegistro(lojaFiltro, per.from, per.to); setAjustePeriodoAdsInput(a ? num(Number(a.valor) || 0, 2) : ''); setAjustePeriodoVendedorInput(p ? num(Number(p.valor) || 0, 2) : ''); setAjustePeriodoMsg(''); }, [lojaFiltro, per.from, per.to, adsManuais, pagamentos]); // Custos operacionais do dia que ENTRAM no lucro: // - "Todas as lojas": todos os custos (específicos + gerais), uma única vez. // - Loja individual: apenas os custos específicos daquela loja. Os gerais NÃO são descontados // da loja (aparecem separados como "Custos gerais não alocados"). Sem rateio automático. const custosOpNoDia = (dia) => despesas.reduce((s, d) => { const base = despesaNoDia(d, dia); if (!base) return s; if (lojaFiltro === 'todas') return s + base; return (d.lojaId || 'geral') === lojaFiltro ? s + base : s; }, 0); // Custos gerais (não alocados a uma loja) no dia — usados só para exibição na visão de loja individual. const custosGeraisNoDia = (dia) => despesas.reduce((s, d) => { if ((d.lojaId || 'geral') !== 'geral') return s; return s + despesaNoDia(d, dia); }, 0); const financeiro = useMemo(() => { const dias = diasEntre(per.from, per.to); const pedidosBase = vendasEsc.filter((v) => !vendaCancelada(v) && (financeiroView === 'comercial' ? (v.data >= per.from && v.data <= per.to) : (vendaEntregue(v) && v.dataEntrega && v.dataEntrega >= per.from && v.dataEntrega <= per.to))); const rows = dias.map((dia) => { const pedidosDiaTodos = pedidosBase.filter((v) => (financeiroView === 'comercial' ? v.data : v.dataEntrega) === dia); // Faturamento mostra apenas o dinheiro que deve entrar na própria loja. const pedidosDiaFaturamento = pedidosDiaTodos.filter(vendaEntraFinanceiroLoja); const faturamento = pedidosDiaFaturamento.reduce((s, v) => s + receitaFinanceiraVenda(v), 0); const receitaBaseLucro = pedidosDiaTodos.reduce((s, v) => s + receitaFinanceiraVenda(v), 0); // Lucro Loja, por outro lado, considera TODOS os pedidos válidos. Na Luna isso inclui // os pedidos entregues pelo FG, mesmo quando ficam fora do faturamento financeiro. const taxasCartao = pedidosDiaTodos.reduce((s, v) => s + taxaCartaoValorVenda(v), 0); const fornecedor = pedidosDiaTodos.reduce((s, v) => s + (Number(v.custo) || 0), 0); const frete = pedidosDiaTodos.reduce((s, v) => s + (Number(v.frete) || 0), 0); const vendedorDevido = pedidosDiaTodos.reduce((s, v) => s + vendedorDevidoVenda(v), 0); const vendedorPago = vendedorPagoNoPeriodo(dia, dia); const creditoFg = pedidosDiaTodos.filter(vendaEntreguePeloFgOperacao).reduce((s,v)=>s+Math.max(0,lucroBaseVenda(v)),0); const ads = gastoAdsDia(dia); const custosOp = custosOpNoDia(dia); const margemCalculada = receitaBaseLucro - fornecedor - frete; const margemPedidos = pedidosDiaTodos.reduce((s, v) => s + lucroBaseVenda(v), 0); const ajusteFgManual = margemPedidos - margemCalculada; const lucroLoja = margemPedidos - ads - custosOp; const resultadoAposVendedor = lucroLoja - vendedorPago; const dinheiro = pedidosDiaFaturamento.filter((v) => v.formaPagamento === 'dinheiro').reduce((s, v) => s + receitaFinanceiraVenda(v), 0); return { dia, pedidos: pedidosDiaTodos.length, pedidosFaturamento: pedidosDiaFaturamento.length, faturamento, receitaBaseLucro, taxasCartao, fornecedor, frete, vendedorDevido, vendedorPago, creditoFg, ajusteFgManual, ads, custosOp, lucroLoja, lucro: lucroLoja, resultadoAposVendedor, dinheiro }; }); const total = rows.reduce((a, r) => ({ pedidos: a.pedidos + r.pedidos, pedidosFaturamento: a.pedidosFaturamento + (r.pedidosFaturamento || 0), faturamento: a.faturamento + r.faturamento, receitaBaseLucro: a.receitaBaseLucro + (r.receitaBaseLucro || 0), taxasCartao: a.taxasCartao + r.taxasCartao, fornecedor: a.fornecedor + r.fornecedor, frete: a.frete + r.frete, vendedorDevido: a.vendedorDevido + r.vendedorDevido, vendedorPago: a.vendedorPago + r.vendedorPago, creditoFg: a.creditoFg + r.creditoFg, ajusteFgManual: a.ajusteFgManual + (r.ajusteFgManual || 0), ads: a.ads + r.ads, custosOp: a.custosOp + r.custosOp, lucroLoja: a.lucroLoja + r.lucroLoja, lucro: a.lucro + r.lucro, resultadoAposVendedor: a.resultadoAposVendedor + r.resultadoAposVendedor, dinheiro: a.dinheiro + r.dinheiro, }), { pedidos: 0, pedidosFaturamento: 0, faturamento: 0, receitaBaseLucro: 0, taxasCartao: 0, fornecedor: 0, frete: 0, vendedorDevido: 0, vendedorPago: 0, creditoFg: 0, ajusteFgManual: 0, ads: 0, custosOp: 0, lucroLoja: 0, lucro: 0, resultadoAposVendedor: 0, dinheiro: 0 }); const formas = {}; pedidosBase.filter(vendaEntraFinanceiroLoja).forEach((v) => { const k = v.formaPagamento || 'nao_informado'; formas[k] = (formas[k] || 0) + receitaFinanceiraVenda(v); }); // Custos gerais do período (só para exibir separado quando uma loja específica está selecionada). const custosGerais = lojaFiltro === 'todas' ? 0 : dias.reduce((s, dia) => s + custosGeraisNoDia(dia), 0); return { rows, total, formas, custosGerais }; }, [vendas, vendasEsc, gastosEsc, despesas, adsManuais, pagamentos, modelos, per, financeiroView, impPct, lojaFiltro, fgMovimentos]); const margemFinanceira = financeiro.total.receitaBaseLucro > 0 ? financeiro.total.lucro / financeiro.total.receitaBaseLucro : 0; const margemAposVendedor = financeiro.total.receitaBaseLucro > 0 ? financeiro.total.resultadoAposVendedor / financeiro.total.receitaBaseLucro : 0; const ticketFinanceiro = financeiro.total.pedidosFaturamento > 0 ? financeiro.total.faturamento / financeiro.total.pedidosFaturamento : 0; const pctAds = financeiro.total.faturamento > 0 ? financeiro.total.ads / financeiro.total.faturamento : 0; const pctFornecedor = financeiro.total.faturamento > 0 ? financeiro.total.fornecedor / financeiro.total.faturamento : 0; const formasLabel = { pix: 'Pix', dinheiro: 'Dinheiro', debito: 'Débito', credito: 'Crédito', nao_informado: 'Não informado' }; // Resumo financeiro principal: deixa explícito de onde vem o lucro. // Ads já entram com o imposto configurado e são descontados UMA única vez. const resumoOperacional = useMemo(() => { const dias = diasEntre(per.from, per.to); const ads = dias.reduce((s, dia) => s + gastoAdsDia(dia), 0); const custosOp = dias.reduce((s, dia) => s + custosOpNoDia(dia), 0); const custosGerais = lojaFiltro === 'todas' ? 0 : dias.reduce((s, dia) => s + custosGeraisNoDia(dia), 0); const montar = (tipo) => { const baseTodos = vendasEsc.filter((v) => { if (vendaCancelada(v)) return false; if (tipo === 'realizado') return vendaEntregue(v) && v.dataEntrega && v.dataEntrega >= per.from && v.dataEntrega <= per.to; return v.data && v.data >= per.from && v.data <= per.to; }); const baseFaturamento = baseTodos.filter(vendaEntraFinanceiroLoja); const faturamento = baseFaturamento.reduce((s, v) => s + receitaFinanceiraVenda(v), 0); // O lucro é calculado sobre todos os pedidos, inclusive Luna/FG que não entram no faturamento. const receitaBaseLucro = baseTodos.reduce((s, v) => s + receitaFinanceiraVenda(v), 0); const taxasCartao = baseTodos.reduce((s, v) => s + taxaCartaoValorVenda(v), 0); const fornecedor = baseTodos.reduce((s, v) => s + (Number(v.custo) || 0), 0); const frete = baseTodos.reduce((s, v) => s + (Number(v.frete) || 0), 0); const vendedorDevido = baseTodos.reduce((s, v) => s + vendedorDevidoVenda(v), 0); const vendedorPago = vendedorPagoNoPeriodo(per.from, per.to); const creditoFg = baseTodos.filter(vendaEntreguePeloFgOperacao).reduce((s,v)=>s+Math.max(0,lucroBaseVenda(v)),0); const margemCalculada = receitaBaseLucro - fornecedor - frete; const margemAntesAds = baseTodos.reduce((s, v) => s + lucroBaseVenda(v), 0); const ajusteFgManual = margemAntesAds - margemCalculada; const lucroLoja = margemAntesAds - ads - custosOp; const resultadoAposVendedor = lucroLoja - vendedorPago; const margem = receitaBaseLucro > 0 ? lucroLoja / receitaBaseLucro : 0; const margemAposVendedor = receitaBaseLucro > 0 ? resultadoAposVendedor / receitaBaseLucro : 0; return { faturamento, receitaBaseLucro, taxasCartao, fornecedor, frete, vendedorDevido, vendedorPago, creditoFg, margemAntesAds, ajusteFgManual, ads, custosOp, lucroLoja, lucro: lucroLoja, resultadoAposVendedor, margem, margemAposVendedor, pedidos: baseTodos.length, pedidosFg: baseTodos.filter(vendaEntreguePeloFgOperacao).length, pedidosForaFaturamento: baseTodos.length-baseFaturamento.length }; }; return { comercial: montar('comercial'), realizado: montar('realizado'), custosGerais }; }, [vendas, vendasEsc, gastosEsc, despesas, adsManuais, pagamentos, modelos, per, impPct, lojaFiltro, fgMovimentos]); // Previsão usa a DATA PREVISTA DE ENTREGA dos pedidos ainda não entregues/cancelados. // Ads futuros não são projetados: o lucro previsto abaixo representa a margem conhecida // depois de fornecedor, frete e custos operacionais já cadastrados/recorrentes. const previsaoPeriodo = useMemo(() => { const baseTodos = vendasEsc.filter((v) => { if (vendaCancelada(v) || vendaEntregue(v)) return false; return vendaPendenteNoPeriodo(v, per.from, per.to); }); const baseFaturamento = baseTodos.filter(vendaEntraFinanceiroLoja); const faturamento = baseFaturamento.reduce((s, v) => s + receitaFinanceiraVenda(v), 0); const receitaBaseLucro = baseTodos.reduce((s, v) => s + receitaFinanceiraVenda(v), 0); const taxasCartao = baseTodos.reduce((s, v) => s + taxaCartaoValorVenda(v), 0); const fornecedor = baseTodos.reduce((s, v) => s + (Number(v.custo) || 0), 0); const frete = baseTodos.reduce((s, v) => s + (Number(v.frete) || 0), 0); const vendedorPrevisto = baseTodos.reduce((s, v) => s + vendedorDevidoVenda(v), 0); const dias = diasEntre(per.from, per.to); const custosOp = dias.reduce((s, dia) => s + custosOpNoDia(dia), 0); const margemAntesAds = receitaBaseLucro - fornecedor - frete; const lucroPrevisto = margemAntesAds - custosOp; const resultadoAposVendedorPrevisto = lucroPrevisto - vendedorPrevisto; const margem = receitaBaseLucro > 0 ? lucroPrevisto / receitaBaseLucro : 0; const margemAposVendedor = receitaBaseLucro > 0 ? resultadoAposVendedorPrevisto / receitaBaseLucro : 0; const creditoFgPrevisto = baseTodos.filter(vendaEntreguePeloFgOperacao).reduce((s,v)=>s+Math.max(0,lucroBaseVenda(v)),0); return { pedidos: baseTodos.length, pedidosFg: baseTodos.filter(vendaEntreguePeloFgOperacao).length, pedidosForaFaturamento: baseTodos.length-baseFaturamento.length, faturamento, receitaBaseLucro, taxasCartao, fornecedor, frete, vendedorPrevisto, custosOp, margemAntesAds, lucroPrevisto, resultadoAposVendedorPrevisto, margem, margemAposVendedor, creditoFgPrevisto }; }, [vendas, vendasEsc, despesas, modelos, per, lojaFiltro]); const periodoFuturo = per.from > todayISO(); const historicoPeriodo = useMemo(() => { const base = historico.filter((h) => h.data >= per.from && h.data <= per.to && (lojaFiltro === 'todas' || h.lojaId === lojaFiltro)); return base.reduce((a,h) => ({ dias:a.dias+1, lucroBase:a.lucroBase+(Number(h.lucroBase)||0), ads:a.ads+(Number(h.ads)||0), vendedor:a.vendedor+(Number(h.vendedor)||0), resultado:a.resultado+(Number(h.resultado)||0) }), { dias:0,lucroBase:0,ads:0,vendedor:0,resultado:0 }); }, [historico, per, lojaFiltro]); const contasAPagar = useMemo(() => { const base = vendas.filter((v) => !vendaCancelada(v) && vendaEntregue(v) && ['emporio','luna'].includes(v.lojaId) && (lojaFiltro === 'todas' || v.lojaId === lojaFiltro)); const due = { emporio:{vendedor:0,freteiro:0}, luna:{vendedor:0,freteiro:0} }; base.forEach((v) => { due[v.lojaId].vendedor += vendedorDevidoVenda(v); if (!vendaEntreguePeloFgOperacao(v)) due[v.lojaId].freteiro += Number(v.frete)||0; }); const paid = { emporio:{vendedor:0,freteiro:0}, luna:{vendedor:0,freteiro:0} }; ['emporio','luna'].forEach((lojaId) => { const ajustesVend = pagamentos.filter((x) => x.origem === 'ajuste_periodo' && x.lojaId === lojaId && x.tipo === 'vendedor'); const vendedorRegular = pagamentos.filter((x) => x.origem !== 'ajuste_periodo' && x.lojaId === lojaId && x.tipo === 'vendedor') .filter((x) => !ajustesVend.some((a) => x.data >= a.periodoFrom && x.data <= a.periodoTo)) .reduce((s,x) => s + (Number(x.valor)||0), 0); paid[lojaId].vendedor = vendedorRegular + ajustesVend.reduce((s,x)=>s+(Number(x.valor)||0),0); paid[lojaId].freteiro = pagamentos.filter((x) => x.lojaId === lojaId && x.tipo === 'freteiro').reduce((s,x)=>s+(Number(x.valor)||0),0); }); return { due, paid }; }, [vendas, pagamentos, modelos, lojaFiltro, fgMovimentos]); // Acertos e fechamento FG seguem o mesmo período principal do topo. const semanaAcerto = useMemo(() => ({ from: per.from, to: per.to }), [per.from, per.to]); const candidatosAcerto = useMemo(() => vendas.filter((v) => { if (!vendaEntregue(v) || !v.dataEntrega || v.dataEntrega < semanaAcerto.from || v.dataEntrega > semanaAcerto.to) return false; if (lojaFiltro !== 'todas' && (v.lojaId || LOJA_PADRAO.id) !== lojaFiltro) return false; // Mostra automaticamente lojas com vendedor calculado; pedidos já marcados continuam visíveis mesmo se o valor for zero. return !!v.acertoAtivo || !!vendedorNomeVenda(v) || vendedorDevidoVenda(v) !== 0; }).sort((a,b) => String(a.dataEntrega).localeCompare(String(b.dataEntrega)) || String(a.cliente||'').localeCompare(String(b.cliente||''))), [vendas, semanaAcerto, lojaFiltro, modelos, fgMovimentos]); const acertosSemana = useMemo(() => candidatosAcerto.filter((v) => v.acertoAtivo), [candidatosAcerto]); const acertosSemanaVisiveis = useMemo(() => acertosSemana.filter((v) => !acertoSomentePendentes || !acertoPagoVenda(v)), [acertosSemana, acertoSomentePendentes]); const acertoResumo = useMemo(() => { const selecionados = acertosSemana.length; const total = acertosSemana.reduce((a,v)=>a+acertoValorVenda(v),0); const pagos = acertosSemana.filter(acertoPagoVenda); const pendentes = acertosSemana.filter((v)=>!acertoPagoVenda(v)); const pendentesAcumulados = vendas.filter((v) => v.acertoAtivo && !acertoPagoVenda(v) && (lojaFiltro==='todas'||(v.lojaId||LOJA_PADRAO.id)===lojaFiltro)); return { selecionados, total, pagos:pagos.reduce((a,v)=>a+acertoValorVenda(v),0), pendentes:pendentes.reduce((a,v)=>a+acertoValorVenda(v),0), qtdPendentes:pendentes.length, acumulado:pendentesAcumulados.reduce((a,v)=>a+acertoValorVenda(v),0), qtdAcumulado:pendentesAcumulados.length }; }, [acertosSemana, vendas, lojaFiltro, modelos, fgMovimentos]); const gruposAcerto = useMemo(() => { const map = new Map(); acertosSemanaVisiveis.forEach((v) => { const lojaId = v.lojaId || LOJA_PADRAO.id; const pessoa = acertoRecebedorVenda(v); const tipo = acertoTipoVenda(v); const key = `${lojaId}|${tipo}|${pessoa}`; if (!map.has(key)) map.set(key, { key, lojaId, pessoa, tipo, vendas:[] }); map.get(key).vendas.push(v); }); return [...map.values()].map((g) => ({ ...g, vendas:g.vendas.sort((a,b)=>String(a.dataEntrega).localeCompare(String(b.dataEntrega)) || String(a.cliente||'').localeCompare(String(b.cliente||'')) ) })); }, [acertosSemanaVisiveis]); const textoGrupoAcerto = (g, somenteDia = '') => { const lista = g.vendas.filter((v) => !somenteDia || v.dataEntrega === somenteDia); const dias = [...new Set(lista.map((v)=>v.dataEntrega))].sort(); const blocos = dias.map((dia) => { const vs = lista.filter((v)=>v.dataEntrega===dia); const linhas = vs.map((v,i)=>`${i+1}. ${v.cliente || 'Sem nome'} - ${brlMsg(acertoValorVenda(v))}`); const totalDia = vs.reduce((a,v)=>a+acertoValorVenda(v),0); return `📦 ${nomeLoja(g.lojaId)} — ${diaSemanaPt(dia)}, ${brDateShort(dia)}\n${linhas.join('\n')}\nTotal do dia: ${brlMsg(totalDia)}`; }); const total = lista.reduce((a,v)=>a+acertoValorVenda(v),0); if (somenteDia) return blocos.join('\n\n'); return `${blocos.join('\n\n')}\n\nTotal do período: ${brlMsg(total)}`; }; // Acerto compartilhado com FG. Entram as entregas da Luna feitas pelo FG e // as camas FG da Empório cujo entregador também foi FG. O crédito padrão é o // lucro oficial da venda. Quando ajustado manualmente, o valor corrigido passa a ser a referência do pedido em todo o painel. const fgVendasSemana = useMemo(() => vendas.filter((v) => !vendaCancelada(v) && vendaEntregue(v) && vendaEntreguePeloFgOperacao(v) && v.dataEntrega >= semanaAcerto.from && v.dataEntrega <= semanaAcerto.to ).sort((a,b)=>String(a.dataEntrega).localeCompare(String(b.dataEntrega)) || String(a.cliente||'').localeCompare(String(b.cliente||''))), [vendas, semanaAcerto]); // Retiradas manuais continuam disponíveis para estoque/ajustes que não vieram de um pedido. const fgRetiradasSemana = useMemo(() => fgMovimentos.filter((x) => x.tipo === 'retirada' && x.data >= semanaAcerto.from && x.data <= semanaAcerto.to) .sort((a,b)=>String(a.data).localeCompare(String(b.data)) || String(a.descricao||'').localeCompare(String(b.descricao||''))), [fgMovimentos, semanaAcerto]); // Se a mercadoria do FG foi entregue por outro entregador, ela necessariamente saiu do FG. // Luna: qualquer pedido entregue por Rogério/Tarcísio/Goldstar gera retirada automática. // Empório: a mesma regra vale somente para as camas FG. Entregador = FG NÃO gera retirada, // porque nesse caso o pedido entra como crédito de entrega do próprio FG. const retiradaAutomaticaFgVenda = (v) => { if (vendaCancelada(v) || !vendaEntregue(v) || !v.dataEntrega) return null; const lojaId = v.lojaId || LOJA_PADRAO.id; let entregador = ''; if (lojaId === 'luna') entregador = entregadorLunaVenda(v); else if (lojaId === 'emporio' && ehCamaFgEmporio(v)) entregador = entregadorEmporioCamaVenda(v); else return null; if (!entregador || entregador === 'FG') return null; const m = modeloPorId(v.modeloId); const qtd = Math.max(1, Number(v.qtdItens) || 1); const regra = lojaId === 'luna' ? (regraModeloLuna(m?.nome) || LUNA_MODELOS.find((x)=>x.id===v.modeloId)) : (regraModeloEmporio(m?.nome) || EMPORIO_CAMA_FG_MODELOS.find((x)=>x.id===v.modeloId)); let custoUnitario = Number(regra?.custo) || Number(m?.custo) || 0; // Modelos com custo ainda não cadastrado (ex.: algum FG novo) usam o custo informado no pedido. if (!(custoUnitario > 0) && Number(v.custo) > 0) custoUnitario = (Number(v.custo) || 0) / qtd; const valor = Math.max(0, custoUnitario * qtd); if (!(valor > 0)) return null; const nome = String(m?.nome || regra?.nome || 'Produto FG').trim(); const n = normalizarNomeModelo(nome); let item = nome; if (n.includes('cama solteiro') && n.includes('auxiliar') && n.includes('7')) item = 'solteiro com auxiliar 7cm'; else if (n.includes('cama solteiro') && n.includes('7')) item = 'solteiro 7cm'; else if (n.includes('cama casal') && n.includes('14')) item = 'casal 14cm'; else if (n.includes('cama casal') && n.includes('12')) item = 'casal 12cm'; else if (n.includes('cama casal') && n.includes('7')) item = 'casal 7cm'; return { id:`auto-ret-${v.id}`, tipo:'retirada_auto', automatico:true, vendaId:v.id, data:v.dataEntrega, descricao:`${qtd} ${item}`, valor, lojaId, entregador, cliente:v.cliente || 'Sem nome' }; }; const fgRetiradasAutomaticasSemana = useMemo(() => vendas .map(retiradaAutomaticaFgVenda) .filter((x) => x && x.data >= semanaAcerto.from && x.data <= semanaAcerto.to) .sort((a,b)=>String(a.data).localeCompare(String(b.data)) || String(a.descricao||'').localeCompare(String(b.descricao||''))), [vendas, modelos, semanaAcerto]); const fgRetiradasTodasSemana = useMemo(() => [...fgRetiradasSemana, ...fgRetiradasAutomaticasSemana] .sort((a,b)=>String(a.data).localeCompare(String(b.data)) || String(a.descricao||'').localeCompare(String(b.descricao||''))), [fgRetiradasSemana, fgRetiradasAutomaticasSemana]); const fgSaldoAnteriorRec = useMemo(() => fgMovimentos.find((x) => x.tipo === 'saldo_anterior' && x.semana === semanaAcerto.from) || null, [fgMovimentos, semanaAcerto]); const fgSaldoAnterior = Number(fgSaldoAnteriorRec?.valor) || 0; const fgCreditoOverride = (vendaId) => fgMovimentos.find((x)=>x.tipo==='credito_entrega_override' && x.vendaId===vendaId) || null; const creditoFgVenda = (v) => { const over = fgCreditoOverride(v.id); return over && Number.isFinite(Number(over.valor)) ? Math.max(0, Number(over.valor)||0) : Math.max(0,lucroBaseVendaCalculado(v)); }; const setCreditoFgVenda = (v, raw) => { const texto = String(raw ?? '').trim(); setFgMovimentos((prev)=>{ const sem = prev.filter((x)=>!(x.tipo==='credito_entrega_override' && x.vendaId===v.id)); if (texto === '') return sem; const valor = Math.max(0, parseBRL(texto)); const atual = prev.find((x)=>x.tipo==='credito_entrega_override' && x.vendaId===v.id); return [...sem,{ id:atual?.id || uid(), tipo:'credito_entrega_override', vendaId:v.id, data:v.dataEntrega || todayISO(), descricao:`Crédito entrega FG · ${v.cliente || 'Sem nome'}`, valor }]; }); }; const updateMovimentoFg = (id, patch) => setFgMovimentos((prev)=>prev.map((x)=>x.id===id?{...x,...patch}:x)); const fgTotalRetirado = fgRetiradasTodasSemana.reduce((s,x)=>s+(Number(x.valor)||0),0); const fgTotalEntregue = fgVendasSemana.reduce((s,v)=>s+creditoFgVenda(v),0); const fgSaldoFinal = fgTotalRetirado - fgTotalEntregue - fgSaldoAnterior; useEffect(() => { setFgSaldoAnteriorInput(fgSaldoAnterior ? String(fgSaldoAnterior) : ''); }, [semanaAcerto.from, fgSaldoAnteriorRec?.id, fgSaldoAnteriorRec?.valor]); const salvarSaldoAnteriorFg = () => { const valor = parseBRL(fgSaldoAnteriorInput); setFgMovimentos((prev) => { const sem = prev.filter((x) => !(x.tipo === 'saldo_anterior' && x.semana === semanaAcerto.from)); return valor > 0 ? [...sem, { id: fgSaldoAnteriorRec?.id || uid(), tipo:'saldo_anterior', semana:semanaAcerto.from, data:semanaAcerto.from, descricao:'Saldo anterior com FG', valor }] : sem; }); }; const addRetiradaFg = () => { const valor = parseBRL(fgRetiradaValor); if (!fgRetiradaData || !fgRetiradaDescricao.trim() || valor <= 0) return; setFgMovimentos((prev)=>[...prev,{ id:uid(), tipo:'retirada', data:fgRetiradaData, descricao:fgRetiradaDescricao.trim(), valor }]); setFgRetiradaDescricao(''); setFgRetiradaValor(''); }; const delMovimentoFg = (id) => setFgMovimentos((prev)=>prev.filter((x)=>x.id!==id)); const textoAcertoFg = () => { const dias = [...new Set([...fgVendasSemana.map((v)=>v.dataEntrega), ...fgRetiradasTodasSemana.map((x)=>x.data)])].sort(); const blocos = dias.map((dia) => { const vendasDia = fgVendasSemana.filter((v)=>v.dataEntrega===dia); const retiradasDia = fgRetiradasTodasSemana.filter((x)=>x.data===dia); const partes = [`${diaSemanaPt(dia)} ${brDateShort(dia)}`]; if (vendasDia.length) { partes.push('Entregue'); vendasDia.forEach((v)=>{ const p=prefixoCep(v.cep); partes.push(`${p===null?'SEM CEP':String(p).padStart(3,'0')} - ${brlMsg(creditoFgVenda(v))}`); }); if (vendasDia.length>1) partes.push(`Total entregue: ${brlMsg(vendasDia.reduce((a,v)=>a+creditoFgVenda(v),0))}`); } if (retiradasDia.length) { partes.push('Retirado'); retiradasDia.forEach((x)=>partes.push(`${x.descricao} - ${brlMsg(x.valor)}`)); } return partes.join('\n'); }); const resultado = fgSaldoFinal > 0.005 ? `${brlMsg(fgSaldoFinal)} a pagar ao FG` : fgSaldoFinal < -0.005 ? `${brlMsg(Math.abs(fgSaldoFinal))} com FG` : 'Acerto zerado'; const formula = `${brlMsg(fgTotalRetirado)} - ${brlMsg(fgTotalEntregue)} - ${brlMsg(fgSaldoAnterior)} = ${brlMsg(fgSaldoFinal)}`; return `FG -\n\n${fgSaldoAnterior>0?`${brlMsg(fgSaldoAnterior)} com FG (saldo anterior ao período)\n\n`:''}${blocos.join('\n\n')}\n\nTotal\n\nRetirado - ${brlMsg(fgTotalRetirado)}\nEntregue - ${brlMsg(fgTotalEntregue)}\nSaldo anterior - ${brlMsg(fgSaldoAnterior)}\n\n${formula}\n\n${resultado}`; }; // Fechamento Luna também segue o período global. Em um único dia mantém o formato curto; // em períodos maiores, agrupa por dia e inclui o total do período. const lunaFechamentoVendas = useMemo(() => vendas.filter((v) => !vendaCancelada(v) && vendaEntregue(v) && (v.lojaId || LOJA_PADRAO.id) === 'luna' && v.dataEntrega && v.dataEntrega >= per.from && v.dataEntrega <= per.to ).sort((a,b) => String(a.dataEntrega || '').localeCompare(String(b.dataEntrega || '')) || String(a.cliente || '').localeCompare(String(b.cliente || ''))), [vendas, per.from, per.to]); // As camas da Empório entregues por Tarcísio também pertencem ao fechamento // de fretes dele. Elas entram somente na conta de frete, sem alterar lucro, // faturamento ou o acerto do Douglas da Luna Comfort. const emporioCamasTarcisioFretes = useMemo(() => vendas.filter((v) => !vendaCancelada(v) && vendaEntregue(v) && ehCamaFgEmporio(v) && entregadorEmporioCamaVenda(v) === 'Tarcísio' && v.dataEntrega && v.dataEntrega >= per.from && v.dataEntrega <= per.to ).sort((a,b) => String(a.dataEntrega || '').localeCompare(String(b.dataEntrega || '')) || String(a.cliente || '').localeCompare(String(b.cliente || ''))), [vendas, per.from, per.to, modelos]); const lunaFretesVendas = useMemo(() => [...lunaFechamentoVendas, ...emporioCamasTarcisioFretes].filter((v) => { const entregador = (v.lojaId || LOJA_PADRAO.id) === 'emporio' ? entregadorEmporioCamaVenda(v) : entregadorLunaVenda(v); if (entregador === 'FG') return false; return lunaFreteEntregadorFiltro === 'todos' || entregador === lunaFreteEntregadorFiltro; }), [lunaFechamentoVendas, emporioCamasTarcisioFretes, lunaFreteEntregadorFiltro]); const lunaFechamentoResumo = useMemo(() => { const fretes = lunaFretesVendas.reduce((a,v) => a + (Number(v.frete) || 0), 0); const lucroLoja = lunaFechamentoVendas.reduce((a,v) => a + lucroBaseVenda(v), 0); const semCep = lunaFechamentoVendas.filter((v) => prefixoCep(v.cep) === null).length; const fg = lunaFechamentoVendas.filter(vendaEntreguePeloFgOperacao); const creditoFg = fg.reduce((a,v)=>a+Math.max(0,lucroBaseVenda(v)),0); return { pedidos:lunaFechamentoVendas.length, fretes, lucroLoja, semCep, pedidosFg:fg.length, creditoFg }; }, [lunaFechamentoVendas, lunaFretesVendas, fgMovimentos]); const textoFretesLuna = () => { if (!lunaFretesVendas.length) return ''; const dias = [...new Set(lunaFretesVendas.map((v)=>v.dataEntrega))].sort(); const tituloEntregador = lunaFreteEntregadorFiltro === 'todos' ? '' : ` — ${lunaFreteEntregadorFiltro}`; const blocos = dias.map((dia) => { const vs = lunaFretesVendas.filter((v)=>v.dataEntrega===dia); const linhas = vs.map((v) => { const p = prefixoCep(v.cep); return `${p === null ? 'SEM CEP' : String(p).padStart(3,'0')}: ${brlMsgCurto(Number(v.frete) || 0)}`; }); const totalDia = vs.reduce((a,v)=>a+(Number(v.frete)||0),0); return `${brDateShort(dia)}${tituloEntregador}\n${linhas.join('\n')}\nTotal: ${brlMsgCurto(totalDia)}`; }); return `${blocos.join('\n\n')}${dias.length > 1 ? `\n\nTotal do período: ${brlMsgCurto(lunaFechamentoResumo.fretes)}` : ''}`; }; const textoLucroLojaLuna = () => { if (!lunaFechamentoVendas.length) return ''; const dias = [...new Set(lunaFechamentoVendas.map((v)=>v.dataEntrega))].sort(); const blocos = dias.map((dia) => { const vs = lunaFechamentoVendas.filter((v)=>v.dataEntrega===dia); const linhas = vs.map((v) => { const p = prefixoCep(v.cep); return `${p === null ? 'SEM CEP' : String(p).padStart(3,'0')}: ${brlMsg(lucroBaseVenda(v))}`; }); const totalDia = vs.reduce((a,v)=>a+lucroBaseVenda(v),0); return `${diaSemanaPt(dia)}, ${brDateShort(dia)} — Lucro Loja\n${linhas.join('\n')}\nTotal: ${brlMsg(totalDia)}`; }); return `${blocos.join('\n\n')}${dias.length > 1 ? `\n\nTotal do período: ${brlMsg(lunaFechamentoResumo.lucroLoja)}` : ''}`; }; const textoAcertoDouglasLuna = () => { if (!lunaFechamentoVendas.length) return ''; // Envia ao Douglas o número CHEIO de cada pedido. A divisão de 50% só acontece // quando o pagamento é efetivamente marcado como pago no painel. const dias = [...new Set(lunaFechamentoVendas.map((v)=>v.dataEntrega))].sort(); const blocos = dias.map((dia) => { const vs = lunaFechamentoVendas.filter((v)=>v.dataEntrega===dia); const linhas = vs.map((v, i) => `${i + 1}. ${v.cliente || 'Sem nome'} - ${brlMsg(Math.max(0, resultadoBaseVendedorLuna(v)))}`); const totalDia = vs.reduce((a, v) => a + Math.max(0, resultadoBaseVendedorLuna(v)), 0); return `${brDateShort(dia)}\n${linhas.join('\n')}\nTotal do dia: ${brlMsg(totalDia)}`; }); const total = lunaFechamentoVendas.reduce((a, v) => a + Math.max(0, resultadoBaseVendedorLuna(v)), 0); return `${blocos.join('\n\n')}${dias.length > 1 ? `\n\nTotal do período: ${brlMsg(total)}` : ''}`; }; const modeloOptions = ( <> {modelosVisiveis.map((m) => )} ); const renderVenda = (v) => { if (editId === v.id && editDraft) { return (
setEditDraft((d) => ({ ...d, data: e.target.value }))} className="inp" /> {editDraft.tipo === ORG && } {[ORG,ADX].includes(editDraft.tipo) && } setEditDraft((d) => ({ ...d, cliente: e.target.value }))} className="inp" /> {editDraft.formaPagamento === 'credito' && } {['emporio','luna'].includes(editDraft.lojaId) && { const cep=e.target.value.replace(/\D/g,'').slice(0,8); setEditDraft((d) => { if (d.lojaId === 'luna') { const q=d.qtdItens||1, f2=freteLuna(cep,FRETE_LUNA_T2,q), f1=freteLuna(cep,FRETE_LUNA_T1,q); return { ...d, cep, frete:f2===null?d.frete:String(f2), freteTabela1:f1===null?d.freteTabela1:String(f1) }; } if (d.lojaId === 'emporio' && ehCamaFgEmporio(d)) { const f2=freteLuna(cep,FRETE_LUNA_T2,d.qtdItens||1); return { ...d, cep, frete:f2===null?d.frete:String(f2), tipoProduto:'cama_fg' }; } return { ...d, cep }; }); }} className="inp" placeholder="00000000" />} {(editDraft.lojaId === 'luna' || (editDraft.lojaId==='emporio' && ehCamaFgEmporio(editDraft))) && { const q=Math.max(1,Number(e.target.value)||1); setEditDraft((d) => { const f2=freteLuna(d.cep,FRETE_LUNA_T2,q); if (d.lojaId==='luna') { const f1=freteLuna(d.cep,FRETE_LUNA_T1,q); return { ...d, qtdItens:q, frete:f2===null?d.frete:String(f2), freteTabela1:f1===null?d.freteTabela1:String(f1) }; } return { ...d, qtdItens:q, frete:f2===null?d.frete:String(f2), tipoProduto:'cama_fg' }; }); }} className="inp" />} {editDraft.lojaId === 'luna' && } {editDraft.lojaId === 'emporio' && (editDraft.tipoProduto === 'cama_fg' || modeloEhCamaFgEmporio(modelos.find((x)=>x.id===editDraft.modeloId))) && } {editDraft.lojaId === 'luna' && } setEditDraft((d) => ({ ...d, dataPrevista: e.target.value }))} className="inp" /> {editDraft.status === 'entregue' && setEditDraft((d) => ({ ...d, dataEntrega: e.target.value }))} className="inp" />} 1 ? 'Valor parcelado (com taxa)' : 'Valor da venda'}> setEditDraft((d) => ({ ...d, receita: e.target.value }))} inputMode="decimal" className="inp" /> setEditDraft((d) => ({ ...d, custo: e.target.value }))} inputMode="decimal" className="inp" /> setEditDraft((d) => ({ ...d, frete: e.target.value }))} inputMode="decimal" className="inp" /> {['emporio','luna'].includes(editDraft.lojaId) &&
setEditDraft((d) => ({ ...d, comissaoVendedorManual:e.target.value, comissaoVendedorManualAtiva:true }))} inputMode="decimal" className="inp" placeholder="0,00" />
{editDraft.comissaoVendedorManualAtiva ? 'Valor manual — substitui o cálculo automático' : `Automático: ${brl(vendedorDevidoVenda({ ...v, ...editDraft, comissaoVendedorManual:null, receita:parseBRL(editDraft.receita), custo:parseBRL(editDraft.custo), frete:parseBRL(editDraft.frete), freteTabela1:parseBRL(editDraft.freteTabela1) }))}`} {editDraft.comissaoVendedorManualAtiva && }
} {['emporio','luna'].includes(editDraft.lojaId) && parseBRL(editDraft.receita) > 0 && (() => { const tmpEdit = enriquecerVenda({ ...v, ...editDraft, receita:parseBRL(editDraft.receita), custo:parseBRL(editDraft.custo), frete:parseBRL(editDraft.frete), freteTabela1:parseBRL(editDraft.freteTabela1), comissaoVendedorManual:null, taxaCartaoPct:null, taxaCartaoValor:null }, editDraft.lojaId, editDraft.modeloId || null); const detEdit = editDraft.lojaId === 'emporio' ? detalheNatanVenda({ ...tmpEdit, comissaoVendedorManual:null }) : null; return
Valor {tmpEdit.formaPagamento==='credito' && Number(tmpEdit.parcelas)>1 ? `parcelado (${tmpEdit.parcelas}x)` : 'cobrado'}: {brl(tmpEdit.receita)} Taxa da loja ({num(taxaCartaoPctVenda(tmpEdit),2)}%): -{brl(taxaCartaoValorVenda(tmpEdit))} Líquido que cai na conta: {brl(receitaLiquidaVenda(tmpEdit))} {detEdit && !detEdit.camaFg && tmpEdit.formaPagamento==='credito' && Number(tmpEdit.parcelas)>1 && <> Base normal Natan: {brl(detEdit.valorNormalPagamento)} Líquido p/ Natan após taxa {num(detEdit.taxaAtual,2)}%: {brl(detEdit.valorLiquidoVendedor || 0)} Extra líquido: {brl(detEdit.acrescimoParcelamento)} 50% do extra: {brl(detEdit.extraNatan)} Natan automático: {brl(detEdit.total)} }
; })()} {editDraft.lojaId === 'luna' &&
Douglas usa a conta própria do acerto (taxa do vendedor + fornecedor + frete T1) e recebe 50% do valor cheio. Você pode sobrescrever a comissão neste pedido; nesse caso o acerto cheio vira 2x o valor informado. O Lucro Loja continua separado (taxa geral + frete T2). Se a entrega foi pelo FG e o crédito foi corrigido manualmente, esse valor corrigido passa a valer nas duas contas.
} {editDraft.lojaId === 'emporio' && ehCamaFgEmporio(editDraft) &&
Cama FG: comissão automática do Natan R$50,00 e frete pela Tabela 2 da Luna. Você pode alterar a comissão deste pedido no campo acima; sofás continuam com as regras atuais.
}
); } const st = statusVenda(v); const info = STATUS_INFO[st] || STATUS_INFO.pendente; return (
{nomeModelo(v.modeloId) || 'Sem modelo'} · {v.cliente || 'sem nome'}
Pedido {(v.data || '').slice(8, 10)}/{(v.data || '').slice(5, 7)} {nomeVenda(v)} {nomeLoja(lojaDaVenda(v))} {taxaCartaoValorVenda(v) > 0 && taxa {brl(taxaCartaoValorVenda(v))}} {vendedorDevidoVenda(v) !== 0 && {vendedorNomeVenda(v)} {brl(vendedorDevidoVenda(v))}} {(v.lojaId||LOJA_PADRAO.id)==='luna' && entregador {entregadorLunaVenda(v)}} {ehCamaFgEmporio(v) && entregador {entregadorEmporioCamaVenda(v)}} {(v.lojaId||LOJA_PADRAO.id)==='luna' && recebido por {pagamentoRecebidoPorLunaVenda(v)==='fg'?'FG':'Luna'}} {info.label} {v.dataPrevista && st !== 'entregue' && prev. {brDate(v.dataPrevista)}} {v.dataEntrega && st === 'entregue' && entregue {brDate(v.dataEntrega)}}
{brl(v.receita)} custo {brl(custoTot(v))}{v.frete ? ` (frete ${brl(v.frete)})` : ''}
{vendaPendente(v) && } {vendaPendente(v) && }
); }; const font = { fontFamily: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif' }; const presetBtn = (tipo, txt) => ( ); const navegarPainel = (destino, abaDestino = null) => { setMobileNavOpen(false); setPaginaAtiva('painel'); if (abaDestino) setAba(abaDestino); window.setTimeout(() => { if (destino === 'topo') { window.scrollTo({ top: 0, behavior: 'smooth' }); return; } document.getElementById(destino)?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, abaDestino ? 80 : 0); }; const abrirAjustes = () => { setMobileNavOpen(false); setPaginaAtiva('ajustes'); window.setTimeout(() => window.scrollTo({ top: 0, behavior: 'smooth' }), 0); }; const atualizarConfigComissao = (lojaId, patch) => { setConfig((c) => { const atual = c?.comissoes?.[lojaId] || {}; return { ...c, comissoes: { ...(c.comissoes || {}), [lojaId]: { ...atual, ...patch } } }; }); }; const atualizarComissaoModelo = (lojaId, modeloId, raw) => { setConfig((c) => { const atual = c?.comissoes?.[lojaId] || {}; const porModelo = { ...(atual.porModelo || {}) }; if (raw === '') delete porModelo[modeloId]; else porModelo[modeloId] = Math.max(0, parseBRL(raw)); return { ...c, comissoes: { ...(c.comissoes || {}), [lojaId]: { ...atual, porModelo } } }; }); }; const syncUi = { carregando: { label: 'Carregando banco', tone: 'text-slate-300', icon: }, salvando: { label: 'Salvando', tone: 'text-amber-300', icon: }, salvo: { label: 'Banco sincronizado', tone: 'text-emerald-300', icon: }, erro: { label: 'Cópia local ativa', tone: 'text-rose-300', icon: }, conflito: { label: 'Conflito de dados', tone: 'text-rose-300', icon: }, }[syncStatus] || { label: 'Sincronização', tone: 'text-slate-300', icon: }; if (!authChecked) { return
Carregando painel…
; } if (!authenticated) { return (

Entrar no painel

Os dados financeiros agora ficam protegidos por senha e salvos no banco.

setLoginPassword(e.target.value)} className="inp mt-1" placeholder="Digite sua senha" /> {loginError &&
{loginError}
}
); } if (!loaded) { return
Carregando dados…
; } return (
{mobileNavOpen &&

{paginaAtiva === 'ajustes' ? 'Ajustes da conta' : aba === 'marketplaces' ? 'Marketplaces · Magalu e Shopee' : aba === 'zpl' ? 'Converter etiquetas ZPL em PDF' : 'Métricas de Campanhas · WhatsApp'}

{paginaAtiva === 'painel' && !['marketplaces','zpl'].includes(aba) && Pedidos + Entregas}
{paginaAtiva === 'ajustes' ? 'Configurações, comissões e conta' : aba === 'zpl' ? 'Etiquetas 10 × 15 cm ou folha A4' : per.label}
{syncUi.icon}{syncUi.label}
{aba !== 'zpl' && <>
{monthLabel(mesRefAtual)}
}
GA
Administrador
Painel
{paginaAtiva === 'ajustes' ? (
{(syncError || bootError) &&
{syncError || bootError}
}

Configurações da conta

Gerencie lojas, comissões dos vendedores e preferências administrativas. As alterações são salvas automaticamente no banco.

Comissões dos vendedores

A comissão manual definida diretamente em um pedido continua tendo prioridade. Depois dela, vale o valor específico do modelo e, por último, a regra geral abaixo.

{['emporio','luna'].map((lojaId) => { const cfg = comissaoConfigLoja(lojaId); const vendedor = PAGAMENTO_PESSOA[lojaId]?.vendedor || 'Vendedor'; const mods = modelos.filter((m) => (m.lojaId || LOJA_PADRAO.id) === lojaId); return
{nomeLoja(lojaId)} · {vendedor}
Regra padrão para novos cálculos e pedidos ainda não pagos
Salvo automaticamente
{cfg.tipo === 'fixo' &&
R$atualizarConfigComissao(lojaId,{valorFixo:parseBRL(e.target.value)})} inputMode="decimal" className="inp !pl-9" placeholder="0,00" />
} {cfg.tipo === 'metade_lucro' &&
O vendedor recebe 50% do lucro do pedido antes da comissão: valor líquido que cai na conta − fornecedor − frete. Ads e custos gerais do período não entram na base de um pedido individual.
} {cfg.tipo === 'atual' &&
{lojaId==='emporio' ? 'Mantém as regras atuais do Natan, incluindo comissão por modelo e ajuste de parcelamento; camas FG continuam com a regra atual.' : 'Mantém a regra atual do Douglas, calculada separadamente com taxa do vendedor, fornecedor e frete T1, com divisão 50/50.'}
}
Valores específicos por modeloopcional

Se preencher um valor, ele substitui a regra geral somente para aquele modelo. Deixe vazio para usar a regra selecionada acima.

{mods.length === 0 ?
Nenhum modelo específico desta loja cadastrado.
: mods.map((m) =>
{m.nome}
{m.fornecedor || 'Sem fornecedor'}
atualizarComissaoModelo(lojaId,m.id,e.target.value)} inputMode="decimal" className="inp text-right" placeholder="Automático" />
)}
; })}

Conta e lojas

Preferências administrativas gerais do painel.

Lojas
{lojas.map((l) =>
renameLoja(l.id,e.target.value)} className="inp !w-auto flex-1 text-sm" />{l.id===LOJA_PADRAO.id ? principal : }
)}
setNovaLoja(e.target.value)} placeholder="Nova loja…" className="inp"/>
{lojaMsg &&

{lojaMsg}

}
setConfig((c)=>({...c,impostoAdPct:parseBRL(e.target.value)}))} inputMode="decimal" className="inp" />
Backup dos dados

O banco salva automaticamente; o backup manual serve como cópia adicional.

{backupMsg &&
{backupMsg}
}
) : (<> {(syncError || bootError) &&
{syncError || bootError}{syncStatus === 'conflito' && }
} {aba !== 'zpl' &&
{/* Filtro de data */}
Período {presetBtn('hoje', 'Hoje')} {presetBtn('ontem', 'Ontem')} {presetBtn('amanha', 'Amanhã')} {presetBtn('mes', 'Este mês')} {presetBtn('mes_passado', 'Mês passado')} {presetBtn('ano', 'Este ano')} {presetBtn('custom', 'Personalizado')} {periodo.tipo === 'custom' && (
setPeriodo((p) => ({ ...p, from: e.target.value }))} className="inp !w-auto text-sm" /> até setPeriodo((p) => ({ ...p, to: e.target.value }))} className="inp !w-auto text-sm" />
)}
{/* Seletor global de loja */} {!['marketplaces','zpl'].includes(aba) && (
Loja {lojaFiltro !== 'todas' && Mostrando apenas {nomeLoja(lojaFiltro)}}
)}
} {!['marketplaces','zpl'].includes(aba) && <> {/* Resumo do período */} {periodoFuturo ? (
} label="Faturamento previsto" value={brl(previsaoPeriodo.faturamento)} tone="emerald" /> } label="Lucro previsto*" value={brl(previsaoPeriodo.lucroPrevisto)} tone={previsaoPeriodo.lucroPrevisto >= 0 ? 'emerald' : 'rose'} /> } label="Entregas previstas" value={num(previsaoPeriodo.pedidos, 0)} hint="Pedidos ainda não entregues com entrega prevista no período. Entregas FG continuam na contagem; na Luna e nas camas FG da Empório elas ficam fora do faturamento financeiro." /> } label="Fornecedor previsto" value={brl(previsaoPeriodo.fornecedor)} /> } label="Frete previsto" value={brl(previsaoPeriodo.frete)} /> } label="Margem prevista" value={`${num(previsaoPeriodo.margem * 100, 2)}%`} /> } label="Pedidos a entregar" value={num(previsaoPeriodo.pedidos, 0)} onClick={abrirPendentesPrevistosPeriodo} hint="Pedidos ainda não entregues com entrega prevista dentro do período selecionado. Clique para ver somente esses pedidos." />
) : (
} label="Faturamento financeiro líquido" value={brl(resumoOperacional.comercial.faturamento)} /> } label="Recebido pela loja" value={brl(resumoOperacional.realizado.faturamento)} tone="emerald" /> {['emporio','luna'].includes(lojaFiltro) ? (<> = 0 ? : } label={`Lucro antes do ${PAGAMENTO_PESSOA[lojaFiltro]?.vendedor} (pedidos)`} value={brl(resumoOperacional.comercial.lucro)} tone={resumoOperacional.comercial.lucro > 0 ? 'emerald' : resumoOperacional.comercial.lucro < 0 ? 'rose' : 'slate'} /> = 0 ? : } label="Lucro Loja (pedidos)" value={brl(resumoOperacional.comercial.resultadoAposVendedor)} tone={resumoOperacional.comercial.resultadoAposVendedor > 0 ? 'emerald' : resumoOperacional.comercial.resultadoAposVendedor < 0 ? 'rose' : 'slate'} /> = 0 ? : } label={`Lucro antes do ${PAGAMENTO_PESSOA[lojaFiltro]?.vendedor} (entregas)`} value={brl(resumoOperacional.realizado.lucro)} tone={resumoOperacional.realizado.lucro > 0 ? 'emerald' : resumoOperacional.realizado.lucro < 0 ? 'rose' : 'slate'} /> = 0 ? : } label="Lucro Loja (entregas)" value={brl(resumoOperacional.realizado.resultadoAposVendedor)} tone={resumoOperacional.realizado.resultadoAposVendedor > 0 ? 'emerald' : resumoOperacional.realizado.resultadoAposVendedor < 0 ? 'rose' : 'slate'} /> ) : (<> = 0 ? : } label="Lucro loja (pedidos)" value={brl(resumoOperacional.comercial.lucro)} tone={resumoOperacional.comercial.lucro > 0 ? 'emerald' : resumoOperacional.comercial.lucro < 0 ? 'rose' : 'slate'} /> = 0 ? : } label="Lucro loja (entregas)" value={brl(resumoOperacional.realizado.lucro)} tone={resumoOperacional.realizado.lucro > 0 ? 'emerald' : resumoOperacional.realizado.lucro < 0 ? 'rose' : 'slate'} /> )} } label={per.from === per.to ? 'Faturamento previsto do dia' : 'Faturamento previsto no período'} value={brl(previsaoPeriodo.faturamento)} tone={previsaoPeriodo.faturamento > 0 ? 'emerald' : 'slate'} hint="Soma apenas dos pedidos previstos que entram no faturamento financeiro. Na Luna, entregas pelo FG ficam fora deste faturamento; nas camas FG da Empório, vale a mesma regra. Esses pedidos continuam no Lucro Loja e nas obrigações do vendedor." /> = 0 ? : } label={per.from === per.to ? 'Lucro previsto do dia' : 'Lucro previsto no período'} value={brl(previsaoPeriodo.lucroPrevisto)} tone={previsaoPeriodo.lucroPrevisto > 0 ? 'emerald' : previsaoPeriodo.lucroPrevisto < 0 ? 'rose' : 'slate'} hint="Lucro previsto sobre o faturamento líquido que cai na conta. A taxa da maquininha já foi retirada antes; depois entram fornecedor, frete e custos operacionais. Comissão de vendedor só é descontada quando o pagamento for marcado como pago. Ads futuros não são projetados." /> } label="Pedidos a entregar" value={num(previsaoPeriodo.pedidos, 0)} onClick={abrirPendentesPrevistosPeriodo} hint="Pedidos ainda não entregues com entrega prevista dentro do período selecionado. Clique para ver somente esses pedidos." /> } label="Pendentes no período" value={num(geral.pendentes, 0)} onClick={abrirPendentesDoPeriodo} hint="Pedidos ainda não entregues com entrega prevista dentro do período selecionado. Clique para ver." /> } label="Total de pedidos" value={num(geral.totalPedidos, 0)} />
)} {/* Resultado financeiro separado: pedidos em cima, entregas embaixo */} } title={`Resultado financeiro · ${per.label}`} defaultOpen={false}>
{['emporio','luna'].includes(lojaFiltro) && (() => { const vendedor = PAGAMENTO_PESSOA[lojaFiltro]?.vendedor || 'Vendedor'; const temAjusteAds = !!ajusteAdsPeriodoRegistro(lojaFiltro, per.from, per.to); const temAjusteVend = !!ajusteVendedorPeriodoRegistro(lojaFiltro, per.from, per.to); return
Valores manuais do período · {nomeLoja(lojaFiltro)}
Edite o total gasto em anúncios e quanto foi efetivamente pago ao {vendedor}. Esses números passam a ser a referência deste período.
{(temAjusteAds || temAjusteVend) && Ajuste manual ativo}
setAjustePeriodoAdsInput(e.target.value)} inputMode="decimal" placeholder="Ex.: 1.250,00" className="inp" /> setAjustePeriodoVendedorInput(e.target.value)} inputMode="decimal" placeholder="Ex.: 500,00" className="inp" />
0 ? `${brl(adReal.gastoTotal)} ÷ ${adReal.nPedidos} pedido(s) de anúncio` : 'Sem pedidos de anúncio no período'} /> 0 ? `${brl(adReal.receita)} em receita entregue ÷ ${brl(adReal.gastoTotal)} em Ads` : 'Sem gasto em anúncios no período'} tone={adReal.roas != null ? (adReal.roas >= 1 ? 'emerald' : 'rose') : 'slate'} />
{ajustePeriodoMsg &&
{ajustePeriodoMsg}
}
; })()} {[ ['PEDIDOS REALIZADOS NO PERÍODO', 'Considera a data em que o pedido foi feito.', resumoOperacional.comercial, 'Pedidos'], ['PEDIDOS ENTREGUES NO PERÍODO', 'Considera a data real em que a entrega foi confirmada.', resumoOperacional.realizado, 'Entregues'], ].map(([titulo, subtitulo, r, qtdLabel]) => (
{titulo}
{subtitulo}
{qtdLabel}
{num(r.pedidos, 0)}
Faturamento
{brl(r.faturamento)}
{Math.abs((r.receitaBaseLucro || 0) - (r.faturamento || 0)) > 0.005 &&
Receita base do lucro (inclui FG)
{brl(r.receitaBaseLucro || 0)}
}
Fornecedor
{brl(r.fornecedor)}
Frete
{brl(r.frete)}
Taxas maquininha (já descontadas)
{brl(r.taxasCartao || 0)}
Margem antes de Ads/vendedor
{brl(r.margemAntesAds)}
{Math.abs(r.ajusteFgManual || 0) > 0.005 &&
Ajuste manual FG
=0?'text-emerald-600':'text-rose-600'}`}>{(r.ajusteFgManual||0)>=0?'+ ':''}{brl(r.ajusteFgManual||0)}
}
Ads
{brl(r.ads)}
Custos operacionais
{brl(r.custosOp)}
{lojaFiltro==='luna' ? 'Douglas devido (não descontado)' : (titulo.startsWith('PEDIDOS REALIZADOS') ? 'Vendedor previsto (não descontado)' : 'Vendedor devido (não descontado)')}
{brl(r.vendedorDevido || 0)}
{['emporio','luna'].includes(lojaFiltro) ? `Lucro antes do ${PAGAMENTO_PESSOA[lojaFiltro]?.vendedor}` : 'Lucro da loja'}
= 0 ? 'text-emerald-600' : 'text-rose-600'}`}>{brl(r.lucro)} ({num(r.margem * 100, 2)}%)
{(r.creditoFg || 0) !== 0 &&
Crédito com FG (fora do faturamento)
{brl(r.creditoFg)}
{r.pedidosFg || 0} pedido(s) fora do caixa da loja
} {['emporio','luna'].includes(lojaFiltro) &&
{PAGAMENTO_PESSOA[lojaFiltro]?.vendedor} pago no período
− {brl(r.vendedorPago || 0)}
} {['emporio','luna'].includes(lojaFiltro) &&
Lucro Loja (após pagamento)
= 0 ? 'text-emerald-600' : 'text-rose-600'}`}>{brl(r.resultadoAposVendedor)} ({num(r.margemAposVendedor * 100, 2)}%)
}
))} {(previsaoPeriodo.pedidos > 0 || periodoFuturo) && (
PREVISÃO DE ENTREGAS NO PERÍODO
Usa a data prevista de entrega dos pedidos ainda pendentes.
Entregas previstas
{num(previsaoPeriodo.pedidos, 0)}
Faturamento previsto
{brl(previsaoPeriodo.faturamento)}
{Math.abs((previsaoPeriodo.receitaBaseLucro || 0) - (previsaoPeriodo.faturamento || 0)) > 0.005 &&
Receita base do lucro (inclui FG)
{brl(previsaoPeriodo.receitaBaseLucro || 0)}
}
Fornecedor previsto
{brl(previsaoPeriodo.fornecedor)}
Frete previsto
{brl(previsaoPeriodo.frete)}
Taxas maquininha previstas (já descontadas)
{brl(previsaoPeriodo.taxasCartao || 0)}
Margem antes Ads/vendedor
{brl(previsaoPeriodo.margemAntesAds)}
{lojaFiltro==='luna'?'Douglas previsto (não descontado)':'Vendedor previsto (não descontado)'}
{brl(previsaoPeriodo.vendedorPrevisto || 0)}
Custos operacionais previstos
{brl(previsaoPeriodo.custosOp)}
Lucro da loja previsto*
= 0 ? 'text-emerald-700' : 'text-rose-600'}`}>{brl(previsaoPeriodo.lucroPrevisto)} ({num(previsaoPeriodo.margem * 100, 2)}%)
{(previsaoPeriodo.creditoFgPrevisto || 0) !== 0 &&
Crédito FG previsto (fora do faturamento)
{brl(previsaoPeriodo.creditoFgPrevisto)}
}
* Não projeta gasto futuro de Ads. O faturamento previsto já é líquido da taxa da maquininha; o lucro desconta fornecedor, frete e custos operacionais. Vendedor aparece separado como obrigação e só é descontado quando o pagamento for marcado como pago.
)}
Nos resultados de pedidos e entregas, Ads já são descontados uma única vez. Não subtraia novamente o valor exibido em Ads.
{/* Registrar pedido */} } title="Registrar pedido">
Colar ficha de pedido e preencher automaticamente