/* 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]) => `
`).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}
Data
Categoria
Descrição
Status
Valor
${rows}
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} Imprimir / PDF Fechar
`);
win.document.close();
};
return (
<>
Movimentações Financeiras
Entradas, saídas, saldo e previsões
} onClick={() => exportFinanceCsv(all, period)}>Exportar CSV
} onClick={doImport}>Importar
} onClick={printList}>Imprimir / PDF
} onClick={() => setEditing("new")}>Lançar manual
} 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.
) : (
Data
Categoria
Descrição / Referência
Status
Valor
{filtered.map(f => (
{fmtDate(f.date)}
{f.cat}
{f.ref}
{f.status === "confirmado" ? Confirmado : A receber }
{f.kind === "in" ? "+" : "−"} {fmtBRL(f.value)}
{f.status === "a_receber" && markPaid(f)}> }
setEditing(f)}>
onDelete(f)}>
))}
)}
{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 (
Cancelar
} onClick={submitAndPrint}>
{initial ? "Salvar e gerar comprovante" : "Lançar e gerar comprovante"}
{initial ? "Salvar" : "Lançar"}
>
}
>
setCat(e.target.value)}>
{cats.map(c => {c} )}
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" />
setMethod(e.target.value)}>
{methods.map(m => {m} )}
setStatus(e.target.value)}>
{kind === "in" ? "Recebido" : "Pago"}
{kind === "in" ? "A receber" : "A pagar"}
);
}
/* 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 : ""}
${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} Imprimir / PDF Fechar
`);
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 ;
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;