// Workspace por Empresa - 4 abas
const { useState: useState_W, useMemo: useMemo_W, useEffect: useEffect_W } = React;
function WorkspaceEmpresa({ empresa, lancamentos, portadores, centrosCusto, formasPagamento, onBack, onUpsertLanc, onDeleteLanc, onPayLanc }) {
const [aba, setAba] = useState_W('contas');
const [novoLancHeader, setNovoLancHeader] = useState_W(null);
const hoje = todayISO();
const stats = useMemo_W(() => empresaStats(empresa, lancamentos, hoje), [empresa, lancamentos, hoje]);
return (
{/* Header da empresa */}
{empresa.nome.charAt(0)}
{empresa.nome}
{empresa.cnpj}
·
{empresa.segmento}
{empresa.responsavel && <>·Resp: {empresa.responsavel}>}
{/* Tabs */}
{[
{ id: 'contas', label: 'Contas a Pagar/Receber', icon: 'list', count: lancamentos.length },
{ id: 'portadores', label: 'Portadores', icon: 'bank' },
{ id: 'centros', label: 'Centros de Custo', icon: 'target' },
{ id: 'relatorio', label: 'Relatório', icon: 'chart' },
].map(t => (
))}
{/* Tab content */}
{aba === 'contas' &&
}
{aba === 'portadores' &&
}
{aba === 'centros' &&
}
{aba === 'relatorio' &&
}
{novoLancHeader && (
setNovoLancHeader(null)}>
setNovoLancHeader(null)}
onSave={(l) => {
onUpsertLanc({ ...l, empresaId: empresa.id });
setNovoLancHeader(null);
}}
/>
)}
);
}
// ----- Tab 1: Contas -----
function ContasTab({ empresa, lancamentos, portadores, centrosCusto, formasPagamento, onUpsertLanc, onDeleteLanc, onPayLanc }) {
const isMobile = useIsMobile();
const [filtros, setFiltros] = useState_W({
tipo: 'todos', status: 'todos', portador: 'todos', centroCusto: 'todos',
formaPgto: 'todos', dataIni: '', dataFim: '', busca: ''
});
const [novoLanc, setNovoLanc] = useState_W(null);
const [pgto, setPgto] = useState_W(null);
const [comprovante, setComprovante] = useState_W(null);
const [showImport, setShowImport] = useState_W(false);
const toast = useToast();
const hoje = todayISO();
const POR_PAGINA = 20;
const [pagina, setPagina] = useState_W(1);
const [inlineFormOpen, setInlineFormOpen] = useState_W(true);
const [fInline, setFInline] = useState_W({
tipo: 'saida',
descricao: '',
valor: '',
vencimento: todayISO(),
pago: false,
portadorId: portadores[0]?.id || '',
centroCustoId: centrosCusto.find(c => c.tipo === 'saida')?.id || centrosCusto[0]?.id || '',
formaPagamento: formasPagamento[0] || 'PIX',
parcelas: 1
});
const setInlineVal = (k, v) => {
setFInline(prev => {
const next = { ...prev, [k]: v };
if (k === 'tipo') {
next.centroCustoId = centrosCusto.find(c => c.tipo === v)?.id || '';
}
return next;
});
};
const ccsFiltradosInline = centrosCusto.filter(c => c.tipo === fInline.tipo);
const [errosInline, setErrosInline] = useState_W({});
const [salvandoInline, setSalvandoInline] = useState_W(false);
function addMonthsString(isoDateStr, numMonths) {
if (!isoDateStr) return '';
const d = new Date(isoDateStr + 'T12:00:00Z');
d.setUTCMonth(d.getUTCMonth() + numMonths);
return d.toISOString().split('T')[0];
}
async function submitInline(e) {
e?.preventDefault();
const e_val = {};
if (!fInline.tipo) e_val.tipo = 'Tipo obrigatório';
if (!fInline.descricao?.trim()) e_val.descricao = 'Descrição obrigatória';
const val = parseFloat(String(fInline.valor).replace(',', '.'));
if (!val || val <= 0) e_val.valor = 'Valor deve ser maior que zero';
if (!fInline.vencimento) e_val.vencimento = 'Vencimento obrigatório';
if (fInline.pago === null || fInline.pago === undefined) e_val.pago = 'Status pago obrigatório';
if (!fInline.portadorId) e_val.portadorId = 'Portador obrigatório';
if (!fInline.centroCustoId) e_val.centroCustoId = 'Centro de Custo obrigatório';
if (!fInline.formaPagamento) e_val.formaPagamento = 'Forma de Pagamento obrigatória';
if (Object.keys(e_val).length > 0) {
setErrosInline(e_val);
return;
}
setErrosInline({});
const numParc = parseInt(fInline.parcelas) || 1;
setSalvandoInline(true);
try {
if (numParc > 1) {
const baseVal = Math.floor((val * 100) / numParc) / 100;
const diff = Math.round((val - baseVal * numParc) * 100) / 100;
const ref = uid('parc');
const payloadArr = [];
for (let i = 0; i < numParc; i++) {
const vDate = i === 0 ? fInline.vencimento : addMonthsString(fInline.vencimento, i);
payloadArr.push({
...fInline,
id: uid('lanc'),
valor: i === numParc - 1 ? baseVal + diff : baseVal,
vencimento: vDate,
competencia: competenciaFromDate(vDate),
empresaId: empresa.id,
parcelaRef: ref,
parcelaNum: i + 1,
parcelaTotal: numParc,
descricao: `${fInline.descricao} (${i + 1}/${numParc})`
});
}
await onUpsertLanc(payloadArr);
} else {
await onUpsertLanc({
id: uid('lanc'),
...fInline,
valor: val,
empresaId: empresa.id,
competencia: competenciaFromDate(fInline.vencimento)
});
}
setFInline({
tipo: 'saida',
descricao: '',
valor: '',
vencimento: todayISO(),
pago: false,
portadorId: portadores[0]?.id || '',
centroCustoId: centrosCusto.find(c => c.tipo === 'saida')?.id || centrosCusto[0]?.id || '',
formaPagamento: formasPagamento[0] || 'PIX',
parcelas: 1
});
toast.push('Lançamento cadastrado com sucesso!', 'success');
} finally {
setSalvandoInline(false);
}
}
useEffect_W(() => setPagina(1), [filtros]);
const portMap = Object.fromEntries(portadores.map(p => [p.id, p]));
const ccMap = Object.fromEntries(centrosCusto.map(c => [c.id, c]));
const filtrados = lancamentos.filter(l => {
if (filtros.tipo !== 'todos' && l.tipo !== filtros.tipo) return false;
if (filtros.portador !== 'todos' && l.portadorId !== filtros.portador) return false;
if (filtros.centroCusto !== 'todos' && l.centroCustoId !== filtros.centroCusto) return false;
if (filtros.formaPgto !== 'todos' && l.formaPagamento !== filtros.formaPgto) return false;
if (filtros.dataIni && l.vencimento < filtros.dataIni) return false;
if (filtros.dataFim && l.vencimento > filtros.dataFim) return false;
if (filtros.busca && !l.descricao.toLowerCase().includes(filtros.busca.toLowerCase())) return false;
if (filtros.status !== 'todos') {
const s = lancStatus(l, hoje);
if (s !== filtros.status) return false;
}
return true;
}).sort((a, b) => a.vencimento.localeCompare(b.vencimento));
const totalFiltrados = filtrados.length;
const totalPaginas = Math.ceil(totalFiltrados / POR_PAGINA);
const paginados = filtrados.slice((pagina - 1) * POR_PAGINA, pagina * POR_PAGINA);
const totalReceber = filtrados.filter(l => l.tipo === 'entrada' && !l.pago).reduce((s, l) => s + (l.saldoRestante !== undefined ? l.saldoRestante : l.valor), 0);
const totalPagar = filtrados.filter(l => l.tipo === 'saida' && !l.pago).reduce((s, l) => s + (l.saldoRestante !== undefined ? l.saldoRestante : l.valor), 0);
const totalRecebido = filtrados.filter(l => l.tipo === 'entrada').reduce((s, l) => s + (l.totalPago !== undefined ? l.totalPago : (l.pago ? l.valor : 0)), 0);
const totalPago = filtrados.filter(l => l.tipo === 'saida').reduce((s, l) => s + (l.totalPago !== undefined ? l.totalPago : (l.pago ? l.valor : 0)), 0);
function clearFilters() {
setFiltros({ tipo: 'todos', status: 'todos', portador: 'todos', centroCusto: 'todos', formaPgto: 'todos', dataIni: '', dataFim: '', busca: '' });
}
const hasFilter = filtros.tipo !== 'todos' || filtros.status !== 'todos' || filtros.portador !== 'todos' || filtros.centroCusto !== 'todos' || filtros.formaPgto !== 'todos' || filtros.dataIni || filtros.dataFim || filtros.busca;
return (
{/* mini KPIs */}
l.tipo === 'entrada' && !l.pago).length} lançamentos`} />
l.tipo === 'saida' && !l.pago).length} lançamentos`} />
l.tipo === 'entrada' && (l.pago || l.totalPago > 0)).length} lançamentos`} />
l.tipo === 'saida' && (l.pago || l.totalPago > 0)).length} lançamentos`} />
{/* Rápido Cadastro de Lançamento */}
Rápido Cadastro de Lançamento
{inlineFormOpen && (
)}
{/* Filtros */}
setFiltros({ ...filtros, busca: e.target.value })} placeholder="Buscar descrição..." style={{ paddingLeft: 34, width: '100%' }} />
setFiltros({ ...filtros, tipo: e.target.value })} options={[
{ value: "todos", label: "Todos" },
{ value: "entrada", label: "Entradas" },
{ value: "saida", label: "Saídas" }
]} style={{ minWidth: isMobile ? 0 : 140 }} />
setFiltros({ ...filtros, status: e.target.value })} options={[
{ value: "todos", label: "Todos" },
{ value: "pago", label: "Pago" },
{ value: "parcial", label: "Parcial" },
{ value: "em-dia", label: "Em dia" },
{ value: "vencendo", label: "Vencendo" },
{ value: "vencido", label: "Vencido" }
]} style={{ minWidth: isMobile ? 0 : 140 }} />
setFiltros({ ...filtros, portador: e.target.value })} options={[
{ value: "todos", label: "Todos" },
...portadores.map(p => ({ value: p.id, label: p.nome }))
]} style={{ minWidth: isMobile ? 0 : 140 }} />
setFiltros({ ...filtros, centroCusto: e.target.value })} options={[
{ value: "todos", label: "Todos" },
...centrosCusto.map(c => ({ value: c.id, label: c.nome }))
]} style={{ minWidth: isMobile ? 0 : 140 }} />
setFiltros({ ...filtros, formaPgto: e.target.value })} options={[
{ value: "todos", label: "Todas" },
...formasPagamento.map(f => ({ value: f.nome, label: f.nome }))
]} style={{ minWidth: isMobile ? 0 : 140 }} />
setFiltros({ ...filtros, dataIni: e.target.value })} />
setFiltros({ ...filtros, dataFim: e.target.value })} />
{hasFilter && Limpar}
setShowImport(true)}>Importar XLSX
{/* Tabela */}
| Vencimento |
Tipo |
Descrição |
Centro de Custo |
Portador |
Forma Pgto. |
Valor |
Status |
Ações |
{paginados.map(l => {
const s = lancStatus(l, hoje);
const c = statusColor(s);
const port = portMap[l.portadorId];
const cc = ccMap[l.centroCustoId];
return (
|
{formatDate(l.vencimento)}
{l.competencia}
|
{l.tipo === 'entrada' ? 'Entrada' : 'Saída'}
|
{l.descricao}
|
{cc?.nome} |
{port?.nome}
|
{l.formaPagamento} |
{l.tipo === 'saida' && '-'}{formatBRL(l.valor)}
{l.statusPg === 'parcial' && (
Falta: {formatBRL(l.saldoRestante)} (pago {formatBRL(l.totalPago)})
)}
|
|
{(!l.pago || l.statusPg === 'parcial') && setPgto(l)}>Pagar}
{(l.pago || l.statusPg === 'parcial') && setComprovante(l)}>Comprovante}
|
);
})}
{totalFiltrados === 0 && (
{
setInlineFormOpen(true);
setTimeout(() => {
const el = document.getElementById('inline-descricao');
if (el) el.focus();
}, 50);
}}>Novo lançamento
} />
)}
{totalPaginas > 1 && (
Mostrando {((pagina-1)*POR_PAGINA)+1}–{Math.min(pagina*POR_PAGINA, totalFiltrados)}
de {totalFiltrados} lançamentos
setPagina(p => p - 1)}>
← Anterior
{Array.from({ length: Math.min(5, totalPaginas) }, (_, i) => {
let p;
if (totalPaginas <= 5) p = i + 1;
else if (pagina <= 3) p = i + 1;
else if (pagina >= totalPaginas - 2) p = totalPaginas - 4 + i;
else p = pagina - 2 + i;
return (
);
})}
setPagina(p => p + 1)}>
Próxima →
)}
{novoLanc && (
setNovoLanc(null)}>
setNovoLanc(null)}
onSave={(l) => {
onUpsertLanc({ ...l, empresaId: empresa.id });
toast.push(novoLanc.id ? 'Lançamento atualizado' : 'Lançamento criado');
setNovoLanc(null);
}}
/>
)}
{pgto && (
setPgto(null)}
onConfirm={(payload) => {
onPayLanc(pgto.id, payload);
toast.push(`${pgto.tipo === 'entrada' ? 'Recebimento' : 'Pagamento'} confirmado`);
setPgto(null);
}}
/>
)}
{comprovante && (
setComprovante(null)} />
)}
{showImport && (
setShowImport(false)}
onImport={(lancsArray) => {
lancsArray.forEach(l => {
onUpsertLanc({ ...l, empresaId: empresa.id });
});
toast.push(`${lancsArray.length} lançamentos importados`);
setShowImport(false);
}}
/>
)}
);
}
const iconBtn = {
background: 'transparent', border: '1px solid var(--c-border)', borderRadius: 6,
width: 28, height: 28, cursor: 'pointer', color: 'var(--c-text-muted)',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center'
};
class LancamentoErrorBoundary extends React.Component {
constructor(props) { super(props); this.state = { hasError: false, error: null }; }
static getDerivedStateFromError(error) { return { hasError: true, error }; }
render() {
if (this.state.hasError) {
return (
Ocorreu um erro ao carregar os dados deste lançamento. Por favor, feche e tente novamente.
{String(this.state.error)}
);
}
return this.props.children;
}
}
// ----- Form de lançamento -----
function LancamentoFormModal({ lanc, portadores, centrosCusto, formasPagamento, onClose, onSave }) {
const isMobile = useIsMobile();
const l = lanc || {};
const [f, setF] = useState_W({
id: l.id ?? uid('lanc'),
tipo: l.tipo ?? 'saida',
descricao: l.descricao ?? '',
valor: l.valor ?? '',
vencimento: l.vencimento ?? todayISO(),
competencia: l.competencia ?? competenciaFromDate(l.vencimento ?? todayISO()),
portadorId: l.portadorId ?? portadores[0]?.id,
centroCustoId: l.centroCustoId ?? centrosCusto[0]?.id,
formaPagamento: l.formaPagamento ?? formasPagamento[0] ?? '',
observacao: l.observacao ?? '',
pago: l.pago ?? false,
pagamento: l.pagamento ?? null,
});
const set = (k, v) => setF(prev => ({ ...prev, [k]: v }));
const ccsFiltrados = centrosCusto.filter(c => c.tipo === f.tipo);
const toast = useToast();
const [isParcelado, setIsParcelado] = useState_L(false);
const [numParcelas, setNumParcelas] = useState_L(2);
const [erros, setErros] = useState_L({});
const [salvando, setSalvando] = useState_L(false);
function addMonthsString(isoDateStr, numMonths) {
if (!isoDateStr) return '';
const d = new Date(isoDateStr + 'T12:00:00Z');
d.setUTCMonth(d.getUTCMonth() + numMonths);
return d.toISOString().split('T')[0];
}
function validarLancamento(f) {
const e = {};
if (!f.descricao?.trim()) e.descricao = 'Descrição obrigatória';
const val = parseFloat(String(f.valor).replace(',','.'));
if (!val || val <= 0) e.valor = 'Valor deve ser maior que zero';
if (!f.vencimento) e.vencimento = 'Data de vencimento obrigatória';
if (!f.tipo) e.tipo = 'Tipo obrigatório';
return e;
}
async function submit(e) {
e?.preventDefault();
const e_validation = validarLancamento(f);
if (Object.keys(e_validation).length > 0) {
setErros(e_validation);
return;
}
setErros({});
const cc = ccsFiltrados.find(c => c.id === f.centroCustoId) || ccsFiltrados[0];
const valOriginal = parseFloat(String(f.valor).replace(',','.'));
if (isParcelado && !lanc.id && numParcelas > 1) {
const baseVal = Math.floor((valOriginal * 100) / numParcelas) / 100;
const diff = Math.round((valOriginal - baseVal * numParcelas) * 100) / 100;
const parcelas = [];
const ref = uid('parc');
for (let i = 0; i < numParcelas; i++) {
const vDate = i === 0 ? f.vencimento : addMonthsString(f.vencimento, i);
parcelas.push({
...f,
id: uid('lanc'),
valor: i === numParcelas - 1 ? baseVal + diff : baseVal,
vencimento: vDate,
competencia: competenciaFromDate(vDate),
centroCustoId: cc.id,
parcelaRef: ref,
parcelaNum: i + 1,
parcelaTotal: numParcelas,
descricao: `${f.descricao} (${i + 1}/${numParcelas})`
});
}
setSalvando(true);
try {
await onSave(parcelas);
} finally {
setSalvando(false);
}
} else {
setSalvando(true);
try {
await onSave({ ...f, valor: valOriginal, centroCustoId: cc.id, competencia: competenciaFromDate(f.vencimento) });
} finally {
setSalvando(false);
}
}
}
return (
Cancelar
{salvando ? <> Salvando...> : (lanc.id ? 'Salvar' : 'Criar Lançamento')}
>}>
);
}
// ----- Pagamento -----
function PagamentoModal({ lanc, portadores, centrosCusto, onClose, onConfirm }) {
const isMobile = useIsMobile();
const saldoRestante = lanc.saldoRestante !== undefined ? lanc.saldoRestante : lanc.valor;
const [data, setData] = useState_W(todayISO());
const [portadorId, setPortadorId] = useState_W(lanc.portadorId);
const [valorPago, setValorPago] = useState_W(saldoRestante.toString());
const portMap = Object.fromEntries(portadores.map(p => [p.id, p]));
const isEntrada = lanc.tipo === 'entrada';
const toast = useToast();
function salvar() {
const errData = Validacao.required(data, 'Data');
if (errData) return toast.push(errData, 'error');
const errPortador = Validacao.required(portadorId, 'Portador');
if (errPortador) return toast.push(errPortador, 'error');
const val = parseFloat(valorPago.replace(',', '.'));
if (isNaN(val) || val <= 0) return toast.push('O valor pago deve ser maior que zero', 'error');
if (val > saldoRestante) return toast.push('O valor pago não pode ser maior que o saldo restante (' + formatBRL(saldoRestante) + ')', 'error');
onConfirm({
data,
portadorId,
valor: val,
comprovante: `CMP-${Math.floor(Math.random() * 90000 + 10000)}.pdf`
});
}
return (
Cancelar
Confirmar {isEntrada ? 'Recebimento' : 'Pagamento'}
>}>
{lanc.pagamentosParciais && lanc.pagamentosParciais.length > 0 && (
Histórico de pagamentos parciais
{lanc.pagamentosParciais.map((p, idx) => (
{idx + 1}º Pagamento ({formatDate(p.data)}) - {portMap[p.portadorId]?.nome || 'Portador'}
{formatBRL(p.valor)}
))}
Total Pago
{formatBRL(lanc.totalPago)}
)}
{isEntrada ? 'Receber de' : 'Pagar para'}
{lanc.descricao}
Vence em {formatDate(lanc.vencimento)} · {lanc.formaPagamento}
{formatBRL(saldoRestante)}
{lanc.pagamentosParciais && lanc.pagamentosParciais.length > 0 &&
Valor original: {formatBRL(lanc.valor)}
}
Um comprovante será gerado automaticamente e o saldo da conta será atualizado.
);
}
// ----- Comprovante -----
function ComprovanteModal({ lanc, empresa, portador, centro, onClose }) {
const handlePrint = () => {
let pgsHtml = '';
if (lanc.pagamentosParciais && lanc.pagamentosParciais.length > 0) {
pgsHtml = `
PAGAMENTOS PARCIAIS
${lanc.pagamentosParciais.map((p, idx) => `
${idx + 1}º Pagto (${formatDate(p.data)})
${formatBRL(p.valor)}
`).join('')}
Total Pago
${formatBRL(lanc.totalPago)}
${lanc.statusPg === 'parcial' ? `
Saldo Restante
${formatBRL(lanc.saldoRestante)}
` : ''}
`;
}
const html = `
Empresa${empresa.nome}
CNPJ${empresa.cnpj || '—'}
Descrição${lanc.descricao}
Valor Original${formatBRL(lanc.valor)}
Centro de Custo${centro?.nome || ''}
Portador${portador?.nome || ''}
Forma de Pagamento${lanc.formaPagamento || ''}
Vencimento${formatDate(lanc.vencimento)}
${lanc.pagamento ? `
Data de Quitação${formatDate(lanc.pagamento.data)}
` : ''}
Competência${lanc.competencia || ''}
${lanc.pagamento?.comprovante ? `
Nº Comprovante${lanc.pagamento.comprovante}
` : ''}
${pgsHtml}
`;
imprimirPDF(html, 'Comprovante - ' + lanc.descricao);
};
return (
Imprimir Comprovante}>
COMPROVANTE DE {lanc.tipo === 'entrada' ? 'RECEBIMENTO' : 'PAGAMENTO'}
{formatBRL(lanc.statusPg === 'parcial' ? lanc.totalPago : lanc.valor)}
|
{lanc.pagamento &&
|
}
|
{lanc.pagamento?.comprovante &&
|
}
{lanc.pagamentosParciais && lanc.pagamentosParciais.length > 0 && (
Histórico de Parcelas Pagas
{lanc.pagamentosParciais.map((p, idx) => (
{idx + 1}º Pagto ({formatDate(p.data)})
{formatBRL(p.valor)}
))}
Total Pago
{formatBRL(lanc.totalPago)}
{lanc.statusPg === 'parcial' && (
Saldo Restante
{formatBRL(lanc.saldoRestante)}
)}
)}
);
}
function Row({ k, v, mono }) {
return (
{k}
{v}
);
}
// ----- Tab 2: Portadores -----
function PortadoresTab({ empresa, lancamentos, portadores }) {
const isMobile = useIsMobile();
const saldos = portadorSaldos(lancamentos, portadores);
const totalEnt = saldos.reduce((s, p) => s + p.entradas, 0);
const totalSai = saldos.reduce((s, p) => s + p.saidas, 0);
const [selPort, setSelPort] = useState_W(saldos[0]?.id);
const lancsPort = lancamentos.filter(l => l.portadorId === selPort && l.pago).sort((a, b) => (b.pagamento?.data || '').localeCompare(a.pagamento?.data || ''));
const sel = saldos.find(p => p.id === selPort);
return (
{saldos.map(p => (
setSelPort(p.id)} style={{
background: 'var(--c-surface)', borderRadius: 12, padding: 16, cursor: 'pointer',
border: `2px solid ${selPort === p.id ? p.cor : 'var(--c-border)'}`, transition: 'all 0.15s'
}}>
= 0 ? 'var(--c-text)' : '#dc2626', letterSpacing: '-0.02em' }}>
{formatBRL(p.saldo)}
↓ {formatBRL(p.entradas)}
↑ {formatBRL(p.saidas)}
))}
Extrato — {sel?.nome}
{lancsPort.length} movimentações realizadas
Saldo
{formatBRL(sel?.saldo)}
| Data Pgto. |
Descrição |
Forma |
Tipo |
Valor |
{lancsPort.map(l => (
| {l.pagamento && formatDate(l.pagamento.data)} |
{l.descricao} |
{l.formaPagamento} |
{l.tipo === 'entrada' ? '+ Entrada' : '- Saída'} |
{l.tipo === 'entrada' ? '+' : '-'} {formatBRL(l.valor)}
|
))}
{lancsPort.length === 0 && }
);
}
// ----- Tab 3: Centros de Custo -----
function CentrosTab({ empresa, lancamentos, centrosCusto }) {
const isMobile = useIsMobile();
const stats = centroCustoStats(lancamentos, centrosCusto);
const entradas = stats.filter(c => c.tipo === 'entrada');
const saidas = stats.filter(c => c.tipo === 'saida');
const totalEnt = entradas.reduce((s, c) => s + c.total, 0);
const totalSai = saidas.reduce((s, c) => s + c.total, 0);
function bar({ stats, totalGeral, cor, titulo }) {
return (
{titulo}
{formatBRL(totalGeral)}
{stats.map(c => {
const pct = totalGeral > 0 ? (c.total / totalGeral) * 100 : 0;
return (
{c.nome}
{formatBRL(c.total)}
{pct.toFixed(1)}%
{c.qtd} lançamento{c.qtd !== 1 ? 's' : ''}
Pendente: 0 ? 'var(--c-amber-fg)' : 'var(--c-text-muted)' }}>{formatBRL(c.pendente)}
);
})}
);
}
return (
s + c.qtd, 0)} lançamentos em ${entradas.length} centros`} />
s + c.qtd, 0)} lançamentos em ${saidas.length} centros`} />
{bar({ stats: entradas, totalGeral: totalEnt, cor: '#16a34a', titulo: 'Entradas por Centro' })}
{bar({ stats: saidas, totalGeral: totalSai, cor: '#dc2626', titulo: 'Saídas por Centro' })}
);
}
// ----- Tab 4: Relatório -----
function RelatorioTab({ empresa, lancamentos, portadores, centrosCusto, formasPagamento }) {
const isMobile = useIsMobile();
const hoje = todayISO();
const toast = useToast();
// Distribuição forma de pagamento
const formasDist = formasPagamento.map((f, i) => {
const v = lancamentos.filter(l => l.formaPagamento === f).reduce((s, l) => s + l.valor, 0);
return { label: f, value: v, color: ['#4F46E5', '#0EA5E9', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#14B8A6'][i % 8] };
}).filter(d => d.value > 0);
const ccDist = centroCustoStats(lancamentos, centrosCusto).filter(c => c.total > 0);
const ccColors = ['#4F46E5', '#0EA5E9', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#14B8A6', '#F97316'];
// Por mês
const meses = useMemo_W(() => {
const all = [...new Set(lancamentos.map(l => l.competencia))];
return all.sort((a, b) => { const [ma, ya] = a.split('/'); const [mb, yb] = b.split('/'); return (ya + ma).localeCompare(yb + mb); });
}, [lancamentos]);
const seriesMes = meses.map(m => ({
label: m,
entrada: lancamentos.filter(l => l.competencia === m && l.tipo === 'entrada').reduce((s, l) => s + l.valor, 0),
saida: lancamentos.filter(l => l.competencia === m && l.tipo === 'saida').reduce((s, l) => s + l.valor, 0),
}));
return (
Relatório Financeiro
Análises e exportação para Excel
{ exportEmpresaXLSX(empresa, lancamentos, portadores, centrosCusto); toast.push('Arquivo XLSX gerado'); }}>
Exportar Excel (4 abas)
Distribuição por Forma de Pagamento
Volume total movimentado por meio
s + d.value, 0)).replace('R$', '').trim().slice(0, 8)} />
{formasDist.map((f, i) => (
{f.label}
{formatBRL(f.value)}
))}
Distribuição por Centro de Custo
Para onde está indo o dinheiro
({ label: c.nome, value: c.total, color: ccColors[i % ccColors.length] }))}
size={170} thickness={28}
centerLabel="Itens" centerValue={ccDist.length}
/>
{ccDist.map((c, i) => (
{c.nome}
{formatBRL(c.total).replace('R$', '').trim().slice(0, 10)}
))}
Entradas vs Saídas por Competência
Histórico mensal completo
s.entrada) },
{ name: 'Saídas', color: '#dc2626', points: seriesMes.map(s => s.saida) },
]}
labels={seriesMes.map(s => s.label)}
height={240}
/>
Resumo Executivo
Resultado consolidado por competência
| Competência |
Entradas |
Saídas |
Resultado |
{seriesMes.map(s => {
const res = s.entrada - s.saida;
return (
| {s.label} |
{formatBRL(s.entrada)} |
{formatBRL(s.saida)} |
= 0 ? '#16a34a' : '#dc2626' }}>{formatBRL(res)} |
);
})}
| TOTAL |
{formatBRL(seriesMes.reduce((s, x) => s + x.entrada, 0))} |
{formatBRL(seriesMes.reduce((s, x) => s + x.saida, 0))} |
{formatBRL(seriesMes.reduce((s, x) => s + x.entrada - x.saida, 0))} |
);
}
function ModalImportarXLSX({ portadores, centrosCusto, onClose, onImport }) {
const [step, setStep] = useState_W(1);
const [dataRows, setDataRows] = useState_W([]);
const [colNames, setColNames] = useState_W([]);
const [map, setMap] = useState_W({ data: '', descricao: '', valor: '', tipo: '' });
function handleFile(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
const bstr = evt.target.result;
const wb = XLSX.read(bstr, { type: 'binary' });
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
const rows = XLSX.utils.sheet_to_json(ws, { header: 1 });
if (rows.length > 0) {
setColNames(rows[0] || []);
setDataRows(rows.slice(1).filter(r => r.length > 0));
setStep(2);
}
};
reader.readAsBinaryString(file);
}
const toast = useToast();
function doImport() {
const errData = Validacao.required(map.data, 'Coluna de Data');
if (errData) return toast.push(errData, 'error');
const errDesc = Validacao.required(map.descricao, 'Coluna de Descrição');
if (errDesc) return toast.push(errDesc, 'error');
const errValor = Validacao.required(map.valor, 'Coluna de Valor');
if (errValor) return toast.push(errValor, 'error');
const idx = {
data: colNames.indexOf(map.data),
descricao: colNames.indexOf(map.descricao),
valor: colNames.indexOf(map.valor),
tipo: colNames.indexOf(map.tipo)
};
const lancs = dataRows.map(r => {
let v = r[idx.valor] || 0;
if (typeof v === 'string') v = parseFloat(v.replace(/[R$\s.]/g, '').replace(',','.')) || 0;
let dt = r[idx.data];
if (typeof dt === 'number') dt = new Date((dt - 25569) * 86400 * 1000).toISOString().split('T')[0];
else if (typeof dt === 'string' && dt.includes('/')) {
const p = dt.split('/'); dt = `${p[2]}-${p[1]}-${p[0]}`;
} else { dt = todayISO(); }
const tStr = String(r[idx.tipo]).toLowerCase();
const isEntrada = tStr.includes('entrada') || tStr.includes('receita') || tStr === 'c';
return {
descricao: String(r[idx.descricao] || 'Importado').slice(0, 80),
valor: Math.abs(v),
tipo: isEntrada ? 'entrada' : 'saida',
vencimento: dt,
competencia: competenciaFromDate(dt),
portadorId: portadores[0]?.id,
centroCustoId: centrosCusto.find(c => c.tipo === (isEntrada ? 'entrada' : 'saida'))?.id || centrosCusto[0]?.id,
formaPagamento: 'Transferência',
pago: false
};
});
onImport(lancs);
}
return (
Cancelar
{step === 2 && Confirmar Importação ({dataRows.length} linhas)}
>
}>
{step === 1 && (
)}
{step === 2 && (
Mapeie as colunas da sua planilha para os campos do sistema:
setMap({...map, data: e.target.value})} options={[{value:'',label:'Selecione...'}, ...colNames.map(c => ({value:c,label:c}))]} />
setMap({...map, descricao: e.target.value})} options={[{value:'',label:'Selecione...'}, ...colNames.map(c => ({value:c,label:c}))]} />
setMap({...map, valor: e.target.value})} options={[{value:'',label:'Selecione...'}, ...colNames.map(c => ({value:c,label:c}))]} />
setMap({...map, tipo: e.target.value})} options={[{value:'',label:'Selecione...'}, ...colNames.map(c => ({value:c,label:c}))]} />
)}
);
}
Object.assign(window, { WorkspaceEmpresa, ModalImportarXLSX, PagamentoModal, ComprovanteModal });