// Importação de faturas de cartão: upload/colar → parse → IA categoriza → revisão → importa const { useState: useStateFi } = React; // ---- Parsers ---- function fatParseOFX(text) { const out = []; const blocks = text.match(/[\s\S]*?<\/STMTTRN>/gi) || []; for (const b of blocks) { const g = (tag) => { const m = b.match(new RegExp("<" + tag + ">([^<\\r\\n]+)")); return m ? m[1].trim() : ""; }; const dt = g("DTPOSTED").slice(0, 8); const amt = parseFloat(g("TRNAMT").replace(",", ".")); const desc = g("MEMO") || g("NAME") || "Lançamento"; if (!dt || isNaN(amt)) continue; out.push({ date: `${dt.slice(0, 4)}-${dt.slice(4, 6)}-${dt.slice(6, 8)}`, desc, valor: Math.abs(amt), credito: amt > 0 }); } return out; } function fatNormDate(s, year) { s = (s || "").trim(); let m = s.match(/^(\d{4})-(\d{2})-(\d{2})/); if (m) return s.slice(0, 10); m = s.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/); if (m) return `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`; m = s.match(/^(\d{1,2})\/(\d{1,2})/); if (m) return `${year}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`; return null; } function fatParseLinhas(text, year) { const out = []; const sep = text.includes(";") ? ";" : (text.includes("\t") ? "\t" : null); for (const raw of text.split(/\r?\n/)) { const line = raw.trim(); if (!line) continue; let date = null, desc = "", valor = NaN; if (sep) { const cols = line.split(sep).map(c => c.trim().replace(/^"|"$/g, "")); for (const c of cols) { const d = fatNormDate(c, year); if (d) { date = d; break; } } // valor: última coluna numérica for (let i = cols.length - 1; i >= 0; i--) { const v = window.parseValor(cols[i]); if (cols[i] && !isNaN(v) && v !== 0 && !fatNormDate(cols[i], year)) { valor = v; break; } } desc = cols.filter(c => c && !fatNormDate(c, year) && String(window.parseValor(c)) !== String(valor)).sort((a, b) => b.length - a.length)[0] || ""; } else { const dm = line.match(/^(\d{1,2}\/\d{1,2}(\/\d{4})?|\d{4}-\d{2}-\d{2})\s+/); const vm = line.match(/(-?\s?R?\$?\s?[\d.,]+)\s*$/); if (!vm) continue; valor = window.parseValor(vm[1]); date = dm ? fatNormDate(dm[1], year) : null; desc = line.slice(dm ? dm[0].length : 0, vm.index).trim(); } if (!desc || isNaN(valor) || valor === 0) continue; out.push({ date, desc, valor: Math.abs(valor), credito: valor < 0 }); } return out; } function fatParse(text, year) { if (/|/i.test(text)) return fatParseOFX(text); return fatParseLinhas(text, year); } // ---- Heurística de categoria (fallback e pré-classificação) ---- const FAT_REGRAS = [ [/mercado|super|carrefour|pao de acucar|extra|atacad|hortifruti|sacolao|zaffari|assai/i, "🛒 Mercado"], [/uber|99 ?(app|pop)?|taxi|posto|combust|estacion|ipva|pedagio|sem parar|shell|ipiranga|petrobras/i, "🚗 Transporte"], [/ifood|rappi|restaurante|lanche|pizza|burger|hamburg|padaria|cafe|bar |sushi|churras/i, "🍽️ Comida fora"], [/farmacia|drogaria|droga ?raia|pacheco|panvel|clinica|laborat|exame|medic|hospital|dentista|unimed|amil/i, "💊 Saúde"], [/netflix|spotify|prime|disney|hbo|max |globoplay|youtube|apple.com|icloud|google (one|storage)|assinatura|deezer|paramount/i, "📺 Assinaturas"], [/academia|smart ?fit|gympass|wellhub|crossfit|pilates|personal/i, "💪 Fitness"], [/escola|colegio|faculdade|curso|udemy|alura|kumon|wizard|cultura inglesa/i, "🎓 Educação"], [/petshop|petz|cobasi|veterinar|racao|pet /i, "🐶 Pet"], [/renner|zara|c&a|riachuelo|shein|nike|adidas|calcad|vestuar|hering/i, "👚 Roupas"], [/salao|cabele|barbear|manicure|estetica|sephora|boticario|natura/i, "💅 Beleza"], [/cinema|teatro|show|ingresso|viagem|hotel|airbnb|booking|latam|gol |azul |decolar|parque/i, "🏖️ Lazer"], [/presente|gift/i, "🎁 Presente"], [/luz|enel|energia|agua |sabesp|gas |internet|vivo|claro|tim |oi |condominio|aluguel|iptu/i, "🏠 Moradia"], [/brinquedo|fralda|bebe|infantil|ri happy/i, "👨‍👩‍👧‍👦 Crianças"], [/seguro|previdencia|porto seguro|allianz|bradesco seguros/i, "🛡️ Proteção"], [/doacao|dizimo|igreja|caridade/i, "🙏 Caridade"], [/corretora|tesouro|cdb|fundo|acoes|invest/i, "📈 Investimento"], ]; function fatCategoriaHeuristica(desc) { for (const [re, cat] of FAT_REGRAS) if (re.test(desc)) return cat; return "🤷 Outros"; } // ---- PDF: extração de texto via pdf.js (carregado sob demanda) ---- let fatPdfjsPromise = null; function fatLoadPdfjs() { if (window.pdfjsLib) return Promise.resolve(window.pdfjsLib); if (!fatPdfjsPromise) fatPdfjsPromise = new Promise((res, rej) => { const s = document.createElement("script"); s.src = "https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.min.js"; s.onload = () => { window.pdfjsLib.GlobalWorkerOptions.workerSrc = "https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.worker.min.js"; res(window.pdfjsLib); }; s.onerror = () => { fatPdfjsPromise = null; rej(new Error("pdfjs")); }; document.head.appendChild(s); }); return fatPdfjsPromise; } async function fatPdfToText(buf) { const pdfjs = await fatLoadPdfjs(); const doc = await pdfjs.getDocument({ data: buf }).promise; const linhas = []; for (let p = 1; p <= doc.numPages; p++) { const page = await doc.getPage(p); const tc = await page.getTextContent(); const rows = {}; for (const it of tc.items) { if (!it.str || !it.str.trim()) continue; const y = Math.round(it.transform[5] / 2) * 2; // agrupa itens da mesma linha (rows[y] = rows[y] || []).push({ x: it.transform[4], str: it.str }); } const ys = Object.keys(rows).map(Number).sort((a, b) => b - a); for (const y of ys) linhas.push(rows[y].sort((a, b) => a.x - b.x).map(i => i.str).join(" ").replace(/\s+/g, " ").trim()); } return linhas.join("\n"); } function FaturaImport({ contas, categorias, month, year, txsExistentes, onImport }) { const [conta, setConta] = useStateFi(contas[0] || "XP"); const [texto, setTexto] = useStateFi(""); const [itens, setItens] = useStateFi(null); // [{date, desc, valor, categoria, classe, incluir, dup, ia}] const [fase, setFase] = useStateFi("entrada"); // entrada | analisando | revisao const [erro, setErro] = useStateFi(null); const [arquivos, setArquivos] = useStateFi([]); // [{nome, texto}] const [drag, setDrag] = useStateFi(false); const fmt = window.fmtBRLString; const catNomes = categorias.filter(c => c.tipo !== "Receita").map(c => c.nome); const mesLabel = window.MESES_FULL[month] + "/" + year; const defaultDate = `${year}-${String(month + 1).padStart(2, "0")}-15`; function addArquivo(nome, textoArq) { setArquivos(arr => arr.some(a => a.nome === nome) ? arr : [...arr, { nome, texto: textoArq }]); } function lerArquivos(files) { for (const file of Array.from(files || [])) { if (/\.pdf$/i.test(file.name) || file.type === "application/pdf") { const r = new FileReader(); r.onload = async () => { try { const txt = await fatPdfToText(new Uint8Array(r.result)); if (txt.trim()) addArquivo(file.name, txt); else setErro(`"${file.name}": PDF sem texto extraível (provavelmente escaneado). Cole as linhas da fatura manualmente.`); } catch (e) { setErro(`Não consegui ler "${file.name}". Tente exportar a fatura em OFX/CSV ou colar o texto.`); } }; r.readAsArrayBuffer(file); continue; } const r = new FileReader(); r.onload = () => addArquivo(file.name, String(r.result || "")); r.readAsText(file); } } function onDrop(e) { e.preventDefault(); e.stopPropagation(); setDrag(false); if (fase === "revisao") return; lerArquivos(e.dataTransfer.files); } async function analisar() { setErro(null); const fontes = [...arquivos.map(a => a.texto), texto].filter(t => t && t.trim()); const parsed = fontes.flatMap(t => fatParse(t, year)).filter(x => !x.credito); if (!parsed.length) { setErro("Não reconheci lançamentos. Formatos aceitos: OFX, CSV (data; descrição; valor) ou linhas \"05/07 Descrição 123,45\"."); return; } setFase("analisando"); let base = parsed.map(p => ({ ...p, date: p.date || defaultDate, categoria: fatCategoriaHeuristica(p.desc), ia: false, incluir: true, })); // Check IA: pede a categoria correta de cada despesa try { const lista = base.map((x, i) => ({ i, descricao: x.desc, valor: x.valor })); const prompt = `Você é um assistente financeiro. Classifique cada despesa de fatura de cartão na categoria correta.\nCategorias válidas (use EXATAMENTE estas strings): ${JSON.stringify(catNomes)}\nDespesas:\n${JSON.stringify(lista)}\nRetorne SOMENTE um JSON array: [{"i":0,"categoria":"..."}] — uma entrada por despesa, sem texto extra.`; const res = await window.claude.complete(prompt); const m = res.match(/\[[\s\S]*\]/); if (m) { const cls = JSON.parse(m[0]); for (const c of cls) { if (base[c.i] && catNomes.includes(c.categoria)) { base[c.i].categoria = c.categoria; base[c.i].ia = true; } } } } catch (e) { /* mantém heurística */ } // Duplicatas: mesma conta, valor e data já lançados const chaves = new Set(txsExistentes.map(t => `${t.account}|${t.date}|${Number(t.amount).toFixed(2)}`)); base = base.map(x => ({ ...x, dup: chaves.has(`${conta}|${x.date}|${x.valor.toFixed(2)}`), incluir: !chaves.has(`${conta}|${x.date}|${x.valor.toFixed(2)}`) })); setItens(base); setFase("revisao"); } function setItem(i, patch) { setItens(arr => arr.map((x, idx) => idx === i ? { ...x, ...patch } : x)); } function importar() { const tipoDe = (cat) => (categorias.find(c => c.nome === cat) || {}).tipo || ""; const sel = itens.filter(x => x.incluir); onImport(sel.map(x => ({ id: window.uid(), date: x.date, description: x.desc, account: conta, category: x.categoria, type: "Despesa", amount: x.valor, status: "Pago", origem: "fatura", fixa: false, redutivel: tipoDe(x.categoria) === "Não Essencial", aggregate: false, }))); setItens(null); setTexto(""); setArquivos([]); setFase("entrada"); } const nIA = itens ? itens.filter(x => x.ia).length : 0; const nDup = itens ? itens.filter(x => x.dup).length : 0; const totalSel = itens ? itens.filter(x => x.incluir).reduce((s, x) => s + x.valor, 0) : 0; return (
{ e.preventDefault(); if (fase !== "revisao") setDrag(true); }} onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget)) setDrag(false); }} onDrop={onDrop}> {drag &&
⬇️ Solte as faturas aqui
}

📥 Importar fatura

Envie a fatura (PDF, OFX, CSV ou texto) — cada despesa passa por um check de categoria antes de entrar em {mesLabel}.
{fase !== "revisao" && (
{ lerArquivos(e.target.files); e.target.value = ""; }}/>
{arquivos.length > 0 && (
{arquivos.map(a => ( 📄 {a.nome} ))}
)}
{erro &&
⚠️ {erro}
}
)} {fase === "revisao" && itens && (
✓ {itens.length} despesas reconhecidas {nIA > 0 ? `🤖 ${nIA} categorizadas por IA` : "🔎 categorias sugeridas por regras"} {nDup > 0 && ⚠️ {nDup} possíveis duplicatas (desmarcadas)}
{itens.map((x, i) => ( ))}
DataDescriçãoCategoria (check)Valor
setItem(i, { incluir: e.target.checked })}/> setItem(i, { date: e.target.value })}/> {x.desc}{x.dup && · já lançada?} {fmt(x.valor)}
Total a importar: {fmt(totalSel)} no cartão {conta}
)}
); } Object.assign(window, { FaturaImport });