// Confronto do mês: lançamentos manuais × extrato dos cartões (origem "fatura"). // 1) aponta possíveis duplicidades; 2) pede confirmação do total por categoria. const { useState: useStateCf } = React; function cfNorm(s) { return (s || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9 ]/g, " ").replace(/\s+/g, " ").trim(); } function cfSimilar(a, b) { const ta = cfNorm(a).split(" ").filter(w => w.length >= 4); const tb = cfNorm(b).split(" ").filter(w => w.length >= 4); return ta.some(w => tb.some(x => x === w || x.includes(w) || w.includes(x))); } function cfDia(date) { return (date || "").slice(8, 10) + "/" + (date || "").slice(5, 7); } function ConfrontoCard({ despesas, monthKey, monthLabel, confronto, onConfronto, onDelete }) { const fmt = window.fmtBRLString; const data = confronto[monthKey] || {}; const ignoradas = data.ignoradas || {}; const confirmadas = data.confirmadas || {}; const [aberto, setAberto] = useStateCf(true); const fatura = despesas.filter(t => t.origem === "fatura"); const manuais = despesas.filter(t => t.origem !== "fatura" && !t.seguroId); if (fatura.length === 0) return null; // --- Duplicidades: manual × fatura com mesmo valor, ou descrição parecida e valor próximo --- const pares = []; const usadasFat = new Set(); for (const m of manuais) { for (const f of fatura) { if (usadasFat.has(f.id)) continue; const key = m.id + "|" + f.id; if (ignoradas[key]) continue; const dv = Math.abs(Number(m.amount) - Number(f.amount)); const mesmoValor = dv < 0.01; const parecida = cfSimilar(m.description, f.description) && dv <= Number(f.amount) * 0.15; if (mesmoValor || parecida) { pares.push({ key, m, f, mesmoValor }); usadasFat.add(f.id); break; } } } function salvar(patch) { onConfronto(monthKey, { ...data, ...patch }); } function ignorarPar(key) { salvar({ ignoradas: { ...ignoradas, [key]: true } }); } // --- Confirmação por categoria (categorias que têm linhas do extrato) --- const cats = [...new Set(fatura.map(t => t.category))].sort(); const porCat = cats.map(cat => { const doCat = despesas.filter(t => t.category === cat); const total = doCat.reduce((s, t) => s + Number(t.amount || 0), 0); const conf = confirmadas[cat]; const mudou = conf != null && Math.abs(conf - total) > 0.01; return { cat, total, nManual: doCat.filter(t => t.origem !== "fatura").length, nFat: doCat.filter(t => t.origem === "fatura").length, conferida: conf != null && !mudou, confAnterior: conf, mudou }; }); const pendentes = porCat.filter(c => !c.conferida).length; function conferir(cat, total) { salvar({ confirmadas: { ...confirmadas, [cat]: total } }); } function rever(cat) { const c = { ...confirmadas }; delete c[cat]; salvar({ confirmadas: c }); } const tudoOk = pares.length === 0 && pendentes === 0; return (
Estes lançamentos manuais parecem já ter vindo no extrato do cartão. Mantenha só um para não contar duas vezes.
{pares.map(({ key, m, f, mesmoValor }) => (Confirme se o total do mês de cada categoria bate com o que você esperava.
{porCat.map(c => (