/* Movimentações Financeiras */ function Movimentacoes({ ctx }) { const [filter, setFilter] = useState("all"); const [period, setPeriod] = useState({ kind: "month" }); const [editing, setEditing] = useState(null); const fileRef = useRef(null); const refDate = DEFAULT_REF_DATE; const all = ctx.state.finance; const inPeriodFn = (f) => inPeriod(f.date, period, refDate); const periodFinance = all.filter(inPeriodFn); const ins = periodFinance.filter(f => f.kind === "in").reduce((s, f) => s + f.value, 0); const outs = periodFinance.filter(f => f.kind === "out").reduce((s, f) => s + f.value, 0); const saldo = ins - outs; // pendências agora vêm de sales / inputPurchases (não de finance) const aReceber = ctx.state.sales.reduce((s, x) => s + Math.max(0, x.total - x.paid), 0); const pendingSalesCount = ctx.state.sales.filter(s => s.paid < s.total).length; const filtered = periodFinance.filter(f => { if (filter === "all") return true; if (filter === "in") return f.kind === "in"; if (filter === "out") return f.kind === "out"; if (filter === "pending") return f.status === "a_receber"; return true; }).sort((a, b) => b.date.localeCompare(a.date)); // Chart data — últimos 14 dias do período (ou de DEFAULT_REF_DATE se período "all") const days = []; const r = periodRange(period, refDate); const baseEnd = r.end || refDate; for (let i = 13; i >= 0; i--) { const d = new Date(baseEnd); d.setDate(baseEnd.getDate() - i); const key = d.toISOString().slice(0, 10); const dayIn = all.filter(f => f.date === key && f.kind === "in").reduce((s, f) => s + f.value, 0); const dayOut = all.filter(f => f.date === key && f.kind === "out").reduce((s, f) => s + f.value, 0); days.push({ label: d.toLocaleDateString("pt-BR", { day:"2-digit" }), inV: dayIn, outV: dayOut }); } const onDelete = (f) => ctx.askConfirm({ title: "Excluir lançamento?", message: `Excluir ${f.cat} · ${fmtBRL(f.value)}?`, detail: "Esta ação NÃO restaura estoque ou comissões previamente movidas.", danger: true, onConfirm: () => ctx.actions.deleteFinance(f.id), }); const markPaid = (f) => { ctx.actions.updateFinance(f.id, { status: "confirmado" }); }; const doImport = async () => { const file = await pickFile(".json,.csv"); if (!file) return; try { const text = await readFileAsText(file); let entries = []; if (file.name.endsWith(".json")) { const data = JSON.parse(text); if (Array.isArray(data)) entries = data; else if (Array.isArray(data.finance)) entries = data.finance; } else { // CSV: Data;Tipo;Categoria;Descrição;Status;Valor const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/).filter(Boolean); const sep = lines[0].includes(";") ? ";" : ","; const parseLine = (l) => { // simples — lida com aspas const out = []; let cur = "", q = false; for (const c of l) { if (c === '"') { q = !q; continue; } if (c === sep && !q) { out.push(cur); cur = ""; continue; } cur += c; } out.push(cur); return out; }; for (let i = 1; i < lines.length; i++) { const cols = parseLine(lines[i]); if (cols.length < 6) continue; const [date, tipo, cat, ref, status, valor] = cols; const v = parseFloat((valor||"").replace(/[^\d.,-]/g,"").replace(",",".")); if (!v || isNaN(v) || !date) continue; entries.push({ date: date.slice(0,10), kind: /saída|saida|out/i.test(tipo) ? "out" : "in", cat: cat || "Outras Entradas", ref: ref || "—", value: Math.abs(v), status: /receb|pend|a.recebe/i.test(status) ? "a_receber" : "confirmado", }); } } if (entries.length === 0) { ctx.pushToast("Nenhum lançamento válido encontrado"); return; } entries.forEach(en => ctx.actions.addFinance(en)); ctx.pushToast(`${entries.length} lançamento(s) importado(s)`); } catch (err) { console.error(err); alert("Falha ao importar: " + err.message); } }; const printList = () => { const r = periodRange(period, refDate); const logo = ctx.state.company.logo; const logoIsImage = logo && !/^data:application\/pdf/i.test(logo); const logoBlock = logoIsImage ? `` : `
P
`; const head = `
${logoBlock}
${ctx.state.company.name||""}
${ctx.state.company.cnpj ? "CNPJ "+ctx.state.company.cnpj : ""} ${(ctx.state.company.cnpj && ctx.state.company.address) ? "
" : ""} ${ctx.state.company.address||""}

Movimentações Financeiras

Período: ${r.label} ${filtered.length} lançamento(s) · Emitido em ${new Date().toLocaleString("pt-BR")}
${[ ["Entradas", fmtBRL(ins), "#15803D"], ["Saídas", fmtBRL(outs), "#B91C1C"], ["Saldo", fmtBRL(saldo), saldo>=0?"#111":"#B91C1C"], ["A receber", fmtBRL(aReceber), "#1E40AF"], ].map(([l,v,c]) => `
${l}
${v}
`).join("")}
`; const rows = filtered.map(f => ` ${fmtDate(f.date)} ${f.cat} ${f.ref} ${f.status === "confirmado" ? "Confirmado" : "A receber"} ${f.kind==="in"?"+":"−"} ${fmtBRL(f.value)} `).join(""); const body = `
${head} ${rows}
Data Categoria Descrição Status Valor
Documento de controle interno · sem valor fiscal
MOV-${new Date().getTime().toString(36).toUpperCase().slice(-6)} · ${ctx.state.company.name||""} · ${new Date().toLocaleString("pt-BR")}
`; const css = ` @page { size: A4; margin: 12mm 10mm; } * { box-sizing: border-box; } html, body { margin:0; padding:0; } body { font-family: Helvetica, Arial, sans-serif; color:#222; font-size:11px; line-height:1.5; background:#f0f0f0; } .brand-bar { height:3px; background:#D4310B; } table { table-layout: auto; word-wrap: break-word; } table td { padding:6px; border-bottom:1px solid #eee; font-size:10.5px; color:#222; } tbody tr:last-child td { border-bottom:none; } .num { text-align:right; font-variant-numeric:tabular-nums; } .toolbar { position:sticky; top:0; background:#1A1A1A; color:white; padding:10px 14px; display:flex; gap:8px; align-items:center; z-index:50; } .toolbar h3 { margin:0; flex:1; font-size:13px; font-weight:600; } .toolbar button { background:white; color:#1A1A1A; border:none; padding:7px 12px; border-radius:6px; font-weight:600; font-size:12px; cursor:pointer; font-family:Helvetica,Arial,sans-serif; } .doc { background:white; max-width:760px; margin:14px auto; padding:22px 24px; box-shadow:0 4px 24px rgba(0,0,0,.12); } @media print { .toolbar { display:none !important; } body { background:white !important; padding:0 !important; } .doc { box-shadow:none !important; margin:0 !important; max-width:none !important; width:auto !important; padding:0 !important; } } `; const win = window.open("", "_blank", "width=1000,height=900"); win.document.open(); win.document.write(`Movimentações — ${r.label}

Movimentações — ${r.label}

${body}
`); win.document.close(); }; return ( <>

Movimentações Financeiras

Entradas, saídas, saldo e previsões

} label="Entradas" value={fmtBRL(ins)} /> } label="Saídas" value={fmtBRL(outs)} /> =0 ? "brand" : "warning"} medal={} label="Saldo" value={fmtBRL(saldo)} sub="Soma do período" /> } label="A receber" value={fmtBRL(aReceber)} sub={`${pendingSalesCount} venda(s) a prazo`} />
} > {filtered.length === 0 ? (

Nenhum lançamento

Use "Lançar manual" ou registre operações nos PDVs.

) : ( {filtered.map(f => ( ))}
Data Categoria Descrição / Referência Status Valor
{fmtDate(f.date)} {f.cat} {f.ref} {f.status === "confirmado" ? Confirmado : A receber} {f.kind === "in" ? "+" : "−"} {fmtBRL(f.value)}
{f.status === "a_receber" && }
)}
{editing && setEditing(null)} onSave={(v, opts) => { if (editing === "new") ctx.actions.addFinance(v); else ctx.actions.updateFinance(editing.id, v); if (opts?.printReceipt) printFinanceReceipt(v, ctx.state.company); setEditing(null); }} />} ); } function FinanceModal({ ctx, initial, onClose, onSave }) { const methods = (ctx?.state?.settings?.methods) || ["PIX","Dinheiro","Cartão","Boleto 14d","Prazo 30d"]; const [kind, setKind] = useState(initial?.kind || "in"); const [cat, setCat] = useState(initial?.cat || "Despesa Operacional"); const [ref, setRef] = useState(initial?.ref || ""); const [date, setDate] = useState(initial?.date || new Date().toISOString().slice(0,10)); const [value, setValue] = useState(initial?.value ? String(initial.value) : ""); const [status, setStatus] = useState(initial?.status || "confirmado"); const [method, setMethod] = useState(initial?.method || methods[0] || "PIX"); const [err, setErr] = useState({}); const build = () => { const e = {}; if (!ref.trim()) e.ref = "Informe a descrição"; const v = parseFloat((value+"").replace(",", ".")); if (!v || v <= 0) e.value = "Valor inválido"; setErr(e); if (Object.keys(e).length) return null; return { kind, cat, ref: ref.trim(), date, value: v, status, method }; }; const submit = () => { const payload = build(); if (payload) onSave(payload); }; const submitAndPrint = () => { const payload = build(); if (payload) onSave(payload, { printReceipt: true }); }; const categoriesIn = ["Venda Distribuidor","Retorno Picolezeiro","Outras Entradas"]; const categoriesOut = ["Compra de Insumo","Comissão Picolezeiro","Produção","Despesa Operacional","Outras Saídas"]; const cats = kind === "in" ? categoriesIn : categoriesOut; // ensure category is valid for kind useEffect(() => { if (!cats.includes(cat)) setCat(cats[0]); }, [kind]); return ( } >
setDate(e.target.value)} placeholder="AAAA-MM-DD" /> setRef(e.target.value)} placeholder="Ex.: Conta de luz, Compra de polpa..." autoFocus /> setValue(e.target.value.replace(/[^\d.,]/g,""))} placeholder="0,00" />
); } /* Gera um comprovante imprimível (HTML) para um lançamento manual. Mesmo estilo do printList — header com logo, dados da empresa, bloco do lançamento e rodapé. */ function printFinanceReceipt(entry, company) { const isIn = entry.kind === "in"; const titulo = isIn ? "COMPROVANTE DE RECEBIMENTO" : "COMPROVANTE DE PAGAMENTO"; const statusLabel = entry.status === "confirmado" ? (isIn ? "Recebido" : "Pago") : (isIn ? "A receber" : "A pagar"); const logoIsImage = company?.logo && !/^data:application\/pdf/i.test(company.logo); const logoBlock = logoIsImage ? `` : `
P
`; const docId = "MOV-" + (entry.id || Date.now().toString(36)).toString().toUpperCase().slice(-8); const emittedAt = new Date().toLocaleString("pt-BR"); const valueColor = isIn ? "#15803D" : "#B91C1C"; const accentColor = isIn ? "#15803D" : "#B91C1C"; const body = `
${logoBlock}
${company?.name||""}
${company?.cnpj ? "CNPJ "+company.cnpj : ""} ${(company?.cnpj && company?.address) ? "
" : ""} ${company?.address||""} ${company?.phone ? "
"+company.phone : ""}
Documento
${docId}

${titulo}

Emitido em ${emittedAt}
Categoria${entry.cat||"—"}
Descrição${entry.ref||"—"}
Data${fmtDate(entry.date)}
Forma de pagamento${entry.method||"—"}
Status${statusLabel}
${isIn ? "Valor recebido" : "Valor pago"}
${entry.method||""}
${isIn ? "+" : "−"} ${fmtBRL(entry.value)}
Emissor
${company?.name||""}
${isIn ? "Pagador" : "Beneficiário"}
${entry.ref||"—"}
Documento de controle interno · sem valor fiscal
${docId} · ${company?.name||""} · ${emittedAt}
`; const css = ` @page { size: A4; margin: 14mm 12mm; } * { box-sizing: border-box; } html, body { margin:0; padding:0; } body { font-family: Helvetica, Arial, sans-serif; color:#222; font-size:11px; line-height:1.5; background:#f0f0f0; } .brand-bar { height:3px; background:${accentColor}; } .toolbar { position:sticky; top:0; background:#1A1A1A; color:white; padding:10px 14px; display:flex; gap:8px; align-items:center; z-index:50; } .toolbar h3 { margin:0; flex:1; font-size:13px; font-weight:600; } .toolbar button { background:white; color:#1A1A1A; border:none; padding:7px 12px; border-radius:6px; font-weight:600; font-size:12px; cursor:pointer; font-family:Helvetica,Arial,sans-serif; } .doc { background:white; max-width:680px; margin:14px auto; padding:26px 30px; box-shadow:0 4px 24px rgba(0,0,0,.12); } @media print { .toolbar { display:none !important; } body { background:white !important; padding:0 !important; } .doc { box-shadow:none !important; margin:0 !important; max-width:none !important; width:auto !important; padding:0 !important; } } `; const win = window.open("", "_blank", "width=900,height=900"); win.document.open(); win.document.write(`${titulo} — ${docId}

${titulo} — ${docId}

${body}
`); win.document.close(); } function CategoryDistribution({ finance }) { // group outs by category const groups = {}; finance.filter(f => f.kind === "out").forEach(f => { groups[f.cat] = (groups[f.cat] || 0) + f.value; }); const total = Object.values(groups).reduce((s, x) => s + x, 0); const colors = ["#D4310B", "#1A1A1A", "#A3A3A6", "#6B6B70", "#E04A1F"]; const entries = Object.entries(groups).sort((a,b)=>b[1]-a[1]); if (total === 0) return

Sem saídas no período.

; const segments = entries.map(([cat, v], i) => ({ value: v, color: colors[i % colors.length], label: cat })); return (
{segments.slice(0,5).map((s, i) => (
{s.label} {Math.round(s.value / total * 100)}%
))}
); } function DualBarChart({ data }) { const max = Math.max(...data.flatMap(d => [d.inV, d.outV]), 1); return (
{data.map((d, i) => (
0 ? 2 : 0 }} />
0 ? 2 : 0, opacity:.85 }} />
{d.label}
))}
); } function LegendDot({ color, label }) { return ( {label} ); } window.Movimentacoes = Movimentacoes;