// 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 (
{aberto && (
{pares.length > 0 && (

⚠️ Possíveis duplicidades

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 }) => (
✋ manual {m.description} {cfDia(m.date)} · {m.account} {fmt(m.amount)}
💳 extrato {f.description} {cfDia(f.date)} · {f.account} {fmt(f.amount)}
{mesmoValor ? "mesmo valor" : "descrição parecida"}
))}
)}

✔️ Conferência por categoria

Confirme se o total do mês de cada categoria bate com o que você esperava.

{porCat.map(c => (
{c.conferida ? ( <> ✅ {c.cat} conferido em {fmt(c.total)} ) : ( <> O mês de {c.cat} confere no valor de {fmt(c.total)}? {c.nFat} do extrato{c.nManual > 0 ? ` + ${c.nManual} manuais` : ""}{c.mudou ? ` · conferido antes em ${fmt(c.confAnterior)}, o valor mudou` : ""} )}
))}
)}
); } Object.assign(window, { ConfrontoCard });