adicionado extrato e simulação

This commit is contained in:
Diego Freitas 2026-06-02 22:28:10 -03:00
parent 66287cff29
commit 759e642567
26 changed files with 3317 additions and 163 deletions

View File

@ -13,6 +13,7 @@ const movimentosFixosRoutes = require('./modules/movimentosFixos/routes/moviment
const usuariosRoutes = require('./modules/usuarios/routes/usuarios.routes');
const quitacoesCreditoRoutes = require('./modules/quitacoesCredito/routes/quitacoesCredito.routes');
const dashboardRoutes = require('./modules/dashboard/routes/dashboard.routes');
const simulacaoMensalRoutes = require('./modules/simulacaoMensal/routes/simulacaoMensal.routes');
const app = express();
@ -48,6 +49,7 @@ app.use('/api/movimentos-fixos', movimentosFixosRoutes);
app.use('/api/usuarios', usuariosRoutes);
app.use('/api/quitacoes-credito', quitacoesCreditoRoutes);
app.use('/api/dashboard', dashboardRoutes);
app.use('/api/simulacao-mensal', simulacaoMensalRoutes);
app.use((req, res) => {
return res.status(404).json({

View File

@ -238,7 +238,18 @@ async function preverImpactoSaldo(req, res) {
});
}
const impacto = await movimentosService.preverImpactoSaldo(req.body, req.body.idcontasapagar);
const idUsuario =
req.usuario?.idusuarios ||
req.user?.idusuarios ||
req.usuario?.id ||
req.user?.id ||
null;
const impacto = await movimentosService.preverImpactoSaldo(
req.body,
req.body.idcontasapagar,
idUsuario
);
return res.json({
ok: true,

View File

@ -791,6 +791,7 @@ async function criarMovimento(idUsuario, dados) {
tipo_operacao: 'movimento_aplicado',
origem: movimentoCriado.origem || 'movimento',
idusuarios: idUsuario,
permitirBancoDestinoGlobal: movimentoCriado.movimento === 'Sangria',
}
);
@ -882,6 +883,7 @@ async function atualizarMovimento(id, idUsuario, dados) {
tipo_operacao: 'movimento_revertido',
origem: 'edicao_movimento',
idusuarios: idUsuario,
permitirBancoDestinoGlobal: movimentoAntigo.movimento === 'Sangria',
}
);
@ -902,6 +904,7 @@ async function atualizarMovimento(id, idUsuario, dados) {
tipo_operacao: 'movimento_aplicado',
origem: 'edicao_movimento',
idusuarios: idUsuario,
permitirBancoDestinoGlobal: movimentoNovo.movimento === 'Sangria',
}
);
@ -977,6 +980,7 @@ async function deletarMovimento(id, idUsuario) {
tipo_operacao: 'movimento_revertido',
origem: 'exclusao_movimento',
idusuarios: idUsuario,
permitirBancoDestinoGlobal: movimentoAntigo.movimento === 'Sangria',
}
);
@ -1017,20 +1021,28 @@ async function deletarMovimento(id, idUsuario) {
}
}
async function preverImpactoSaldo(dados, idcontasapagar = null) {
async function preverImpactoSaldo(dados, idcontasapagar = null, idUsuario = null) {
let movimentoAntigo = null;
if (idcontasapagar) {
movimentoAntigo = await buscarMovimentoPorId(idcontasapagar);
movimentoAntigo = await buscarMovimentoPorId(idcontasapagar, {
idUsuario,
});
}
const movimentoNovo = normalizarMovimentoParaSalvar(null, dados, {
idusuarios_cad: dados.idusuarios_cad || null,
const movimentoNovo = normalizarMovimentoParaSalvar(idUsuario, dados, {
idusuarios_cad: dados.idusuarios_cad || idUsuario || null,
grupo_parcelamento: dados.grupo_parcelamento || null,
origem: dados.origem || 'manual',
});
return movimentosSaldoService.preverImpactoSaldo(movimentoNovo, movimentoAntigo);
return movimentosSaldoService.preverImpactoSaldo(
movimentoNovo,
movimentoAntigo,
{
idusuarios: idUsuario || null,
}
);
}
module.exports = {

View File

@ -12,6 +12,70 @@ function bancoEhCredito(banco) {
return Number(banco?.debito) === 0;
}
function calcularPercentualDelta(saldoAnterior, valorDelta) {
const anterior = Number(saldoAnterior || 0);
const delta = Number(valorDelta || 0);
if (anterior === 0) {
if (delta === 0) return 0;
return null;
}
return Number(((delta / Math.abs(anterior)) * 100).toFixed(2));
}
function resolverDirecao(valorDelta) {
const delta = Number(valorDelta || 0);
if (delta > 0) return 'alta';
if (delta < 0) return 'queda';
return 'neutro';
}
function resolverTipoVisual(valorDelta) {
const delta = Number(valorDelta || 0);
if (delta > 0) return 'positivo';
if (delta < 0) return 'negativo';
return 'neutro';
}
function montarDescricaoBanco(banco) {
if (!banco) return null;
const usuarioNome = banco.usuario_nome || 'Usuário sem nome';
const bancoNome = banco.banco_nome || banco.descricao || `Banco ${banco.idbancos}`;
return `${usuarioNome} - ${bancoNome}`;
}
function montarImpactoSaldo(delta, banco) {
const saldoAnterior = Number(banco?.saldo || 0);
const valorDelta = Number(delta.valor_delta || 0);
const saldoPosterior = saldoAnterior + valorDelta;
const percentualDelta = calcularPercentualDelta(saldoAnterior, valorDelta);
return {
idbancos: Number(delta.idbancos),
banco_descricao: montarDescricaoBanco(banco) || `Banco ${delta.idbancos}`,
banco_nome: banco?.banco_nome || banco?.descricao || null,
usuario_nome: banco?.usuario_nome || null,
valor_delta: valorDelta,
saldo_anterior: saldoAnterior,
saldo_posterior: saldoPosterior,
percentual_delta: percentualDelta,
direcao: resolverDirecao(valorDelta),
tipo_visual: resolverTipoVisual(valorDelta),
descricao: delta.descricao || null,
};
}
async function buscarBanco(connection, idbancos, idusuarios = null) {
if (!idbancos) return null;
@ -19,22 +83,27 @@ async function buscarBanco(connection, idbancos, idusuarios = null) {
let filtroUsuario = '';
if (idusuarios) {
filtroUsuario = 'AND idusuarios = ?';
filtroUsuario = 'AND b.idusuarios = ?';
params.push(idusuarios);
}
const [rows] = await connection.query(
`
SELECT
idbancos,
descricao,
saldo,
debito,
investimento,
idusuarios
FROM bancos
WHERE idbancos = ?
${filtroUsuario}
b.idbancos,
b.descricao,
b.descricao AS banco_nome,
b.saldo,
b.debito,
b.investimento,
b.idusuarios,
u.nome AS usuario_nome
FROM bancos b
LEFT JOIN usuarios u
ON u.idusuarios = b.idusuarios
WHERE b.idbancos = ?
AND b.habilitado = 1
${filtroUsuario}
LIMIT 1
`,
params
@ -50,22 +119,27 @@ async function buscarBancoParaUpdate(connection, idbancos, idusuarios = null) {
let filtroUsuario = '';
if (idusuarios) {
filtroUsuario = 'AND idusuarios = ?';
filtroUsuario = 'AND b.idusuarios = ?';
params.push(idusuarios);
}
const [rows] = await connection.query(
`
SELECT
idbancos,
descricao,
saldo,
debito,
investimento,
idusuarios
FROM bancos
WHERE idbancos = ?
${filtroUsuario}
b.idbancos,
b.descricao,
b.descricao AS banco_nome,
b.saldo,
b.debito,
b.investimento,
b.idusuarios,
u.nome AS usuario_nome
FROM bancos b
LEFT JOIN usuarios u
ON u.idusuarios = b.idusuarios
WHERE b.idbancos = ?
AND b.habilitado = 1
${filtroUsuario}
LIMIT 1
FOR UPDATE
`,
@ -168,8 +242,27 @@ function calcularDeltasParaMovimento(movimento, bancoOrigem, bancoDestino = null
}
async function calcularImpactoMovimento(connection, movimento, contexto = {}) {
const bancoOrigem = await buscarBanco(connection, movimento.idbancos, contexto.idusuarios);
const bancoDestino = await buscarBanco(connection, movimento.idbancos_p, contexto.idusuarios);
const bancoOrigem = await buscarBanco(
connection,
movimento.idbancos,
contexto.idusuarios || null
);
let bancoDestino = null;
if (movimento.movimento === 'Sangria') {
bancoDestino = await buscarBanco(
connection,
movimento.idbancos_p,
null
);
} else {
bancoDestino = await buscarBanco(
connection,
movimento.idbancos_p,
contexto.idusuarios || null
);
}
return calcularDeltasParaMovimento(movimento, bancoOrigem, bancoDestino);
}
@ -186,15 +279,26 @@ async function aplicarDeltasSaldo(connection, deltas, contexto = {}) {
const impactos = [];
for (const delta of deltas) {
const banco = await buscarBancoParaUpdate(connection, delta.idbancos, contexto.idusuarios);
const filtrarPorUsuario =
contexto.validarUsuario === false
? null
: contexto.idusuarios || null;
let banco = await buscarBancoParaUpdate(
connection,
delta.idbancos,
filtrarPorUsuario
);
if (!banco && contexto.permitirBancoDestinoGlobal === true) {
banco = await buscarBancoParaUpdate(connection, delta.idbancos, null);
}
if (!banco) {
continue;
}
const saldoAnterior = Number(banco.saldo || 0);
const valorDelta = Number(delta.valor_delta || 0);
const saldoPosterior = saldoAnterior + valorDelta;
const impacto = montarImpactoSaldo(delta, banco);
await connection.query(
`
@ -205,7 +309,7 @@ async function aplicarDeltasSaldo(connection, deltas, contexto = {}) {
WHERE idbancos = ?
`,
[
saldoPosterior,
impacto.saldo_posterior,
banco.idbancos,
]
);
@ -232,42 +336,65 @@ async function aplicarDeltasSaldo(connection, deltas, contexto = {}) {
contexto.idcontasquitadas || null,
contexto.tipo_operacao || null,
contexto.origem || null,
valorDelta,
saldoAnterior,
saldoPosterior,
impacto.valor_delta,
impacto.saldo_anterior,
impacto.saldo_posterior,
delta.descricao || null,
contexto.idusuarios || null,
]
);
impactos.push({
idbancos: banco.idbancos,
banco_descricao: banco.descricao,
valor_delta: valorDelta,
saldo_anterior: saldoAnterior,
saldo_posterior: saldoPosterior,
descricao: delta.descricao || null,
});
impactos.push(impacto);
}
return impactos;
}
async function preverImpactoSaldo(dados, movimentoAntigo = null) {
async function enriquecerDeltasComoPrevisao(connection, deltas) {
const impactos = [];
for (const delta of deltas) {
const banco = await buscarBanco(connection, delta.idbancos, null);
if (!banco) {
continue;
}
impactos.push(montarImpactoSaldo(delta, banco));
}
return impactos;
}
async function preverImpactoSaldo(dados, movimentoAntigo = null, contexto = {}) {
const connection = await pool.getConnection();
try {
const deltas = [];
if (movimentoAntigo) {
const deltasAntigos = await calcularImpactoMovimento(connection, movimentoAntigo);
const deltasAntigos = await calcularImpactoMovimento(
connection,
movimentoAntigo,
{
idusuarios: contexto.idusuarios || null,
}
);
deltas.push(...inverterDeltas(deltasAntigos));
}
const deltasNovos = await calcularImpactoMovimento(connection, dados);
const deltasNovos = await calcularImpactoMovimento(
connection,
dados,
{
idusuarios: contexto.idusuarios || null,
}
);
deltas.push(...deltasNovos);
return deltas;
return enriquecerDeltasComoPrevisao(connection, deltas);
} finally {
connection.release();
}

View File

@ -1,5 +1,15 @@
const movimentosFixosService = require('../services/movimentosFixos.service');
function extrairIdUsuarioLogado(req) {
return (
req.usuario?.idusuarios ||
req.user?.idusuarios ||
req.usuario?.id ||
req.user?.id ||
null
);
}
function validarDadosMovimentoFixo(dados) {
if (!dados.movimento) {
return 'Movimento é obrigatório.';
@ -43,6 +53,14 @@ function validarDadosMovimentoFixo(dados) {
}
}
if (dados.idclientes !== undefined && dados.idclientes !== null && dados.idclientes !== '') {
const idclientes = Number(dados.idclientes);
if (!Number.isInteger(idclientes) || idclientes <= 0) {
return 'Cliente inválido.';
}
}
if (dados.idbancos !== undefined && dados.idbancos !== null && dados.idbancos !== '') {
const idbancos = Number(dados.idbancos);
@ -64,6 +82,8 @@ function validarDadosMovimentoFixo(dados) {
async function listar(req, res) {
try {
const idUsuario = extrairIdUsuarioLogado(req);
const resultado = await movimentosFixosService.listarMovimentosFixos({
limite: req.query.limite,
offset: req.query.offset,
@ -72,11 +92,13 @@ async function listar(req, res) {
movimento: req.query.movimento,
idbancos: req.query.idbancos,
idcentrodecustos: req.query.idcentrodecustos,
idclientes: req.query.idclientes,
habilitado: req.query.habilitado,
diaInicio: req.query.diaInicio,
diaFim: req.query.diaFim,
orderBy: req.query.orderBy,
orderDirection: req.query.orderDirection,
idUsuario,
});
return res.json({
@ -97,6 +119,8 @@ async function listar(req, res) {
async function buscarPorId(req, res) {
try {
const idUsuario = extrairIdUsuarioLogado(req);
const id = Number(req.params.id);
if (!id) {
@ -106,7 +130,7 @@ async function buscarPorId(req, res) {
});
}
const movimentoFixo = await movimentosFixosService.buscarMovimentoFixoPorId(id);
const movimentoFixo = await movimentosFixosService.buscarMovimentoFixoPorId(id, idUsuario);
if (!movimentoFixo) {
return res.status(404).json({
@ -131,6 +155,8 @@ async function buscarPorId(req, res) {
async function criar(req, res) {
try {
const idUsuario = extrairIdUsuarioLogado(req);
const erroValidacao = validarDadosMovimentoFixo(req.body);
if (erroValidacao) {
@ -140,7 +166,7 @@ async function criar(req, res) {
});
}
const novoMovimentoFixo = await movimentosFixosService.criarMovimentoFixo(req.body);
const novoMovimentoFixo = await movimentosFixosService.criarMovimentoFixo(req.body, idUsuario);
return res.status(201).json({
ok: true,
@ -159,6 +185,8 @@ async function criar(req, res) {
async function atualizar(req, res) {
try {
const idUsuario = extrairIdUsuarioLogado(req);
const id = Number(req.params.id);
if (!id) {
@ -177,7 +205,7 @@ async function atualizar(req, res) {
});
}
const movimentoAtualizado = await movimentosFixosService.atualizarMovimentoFixo(id, req.body);
const movimentoAtualizado = await movimentosFixosService.atualizarMovimentoFixo(id, req.body, idUsuario);
if (!movimentoAtualizado) {
return res.status(404).json({
@ -203,6 +231,8 @@ async function atualizar(req, res) {
async function alterarHabilitado(req, res) {
try {
const idUsuario = extrairIdUsuarioLogado(req);
const id = Number(req.params.id);
if (!id) {
@ -222,7 +252,7 @@ async function alterarHabilitado(req, res) {
}
const movimentoAtualizado =
await movimentosFixosService.alterarHabilitadoMovimentoFixo(id, habilitado);
await movimentosFixosService.alterarHabilitadoMovimentoFixo(id, habilitado, idUsuario);
if (!movimentoAtualizado) {
return res.status(404).json({
@ -250,6 +280,8 @@ async function alterarHabilitado(req, res) {
async function deletar(req, res) {
try {
const idUsuario = extrairIdUsuarioLogado(req);
const id = Number(req.params.id);
if (!id) {
@ -259,7 +291,7 @@ async function deletar(req, res) {
});
}
const deletado = await movimentosFixosService.deletarMovimentoFixo(id);
const deletado = await movimentosFixosService.deletarMovimentoFixo(id, idUsuario);
if (!deletado) {
return res.status(404).json({

View File

@ -7,18 +7,21 @@ const CAMPOS_MOVIMENTO_FIXO_SELECT = `
mf.valor,
mf.dia_vencimento,
mf.idcentrodecustos,
mf.idclientes,
mf.habilitado,
mf.idbancos,
mf.insert_date,
mf.update_date,
b.descricao AS banco_descricao,
cc.descricao AS centro_custo_descricao
cc.descricao AS centro_custo_descricao,
c.nome AS cliente_nome
`;
const FROM_MOVIMENTO_FIXO_JOIN = `
FROM movimentosfixos mf
LEFT JOIN bancos b ON b.idbancos = mf.idbancos
INNER JOIN bancos b ON b.idbancos = mf.idbancos
LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = mf.idcentrodecustos
LEFT JOIN clientes c ON c.idclientes = mf.idclientes
`;
function normalizarTextoOuNull(valor) {
@ -91,12 +94,14 @@ function resolverOrdenacao(orderBy) {
valor: 'mf.valor',
dia_vencimento: 'mf.dia_vencimento',
idcentrodecustos: 'mf.idcentrodecustos',
idclientes: 'mf.idclientes',
idbancos: 'mf.idbancos',
habilitado: 'mf.habilitado',
insert_date: 'mf.insert_date',
update_date: 'mf.update_date',
banco_descricao: 'b.descricao',
centro_custo_descricao: 'cc.descricao',
cliente_nome: 'c.nome',
};
return camposPermitidos[orderBy] || 'mf.dia_vencimento';
@ -110,6 +115,11 @@ function montarWhereMovimentosFixos(filtros = {}) {
const where = [];
const params = [];
if (filtros.idUsuario) {
where.push('b.idusuarios = ?');
params.push(Number(filtros.idUsuario));
}
if (filtros.movimento) {
where.push('mf.movimento = ?');
params.push(filtros.movimento);
@ -125,6 +135,11 @@ function montarWhereMovimentosFixos(filtros = {}) {
params.push(Number(filtros.idcentrodecustos));
}
if (filtros.idclientes) {
where.push('mf.idclientes = ?');
params.push(Number(filtros.idclientes));
}
if (filtros.habilitado !== undefined && filtros.habilitado !== null && filtros.habilitado !== '') {
where.push('mf.habilitado = ?');
params.push(Number(filtros.habilitado));
@ -147,6 +162,7 @@ function montarWhereMovimentosFixos(filtros = {}) {
OR mf.movimento LIKE ?
OR b.descricao LIKE ?
OR cc.descricao LIKE ?
OR c.nome LIKE ?
)
`);
@ -156,6 +172,7 @@ function montarWhereMovimentosFixos(filtros = {}) {
termo,
termo,
termo,
termo,
termo
);
}
@ -168,6 +185,24 @@ function montarWhereMovimentosFixos(filtros = {}) {
};
}
async function bancoPertenceAoUsuario(idbancos, idUsuario) {
if (!idbancos || !idUsuario) return false;
const [rows] = await pool.query(
`
SELECT idbancos
FROM bancos
WHERE idbancos = ?
AND idusuarios = ?
AND habilitado = 1
LIMIT 1
`,
[Number(idbancos), Number(idUsuario)]
);
return rows.length > 0;
}
async function listarMovimentosFixos(filtros = {}) {
const limite = limitarNumero(filtros.limite, 20, 1, 100);
const page = limitarNumero(filtros.page, 1, 1, 999999);
@ -243,32 +278,58 @@ async function listarMovimentosFixos(filtros = {}) {
};
}
async function buscarMovimentoFixoPorId(id) {
async function buscarMovimentoFixoPorId(id, idUsuario = null) {
const params = [id];
let filtroUsuario = '';
if (idUsuario) {
filtroUsuario = 'AND b.idusuarios = ?';
params.push(Number(idUsuario));
}
const [rows] = await pool.query(
`
SELECT
${CAMPOS_MOVIMENTO_FIXO_SELECT}
${FROM_MOVIMENTO_FIXO_JOIN}
WHERE mf.idmovimentosfixos = ?
${filtroUsuario}
LIMIT 1
`,
[id]
params
);
return rows[0] || null;
}
async function criarMovimentoFixo(dados) {
async function criarMovimentoFixo(dados, idUsuario) {
const {
movimento,
descricao,
valor,
dia_vencimento,
idcentrodecustos,
idclientes,
habilitado,
idbancos,
} = dados;
const idBancoNormalizado = normalizarNumeroOuNull(idbancos);
if (!idBancoNormalizado) {
const error = new Error('Banco/carteira é obrigatório para movimento fixo.');
error.statusCode = 400;
throw error;
}
const bancoValido = await bancoPertenceAoUsuario(idBancoNormalizado, idUsuario);
if (!bancoValido) {
const error = new Error('Banco/carteira não pertence ao usuário logado.');
error.statusCode = 403;
throw error;
}
const [result] = await pool.query(
`
INSERT INTO movimentosfixos (
@ -277,11 +338,12 @@ async function criarMovimentoFixo(dados) {
valor,
dia_vencimento,
idcentrodecustos,
idclientes,
habilitado,
idbancos,
insert_date,
update_date
) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
`,
[
normalizarTextoOuNull(movimento),
@ -289,16 +351,17 @@ async function criarMovimentoFixo(dados) {
normalizarNumero(valor, 0),
normalizarNumero(dia_vencimento, 1),
normalizarNumeroOuNull(idcentrodecustos),
normalizarNumeroOuNull(idclientes),
normalizarFlag(habilitado, 1),
normalizarNumeroOuNull(idbancos),
idBancoNormalizado,
]
);
return buscarMovimentoFixoPorId(result.insertId);
return buscarMovimentoFixoPorId(result.insertId, idUsuario);
}
async function atualizarMovimentoFixo(id, dados) {
const movimentoAtual = await buscarMovimentoFixoPorId(id);
async function atualizarMovimentoFixo(id, dados, idUsuario) {
const movimentoAtual = await buscarMovimentoFixoPorId(id, idUsuario);
if (!movimentoAtual) {
return null;
@ -310,10 +373,27 @@ async function atualizarMovimentoFixo(id, dados) {
valor,
dia_vencimento,
idcentrodecustos,
idclientes,
habilitado,
idbancos,
} = dados;
const idBancoNormalizado = normalizarNumeroOuNull(idbancos);
if (!idBancoNormalizado) {
const error = new Error('Banco/carteira é obrigatório para movimento fixo.');
error.statusCode = 400;
throw error;
}
const bancoValido = await bancoPertenceAoUsuario(idBancoNormalizado, idUsuario);
if (!bancoValido) {
const error = new Error('Banco/carteira não pertence ao usuário logado.');
error.statusCode = 403;
throw error;
}
await pool.query(
`
UPDATE movimentosfixos
@ -323,6 +403,7 @@ async function atualizarMovimentoFixo(id, dados) {
valor = ?,
dia_vencimento = ?,
idcentrodecustos = ?,
idclientes = ?,
habilitado = ?,
idbancos = ?,
update_date = NOW()
@ -334,17 +415,18 @@ async function atualizarMovimentoFixo(id, dados) {
normalizarNumero(valor, 0),
normalizarNumero(dia_vencimento, 1),
normalizarNumeroOuNull(idcentrodecustos),
normalizarNumeroOuNull(idclientes),
normalizarFlag(habilitado, 1),
normalizarNumeroOuNull(idbancos),
idBancoNormalizado,
id,
]
);
return buscarMovimentoFixoPorId(id);
return buscarMovimentoFixoPorId(id, idUsuario);
}
async function alterarHabilitadoMovimentoFixo(id, habilitado) {
const movimentoAtual = await buscarMovimentoFixoPorId(id);
async function alterarHabilitadoMovimentoFixo(id, habilitado, idUsuario) {
const movimentoAtual = await buscarMovimentoFixoPorId(id, idUsuario);
if (!movimentoAtual) {
return null;
@ -364,11 +446,11 @@ async function alterarHabilitadoMovimentoFixo(id, habilitado) {
]
);
return buscarMovimentoFixoPorId(id);
return buscarMovimentoFixoPorId(id, idUsuario);
}
async function deletarMovimentoFixo(id) {
const movimentoAtual = await buscarMovimentoFixoPorId(id);
async function deletarMovimentoFixo(id, idUsuario) {
const movimentoAtual = await buscarMovimentoFixoPorId(id, idUsuario);
if (!movimentoAtual) {
return false;

View File

@ -114,6 +114,7 @@ async function buscarFixosDoUsuario(connection, idUsuario) {
mf.valor,
mf.dia_vencimento,
mf.idcentrodecustos,
mf.idclientes,
mf.idbancos
FROM movimentosfixos mf
INNER JOIN bancos b ON b.idbancos = mf.idbancos
@ -132,7 +133,7 @@ async function jaExisteMovimentoFixo(connection, fixo, competencia) {
const [rowsGeracao] = await connection.query(
`
SELECT idmovimentosfixosgeracoes, idcontasapagar, status
FROM movimentosfixosgeracoes
FROM movimentosfixos_geracoes
WHERE idmovimentosfixos = ?
AND competencia = ?
LIMIT 1
@ -161,7 +162,7 @@ async function jaExisteMovimentoFixo(connection, fixo, competencia) {
if (rowsMovimentoVinculado.length > 0) {
await connection.query(
`
INSERT INTO movimentosfixosgeracoes (
INSERT INTO movimentosfixos_geracoes (
idmovimentosfixos,
competencia,
idcontasapagar,
@ -230,7 +231,7 @@ async function jaExisteMovimentoFixo(connection, fixo, competencia) {
await connection.query(
`
INSERT INTO movimentosfixosgeracoes (
INSERT INTO movimentosfixos_geracoes (
idmovimentosfixos,
competencia,
idcontasapagar,
@ -288,7 +289,7 @@ async function inserirMovimentoFixoGerado(connection, fixo, competencia, idUsuar
insert_date,
update_date,
deleted_at
) VALUES (?, ?, ?, ?, ?, ?, 1, 1, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?, NULL, 'movimento_fixo', 0, NULL, NOW(), NOW(), NULL)
) VALUES (?, ?, ?, ?, ?, ?, 1, 1, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, NULL, 'movimento_fixo', 0, NULL, NOW(), NOW(), NULL)
`,
[
fixo.movimento,
@ -302,6 +303,7 @@ async function inserirMovimentoFixoGerado(connection, fixo, competencia, idUsuar
fixo.idbancos || null,
idUsuario,
dataBaixa ? idUsuario : null,
fixo.idclientes || null,
fixo.idmovimentosfixos,
competencia,
]

View File

@ -18,6 +18,24 @@ async function listarBancos(req, res) {
}
}
async function listarMeusBancos(req, res) {
try {
const bancos = await referenciasService.listarMeusBancos(req.usuario || req.user);
return res.json({
ok: true,
data: bancos,
});
} catch (error) {
console.error('Erro ao listar bancos do usuário:', error);
return res.status(error.statusCode || 500).json({
ok: false,
message: error.message || 'Erro ao listar bancos do usuário.',
});
}
}
async function listarCentrosCusto(req, res) {
try {
const centrosCusto = await referenciasService.listarCentrosCusto();
@ -56,6 +74,7 @@ async function listarClientes(req, res) {
module.exports = {
listarBancos,
listarMeusBancos,
listarCentrosCusto,
listarClientes,
};

View File

@ -7,6 +7,8 @@ const router = express.Router();
router.use(authMiddleware);
router.get('/bancos', referenciasController.listarBancos);
router.get('/bancos/meus', referenciasController.listarMeusBancos);
router.get('/centros-custo', referenciasController.listarCentrosCusto);
router.get('/clientes', referenciasController.listarClientes);

View File

@ -1,5 +1,15 @@
const pool = require('../../../database/mysql');
function extrairIdUsuarioLogado(usuario) {
return (
usuario?.idusuarios ||
usuario?.idusuario ||
usuario?.id ||
usuario?.userId ||
null
);
}
async function listarBancos() {
const [rows] = await pool.query(
`
@ -23,14 +33,46 @@ async function listarBancos() {
ON u.idusuarios = b.idusuarios
WHERE b.habilitado = 1
ORDER BY
b.descricao ASC,
u.nome ASC
COALESCE(u.nome, 'Usuário sem nome') ASC,
b.descricao ASC
`
);
return rows;
}
async function listarMeusBancos(usuarioLogado) {
const idUsuario = extrairIdUsuarioLogado(usuarioLogado);
if (!idUsuario) {
const error = new Error('Usuário logado não identificado.');
error.statusCode = 401;
throw error;
}
const [rows] = await pool.query(
`
SELECT
b.idbancos AS id,
b.idbancos,
b.descricao AS banco_descricao,
b.descricao,
b.saldo,
b.debito,
b.investimento,
b.dia_vencimento
FROM bancos b
WHERE b.habilitado = 1
AND b.idusuarios = ?
ORDER BY
b.descricao ASC
`,
[idUsuario]
);
return rows;
}
async function listarCentrosCusto() {
const [rows] = await pool.query(
`
@ -72,6 +114,7 @@ async function listarClientes() {
module.exports = {
listarBancos,
listarMeusBancos,
listarCentrosCusto,
listarClientes,
};

View File

@ -0,0 +1,51 @@
const simulacaoMensalService = require('../services/simulacaoMensal.service');
function extrairIdUsuarioLogado(req) {
return (
req.usuario?.idusuarios ||
req.user?.idusuarios ||
req.usuario?.id ||
req.user?.id ||
null
);
}
function obterMesAnoPadrao() {
const hoje = new Date();
return {
ano: hoje.getFullYear(),
mes: hoje.getMonth() + 1,
};
}
async function buscarBase(req, res) {
try {
const idUsuario = extrairIdUsuarioLogado(req);
const padrao = obterMesAnoPadrao();
const resultado = await simulacaoMensalService.montarBaseSimulacaoMensal({
idUsuario,
ano: req.query.ano || padrao.ano,
mes: req.query.mes || padrao.mes,
incluirQuitadas: req.query.incluirQuitadas,
incluirFixosVencidosDoMesAtual: req.query.incluirFixosVencidosDoMesAtual,
});
return res.json({
ok: true,
data: resultado,
});
} catch (error) {
console.error('Erro ao montar simulação mensal:', error);
return res.status(error.statusCode || 500).json({
ok: false,
message: error.message || 'Erro ao montar simulação mensal.',
});
}
}
module.exports = {
buscarBase,
};

View File

@ -0,0 +1,11 @@
const express = require('express');
const authMiddleware = require('../../../middlewares/auth.middleware');
const simulacaoMensalController = require('../controllers/simulacaoMensal.controller');
const router = express.Router();
router.use(authMiddleware);
router.get('/base', simulacaoMensalController.buscarBase);
module.exports = router;

View File

@ -0,0 +1,790 @@
const pool = require('../../../database/mysql');
function hojeDataString() {
return new Date().toISOString().slice(0, 10);
}
function normalizarNumero(valor, padrao = 0) {
const numero = Number(valor);
if (!Number.isFinite(numero)) {
return padrao;
}
return numero;
}
function normalizarFlag(valor, padrao = 0) {
if (valor === undefined || valor === null || valor === '') {
return padrao;
}
return Number(valor) === 1 ? 1 : 0;
}
function pad2(valor) {
return String(valor).padStart(2, '0');
}
function ultimoDiaDoMes(ano, mes) {
return new Date(Number(ano), Number(mes), 0).getDate();
}
function montarCompetencia(ano, mes) {
return `${Number(ano)}-${pad2(Number(mes))}`;
}
function montarPeriodo(ano, mes) {
const competencia = montarCompetencia(ano, mes);
const ultimoDia = ultimoDiaDoMes(ano, mes);
return {
competencia,
dataInicio: `${competencia}-01`,
dataFim: `${competencia}-${pad2(ultimoDia)}`,
ultimoDia,
};
}
function montarDataVencimento(ano, mes, diaVencimento) {
const { competencia, ultimoDia } = montarPeriodo(ano, mes);
const dia = Math.min(Math.max(Number(diaVencimento || 1), 1), ultimoDia);
return `${competencia}-${pad2(dia)}`;
}
function movimentoEhEntrada(movimento) {
return movimento === 'Entrada' || movimento === 'Estorno';
}
function movimentoEhSaida(movimento) {
return movimento === 'Saida' || movimento === 'Sangria';
}
function statusEmAberto(status) {
return status === 'A pagar' || status === 'A receber';
}
function statusBaixado(status) {
return status === 'Pago' || status === 'Recebido';
}
function criarItemSimulacao({
tipo,
origem,
descricao,
valor,
movimento = null,
status = null,
data = null,
idcontasapagar = null,
idmovimentosfixos = null,
idbancos = null,
banco_descricao = null,
banco_debito = null,
idcentrodecustos = null,
centro_custo_descricao = null,
idclientes = null,
cliente_nome = null,
investimento = 0,
editavel = true,
detalhes = null,
}) {
const valorNumerico = Number(valor || 0);
return {
id: [
origem,
idcontasapagar || null,
idmovimentosfixos || null,
idbancos || null,
idcentrodecustos || null,
descricao,
].filter(Boolean).join(':'),
tipo,
origem,
descricao,
valor: valorNumerico,
valor_original: valorNumerico,
movimento,
status,
data,
idcontasapagar,
idmovimentosfixos,
idbancos,
banco_descricao,
banco_debito,
idcentrodecustos,
centro_custo_descricao,
idclientes,
cliente_nome,
investimento: Number(investimento || 0),
editavel,
editado: false,
detalhes,
};
}
function calcularPercentual(valor, totalEntradas) {
if (!totalEntradas) return null;
return Number(((Number(valor || 0) / totalEntradas) * 100).toFixed(2));
}
function calcularResumo(entradas, saidas) {
const totalEntradas = entradas.reduce(
(total, item) => total + Number(item.valor || 0),
0
);
const totalSaidas = saidas.reduce(
(total, item) => total + Number(item.valor || 0),
0
);
const totalInvestimentos = saidas
.filter((item) => Number(item.investimento) === 1)
.reduce((total, item) => total + Number(item.valor || 0), 0);
const totalGastos = totalSaidas - totalInvestimentos;
const resultado = totalEntradas - totalSaidas;
return {
totalEntradas,
totalSaidas,
totalGastos,
totalInvestimentos,
resultado,
percentualEntradas: totalEntradas > 0 ? 100 : null,
percentualSaidas: calcularPercentual(totalSaidas, totalEntradas),
percentualGastos: calcularPercentual(totalGastos, totalEntradas),
percentualInvestimentos: calcularPercentual(totalInvestimentos, totalEntradas),
percentualResultado: calcularPercentual(resultado, totalEntradas),
percentualComprometido: calcularPercentual(totalSaidas, totalEntradas),
quantidadeEntradas: entradas.length,
quantidadeSaidas: saidas.length,
quantidadeInvestimentos: saidas.filter((item) => Number(item.investimento) === 1).length,
};
}
async function buscarBancosUsuario(connection, idUsuario) {
const [rows] = await connection.query(
`
SELECT
b.idbancos,
b.descricao,
b.saldo,
b.debito,
b.investimento,
b.idusuarios
FROM bancos b
WHERE b.habilitado = 1
AND b.idusuarios = ?
ORDER BY b.descricao ASC
`,
[idUsuario]
);
return rows;
}
async function buscarMovimentosDoMes(connection, { idUsuario, dataInicio, dataFim }) {
const [rows] = await connection.query(
`
SELECT
cp.idcontasapagar,
cp.movimento,
cp.descricao,
cp.dataentrada,
cp.datavencimento,
cp.databaixa,
cp.valor,
cp.parcela,
cp.parcelas,
cp.status,
cp.idcentrodecustos,
cp.idbancos,
cp.idclientes,
cp.idbancos_p,
cp.idmovimentosfixos,
cp.competencia,
cp.origem,
cp.saldo_processado,
cp.observacao,
b.descricao AS banco_descricao,
b.debito AS banco_debito,
cc.descricao AS centro_custo_descricao,
cc.investimento AS centro_custo_investimento,
c.nome AS cliente_nome,
bp.descricao AS banco_referencia_descricao
FROM contasapagar cp
INNER JOIN bancos b
ON b.idbancos = cp.idbancos
LEFT JOIN centrodecustos cc
ON cc.idcentrodecustos = cp.idcentrodecustos
LEFT JOIN clientes c
ON c.idclientes = cp.idclientes
LEFT JOIN bancos bp
ON bp.idbancos = cp.idbancos_p
WHERE cp.deleted_at IS NULL
AND b.idusuarios = ?
AND DATE(cp.datavencimento) BETWEEN ? AND ?
ORDER BY cp.datavencimento ASC, cp.descricao ASC
`,
[idUsuario, dataInicio, dataFim]
);
return rows;
}
async function buscarMovimentosParaCentrosDoMes(connection, { idUsuario, dataInicio, dataFim }) {
const [rows] = await connection.query(
`
SELECT
cp.idcentrodecustos,
COALESCE(SUM(cp.valor), 0) AS valor_gasto
FROM contasapagar cp
INNER JOIN bancos b
ON b.idbancos = cp.idbancos
WHERE cp.deleted_at IS NULL
AND b.idusuarios = ?
AND cp.idcentrodecustos IS NOT NULL
AND (
(
b.debito = 1
AND DATE(cp.datavencimento) BETWEEN ? AND ?
)
OR
(
b.debito = 0
AND DATE(cp.dataentrada) BETWEEN ? AND ?
)
)
GROUP BY cp.idcentrodecustos
`,
[idUsuario, dataInicio, dataFim, dataInicio, dataFim]
);
return new Map(
rows.map((row) => [
Number(row.idcentrodecustos),
Number(row.valor_gasto || 0),
])
);
}
async function buscarFixosUsuario(connection, idUsuario) {
const [rows] = await connection.query(
`
SELECT
mf.idmovimentosfixos,
mf.movimento,
mf.descricao,
mf.valor,
mf.dia_vencimento,
mf.idcentrodecustos,
mf.idclientes,
mf.idbancos,
b.descricao AS banco_descricao,
b.debito AS banco_debito,
cc.descricao AS centro_custo_descricao,
cc.investimento AS centro_custo_investimento,
c.nome AS cliente_nome
FROM movimentosfixos mf
INNER JOIN bancos b
ON b.idbancos = mf.idbancos
LEFT JOIN centrodecustos cc
ON cc.idcentrodecustos = mf.idcentrodecustos
LEFT JOIN clientes c
ON c.idclientes = mf.idclientes
WHERE mf.habilitado = 1
AND b.idusuarios = ?
AND COALESCE(mf.gerar_automatico, 1) = 1
ORDER BY mf.dia_vencimento ASC, mf.descricao ASC
`,
[idUsuario]
);
return rows;
}
async function buscarCentrosSimulaveis(connection) {
const [rows] = await connection.query(
`
SELECT
idcentrodecustos,
descricao,
limite,
simular,
investimento
FROM centrodecustos
WHERE habilitado = 1
AND simular = 1
ORDER BY descricao ASC
`
);
return rows;
}
function movimentoFixoJaExisteNoMes(fixo, movimentosDoMes) {
return movimentosDoMes.some((movimento) => {
if (
fixo.idmovimentosfixos &&
movimento.idmovimentosfixos &&
Number(fixo.idmovimentosfixos) === Number(movimento.idmovimentosfixos)
) {
return true;
}
const mesmaDescricao =
String(movimento.descricao || '').trim().toLowerCase() ===
String(fixo.descricao || '').trim().toLowerCase();
const mesmoBanco = Number(movimento.idbancos || 0) === Number(fixo.idbancos || 0);
const mesmoCentro =
Number(movimento.idcentrodecustos || 0) === Number(fixo.idcentrodecustos || 0);
return mesmaDescricao && mesmoBanco && mesmoCentro;
});
}
function montarItensMovimentosExistentes(movimentosDoMes, incluirQuitadas) {
const entradas = [];
const saidas = [];
for (const movimento of movimentosDoMes) {
const bancoDebito = Number(movimento.banco_debito) === 1;
const bancoCredito = Number(movimento.banco_debito) === 0;
if (bancoDebito) {
if (!incluirQuitadas && !statusEmAberto(movimento.status)) {
continue;
}
if (movimento.movimento === 'Entrada') {
entradas.push(
criarItemSimulacao({
tipo: 'entrada',
origem: 'movimento_existente',
descricao: movimento.descricao,
valor: movimento.valor,
movimento: movimento.movimento,
status: movimento.status,
data: movimento.datavencimento,
idcontasapagar: movimento.idcontasapagar,
idbancos: movimento.idbancos,
banco_descricao: movimento.banco_descricao,
banco_debito: movimento.banco_debito,
idcentrodecustos: movimento.idcentrodecustos,
centro_custo_descricao: movimento.centro_custo_descricao,
investimento: movimento.centro_custo_investimento,
idclientes: movimento.idclientes,
cliente_nome: movimento.cliente_nome,
detalhes: {
parcela: movimento.parcela,
parcelas: movimento.parcelas,
origem_movimento: movimento.origem,
},
})
);
}
if (movimento.movimento === 'Saida' || movimento.movimento === 'Sangria') {
saidas.push(
criarItemSimulacao({
tipo: 'saida',
origem: 'movimento_existente',
descricao: movimento.descricao,
valor: movimento.valor,
movimento: movimento.movimento,
status: movimento.status,
data: movimento.datavencimento,
idcontasapagar: movimento.idcontasapagar,
idbancos: movimento.idbancos,
banco_descricao: movimento.banco_descricao,
banco_debito: movimento.banco_debito,
idcentrodecustos: movimento.idcentrodecustos,
centro_custo_descricao: movimento.centro_custo_descricao,
investimento: movimento.centro_custo_investimento,
idclientes: movimento.idclientes,
cliente_nome: movimento.cliente_nome,
detalhes: {
parcela: movimento.parcela,
parcelas: movimento.parcelas,
origem_movimento: movimento.origem,
},
})
);
}
if (movimento.movimento === 'Estorno') {
entradas.push(
criarItemSimulacao({
tipo: 'entrada',
origem: 'movimento_existente',
descricao: movimento.descricao,
valor: movimento.valor,
movimento: movimento.movimento,
status: movimento.status,
data: movimento.datavencimento,
idcontasapagar: movimento.idcontasapagar,
idbancos: movimento.idbancos,
banco_descricao: movimento.banco_descricao,
banco_debito: movimento.banco_debito,
idcentrodecustos: movimento.idcentrodecustos,
centro_custo_descricao: movimento.centro_custo_descricao,
investimento: movimento.centro_custo_investimento,
idclientes: movimento.idclientes,
cliente_nome: movimento.cliente_nome,
})
);
}
}
if (bancoCredito) {
// Cartões entram depois agrupados por banco.
continue;
}
}
return {
entradas,
saidas,
};
}
function montarItensCreditoAgrupado(movimentosDoMes) {
const grupos = new Map();
for (const movimento of movimentosDoMes) {
if (Number(movimento.banco_debito) !== 0) {
continue;
}
const chave = Number(movimento.idbancos);
if (!grupos.has(chave)) {
grupos.set(chave, {
idbancos: movimento.idbancos,
banco_descricao: movimento.banco_descricao,
valor: 0,
movimentos: [],
});
}
const grupo = grupos.get(chave);
if (
(movimento.movimento === 'Saida' || movimento.movimento === 'Sangria') &&
movimento.status === 'A pagar'
) {
grupo.valor += Number(movimento.valor || 0);
grupo.movimentos.push(movimento);
}
if (movimento.movimento === 'Entrada' || movimento.movimento === 'Estorno') {
grupo.valor -= Number(movimento.valor || 0);
grupo.movimentos.push(movimento);
}
}
return Array
.from(grupos.values())
.filter((grupo) => Number(grupo.valor || 0) !== 0)
.map((grupo) =>
criarItemSimulacao({
tipo: 'saida',
origem: 'cartao_credito_agrupado',
descricao: `Fatura ${grupo.banco_descricao}`,
valor: grupo.valor,
movimento: 'Saida',
status: 'A pagar',
idbancos: grupo.idbancos,
banco_descricao: grupo.banco_descricao,
banco_debito: 0,
editavel: true,
detalhes: {
quantidade_movimentos: grupo.movimentos.length,
ids: grupo.movimentos.map((item) => item.idcontasapagar),
},
})
);
}
function montarItensFixosPrevistos({
fixos,
movimentosDoMes,
ano,
mes,
competencia,
incluirFixosVencidosDoMesAtual,
}) {
const entradas = [];
const saidas = [];
const hoje = hojeDataString();
const mesAtual = hoje.slice(0, 7);
const diaAtual = Number(hoje.slice(8, 10));
for (const fixo of fixos) {
if (Number(fixo.banco_debito) !== 1) {
continue;
}
if (movimentoFixoJaExisteNoMes(fixo, movimentosDoMes)) {
continue;
}
if (
competencia === mesAtual &&
!incluirFixosVencidosDoMesAtual &&
Number(fixo.dia_vencimento || 1) < diaAtual
) {
continue;
}
const dataVencimento = montarDataVencimento(ano, mes, fixo.dia_vencimento);
if (movimentoEhEntrada(fixo.movimento)) {
entradas.push(
criarItemSimulacao({
tipo: 'entrada',
origem: 'movimento_fixo_previsto',
descricao: fixo.descricao,
valor: fixo.valor,
movimento: fixo.movimento,
status: 'A receber',
data: dataVencimento,
idmovimentosfixos: fixo.idmovimentosfixos,
idbancos: fixo.idbancos,
banco_descricao: fixo.banco_descricao,
banco_debito: fixo.banco_debito,
idcentrodecustos: fixo.idcentrodecustos,
centro_custo_descricao: fixo.centro_custo_descricao,
investimento: fixo.centro_custo_investimento,
idclientes: fixo.idclientes,
cliente_nome: fixo.cliente_nome,
})
);
}
if (movimentoEhSaida(fixo.movimento)) {
saidas.push(
criarItemSimulacao({
tipo: 'saida',
origem: 'movimento_fixo_previsto',
descricao: fixo.descricao,
valor: fixo.valor,
movimento: fixo.movimento,
status: fixo.movimento === 'Sangria' ? 'Pago' : 'A pagar',
data: dataVencimento,
idmovimentosfixos: fixo.idmovimentosfixos,
idbancos: fixo.idbancos,
banco_descricao: fixo.banco_descricao,
banco_debito: fixo.banco_debito,
idcentrodecustos: fixo.idcentrodecustos,
centro_custo_descricao: fixo.centro_custo_descricao,
investimento: fixo.centro_custo_investimento,
idclientes: fixo.idclientes,
cliente_nome: fixo.cliente_nome,
})
);
}
}
return {
entradas,
saidas,
};
}
function montarItensSaldosBancos(bancos) {
return bancos
.filter((banco) => Number(banco.debito) === 1)
.map((banco) =>
criarItemSimulacao({
tipo: 'entrada',
origem: 'saldo_banco',
descricao: `Saldo ${banco.descricao}`,
valor: banco.saldo,
movimento: 'Entrada',
status: 'Disponível',
idbancos: banco.idbancos,
banco_descricao: banco.descricao,
banco_debito: banco.debito,
editavel: true,
})
);
}
function montarItensLimitesCentros({ centrosSimulaveis, gastosPorCentro, avisos }) {
const saidas = [];
for (const centro of centrosSimulaveis) {
const limite = Number(centro.limite || 0);
const gastoAtual = Number(gastosPorCentro.get(Number(centro.idcentrodecustos)) || 0);
const restante = limite - gastoAtual;
if (restante > 0) {
saidas.push(
criarItemSimulacao({
tipo: 'saida',
origem: 'limite_centro_custo',
descricao: `Previsão ${centro.descricao}`,
valor: restante,
movimento: 'Saida',
status: 'Previsto',
idcentrodecustos: centro.idcentrodecustos,
centro_custo_descricao: centro.descricao,
investimento: centro.investimento,
editavel: true,
detalhes: {
limite,
gastoAtual,
restante,
},
})
);
} else if (limite > 0) {
avisos.push({
tipo: 'limite_centro_estourado',
idcentrodecustos: centro.idcentrodecustos,
centro_custo_descricao: centro.descricao,
limite,
gastoAtual,
excesso: Math.abs(restante),
mensagem: `Centro de custo "${centro.descricao}" já passou do limite em ${Math.abs(restante).toFixed(2)}.`,
});
}
}
return saidas;
}
async function montarBaseSimulacaoMensal(opcoes = {}) {
const idUsuario = Number(opcoes.idUsuario || opcoes.idusuarios || 0);
const ano = Number(opcoes.ano);
const mes = Number(opcoes.mes);
if (!idUsuario) {
const error = new Error('Usuário logado não identificado.');
error.statusCode = 401;
throw error;
}
if (!Number.isInteger(ano) || ano < 2000 || ano > 2100) {
const error = new Error('Ano inválido.');
error.statusCode = 400;
throw error;
}
if (!Number.isInteger(mes) || mes < 1 || mes > 12) {
const error = new Error('Mês inválido.');
error.statusCode = 400;
throw error;
}
const incluirQuitadas = normalizarFlag(opcoes.incluirQuitadas, 0) === 1;
const incluirFixosVencidosDoMesAtual =
normalizarFlag(opcoes.incluirFixosVencidosDoMesAtual, 0) === 1;
const {
competencia,
dataInicio,
dataFim,
} = montarPeriodo(ano, mes);
const connection = await pool.getConnection();
try {
const avisos = [];
const [
bancos,
movimentosDoMes,
fixos,
centrosSimulaveis,
gastosPorCentro,
] = await Promise.all([
buscarBancosUsuario(connection, idUsuario),
buscarMovimentosDoMes(connection, {
idUsuario,
dataInicio,
dataFim,
}),
buscarFixosUsuario(connection, idUsuario),
buscarCentrosSimulaveis(connection),
buscarMovimentosParaCentrosDoMes(connection, {
idUsuario,
dataInicio,
dataFim,
}),
]);
const itensExistentes = montarItensMovimentosExistentes(movimentosDoMes, incluirQuitadas);
const itensCredito = montarItensCreditoAgrupado(movimentosDoMes);
const itensFixos = montarItensFixosPrevistos({
fixos,
movimentosDoMes,
ano,
mes,
competencia,
incluirFixosVencidosDoMesAtual,
});
const itensSaldos = montarItensSaldosBancos(bancos);
const itensLimites = montarItensLimitesCentros({
centrosSimulaveis,
gastosPorCentro,
avisos,
});
const entradas = [
...itensSaldos,
...itensExistentes.entradas,
...itensFixos.entradas,
];
const saidas = [
...itensExistentes.saidas,
...itensCredito,
...itensFixos.saidas,
...itensLimites,
];
entradas.sort((a, b) => String(a.data || '').localeCompare(String(b.data || '')));
saidas.sort((a, b) => String(a.data || '').localeCompare(String(b.data || '')));
const resumo = calcularResumo(entradas, saidas);
return {
competencia,
ano,
mes,
dataInicio,
dataFim,
opcoes: {
incluirQuitadas,
incluirFixosVencidosDoMesAtual,
},
entradas,
saidas,
resumo,
fontes: {
movimentosExistentes: movimentosDoMes.length,
movimentosFixos: fixos.length,
bancos: bancos.length,
centrosSimulaveis: centrosSimulaveis.length,
},
avisos,
};
} finally {
connection.release();
}
}
module.exports = {
montarBaseSimulacaoMensal,
};

View File

@ -26,6 +26,7 @@ import { UsuariosPage } from '../features/usuarios/pages/UsuariosPage';
import { NovoUsuarioPage } from '../features/usuarios/pages/NovoUsuarioPage';
import { EditarUsuarioPage } from '../features/usuarios/pages/EditarUsuarioPage';
import { QuitacoesCreditoPage } from '../features/quitacoesCredito/pages/QuitacoesCreditoPage';
import { SimulacaoMensalPage } from '../features/simulacaoMensal/pages/SimulacaoMensalPage';
export const routes: RouteObject[] = [
{
@ -129,6 +130,10 @@ export const routes: RouteObject[] = [
{
path: '/quitacoes-credito',
element: <QuitacoesCreditoPage />,
},
{
path: 'simulacao-mensal',
element: <SimulacaoMensalPage />,
}
],
},

View File

@ -18,6 +18,7 @@ import AccountTreeIcon from '@mui/icons-material/AccountTree';
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
import CreditCardIcon from '@mui/icons-material/CreditCard';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import { NavLink } from 'react-router-dom';
type SidebarProps = {
@ -75,6 +76,11 @@ const menuItems = [
path: '/relatorios',
icon: <AssessmentIcon />,
},
{
label: 'Simulação mensal',
path: '/simulacao-mensal',
icon: <AutoAwesomeIcon />,
},
{
label: 'Configurações',
path: '/configuracoes',

View File

@ -26,6 +26,7 @@ import {
listarBancos,
listarCentrosCusto,
listarClientes,
listarMeusBancos,
} from '../../referencias/services/referenciasService';
import type {
Banco,
@ -118,6 +119,68 @@ function formatarValorMoeda(valor: number) {
}).format(Number(valor || 0));
}
function formatarPercentual(valor: number | null | undefined) {
if (valor === null || valor === undefined || !Number.isFinite(Number(valor))) {
return null;
}
const numero = Number(valor);
return `${numero >= 0 ? '+' : ''}${numero.toFixed(2)}%`;
}
function getImpactoVisual(impacto: MovimentoImpactoSaldo) {
const delta = Number(impacto.valor_delta || 0);
const isAlta = impacto.direcao
? impacto.direcao === 'alta'
: delta > 0;
const isQueda = impacto.direcao
? impacto.direcao === 'queda'
: delta < 0;
const chipLabel = isAlta
? '↗ Alta'
: isQueda
? '↘ Queda'
: '→ Neutro';
const color = isAlta
? 'success'
: isQueda
? 'error'
: 'default';
const textColor = isAlta
? 'success.main'
: isQueda
? 'error.main'
: 'text.primary';
const backgroundColor = isAlta
? 'rgba(34,197,94,0.08)'
: isQueda
? 'rgba(239,68,68,0.08)'
: 'rgba(15,23,42,0.04)';
const borderColor = isAlta
? 'rgba(34,197,94,0.25)'
: isQueda
? 'rgba(239,68,68,0.25)'
: 'divider';
return {
isAlta,
isQueda,
chipLabel,
color,
textColor,
backgroundColor,
borderColor,
} as const;
}
function FormSection({ title, description, children }: FormSectionProps) {
return (
<Paper
@ -174,7 +237,8 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
const isEdit = mode === 'edit';
const [bancos, setBancos] = useState<Banco[]>([]);
const [meusBancos, setMeusBancos] = useState<Banco[]>([]);
const [todosBancos, setTodosBancos] = useState<Banco[]>([]);
const [centrosCusto, setCentrosCusto] = useState<CentroCusto[]>([]);
const [clientes, setClientes] = useState<Cliente[]>([]);
@ -215,8 +279,8 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
const bancoSelecionado = useMemo(() => {
if (!idBanco) return null;
return bancos.find((banco) => String(banco.id) === String(idBanco)) || null;
}, [bancos, idBanco]);
return meusBancos.find((banco) => String(banco.id) === String(idBanco)) || null;
}, [meusBancos, idBanco]);
const statusDisponiveis = useMemo<MovimentoStatus[]>(() => {
if (movimento === 'Entrada') return ['Recebido', 'A receber'];
@ -261,6 +325,11 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
if (movimento === 'Sangria') {
setReferenciaTipo('banco');
setStatus('Pago');
setDataBaixa((dataAtual) => dataAtual || hojeISO());
setParcelas('1');
setParcela('1');
setGerarParcelas(false);
setIdCentroCusto('19');
return;
}
@ -276,13 +345,15 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
setLoadingRefs(true);
setErro('');
const [bancosData, centrosData, clientesData] = await Promise.all([
const [meusBancosData, todosBancosData, centrosData, clientesData] = await Promise.all([
listarMeusBancos(),
listarBancos(),
listarCentrosCusto(),
listarClientes(),
]);
setBancos(bancosData);
setMeusBancos(meusBancosData);
setTodosBancos(todosBancosData);
setCentrosCusto(centrosData);
setClientes(clientesData);
} catch (error: any) {
@ -494,22 +565,33 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
}
function montarPayload(confirmarDuplicado = false): CriarMovimentoRequest {
const isSangria = movimento === 'Sangria';
const statusFinal = isSangria ? 'Pago' : status;
const referenciaTipoFinal = isSangria ? 'banco' : referenciaTipo;
const parcelaFinal = isSangria ? 1 : Number(parcela || 1);
const parcelasFinal = isSangria ? 1 : Number(parcelas || 1);
const dataBaixaFinal =
statusFinal === 'Pago' || statusFinal === 'Recebido'
? dataBaixa || hojeISO()
: null;
return {
movimento,
descricao: descricao.trim(),
valor: Number(valor),
parcela: Number(parcela || 1),
parcelas: Number(parcelas || 1),
status,
parcela: parcelaFinal,
parcelas: parcelasFinal,
status: statusFinal,
dataentrada: dataEntrada,
datavencimento: dataVencimento || null,
databaixa: status === 'Pago' || status === 'Recebido' ? dataBaixa || null : null,
databaixa: dataBaixaFinal,
idcentrodecustos: toNumberOrNull(idCentroCusto),
idbancos: toNumberOrNull(idBanco),
idusuarios_baixa: null,
idclientes: referenciaTipo === 'cliente' ? toNumberOrNull(idCliente) : null,
idclientes: referenciaTipoFinal === 'cliente' ? toNumberOrNull(idCliente) : null,
idveiculosdetalhes: null,
idbancos_p: referenciaTipo === 'banco' ? toNumberOrNull(idBancoReferencia) : null,
idbancos_p: referenciaTipoFinal === 'banco' ? toNumberOrNull(idBancoReferencia) : null,
idmovimentosfixos: null,
competencia: extrairCompetencia(dataVencimento || dataEntrada),
@ -519,8 +601,12 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
observacao: observacao.trim() || null,
gerarParcelas: temParcelamento ? gerarParcelas : false,
modoParcelamento: temParcelamento ? modoParcelamento : 'valor_parcela',
gerarParcelas: isSangria ? false : temParcelamento ? gerarParcelas : false,
modoParcelamento: isSangria
? 'valor_parcela'
: temParcelamento
? modoParcelamento
: 'valor_parcela',
confirmarDuplicado,
};
}
@ -645,6 +731,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
label="Situação"
value={status}
onChange={(event) => setStatus(event.target.value as MovimentoStatus)}
disabled={movimento === 'Sangria'}
fullWidth
sx={fieldSx}
>
@ -685,7 +772,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
sx={fieldSx}
>
<MenuItem value="">Nenhum</MenuItem>
{bancos.map((banco) => (
{meusBancos.map((banco) => (
<MenuItem key={banco.id} value={String(banco.id)}>
{banco.descricao}
{Number(banco.debito) === 0 ? ' · Crédito' : ''}
@ -698,25 +785,35 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
title="Parcelas e datas"
description="Controle vencimento, baixa e parcelamento."
>
<TextField
label="Parcela"
type="number"
value={parcela}
onChange={(event) => setParcela(event.target.value)}
inputProps={{ min: '1' }}
fullWidth
sx={fieldSx}
/>
{movimento !== 'Sangria' && (
<>
<TextField
label="Parcela"
type="number"
value={parcela}
onChange={(event) => setParcela(event.target.value)}
inputProps={{ min: '1' }}
fullWidth
sx={fieldSx}
/>
<TextField
label="Total de parcelas"
type="number"
value={parcelas}
onChange={(event) => setParcelas(event.target.value)}
inputProps={{ min: '1' }}
fullWidth
sx={fieldSx}
/>
<TextField
label="Total de parcelas"
type="number"
value={parcelas}
onChange={(event) => setParcelas(event.target.value)}
inputProps={{ min: '1' }}
fullWidth
sx={fieldSx}
/>
{temParcelamento && (
<Box sx={{ gridColumn: { xs: 'auto', md: '1 / -1' } }}>
...
</Box>
)}
</>
)}
{temParcelamento && (
<Box sx={{ gridColumn: { xs: 'auto', md: '1 / -1' } }}>
@ -815,6 +912,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
onChange={(event) =>
setReferenciaTipo(event.target.value as 'nenhum' | 'cliente' | 'banco')
}
disabled={movimento === 'Sangria'}
fullWidth
sx={fieldSx}
>
@ -853,7 +951,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
sx={fieldSx}
>
<MenuItem value="">Selecione</MenuItem>
{bancos.map((banco) => (
{todosBancos.map((banco) => (
<MenuItem key={banco.id} value={String(banco.id)}>
{banco.descricao}
</MenuItem>
@ -980,7 +1078,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
<Box>
<Typography variant="caption" color="text.secondary">
Impacto previsto no saldo
Impacto previsto no saldo dos bancos
</Typography>
{loadingImpacto ? (
@ -990,40 +1088,88 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
</Box>
) : impactoSaldo.length === 0 ? (
<Typography fontWeight={700} marginTop={0.5}>
Sem impacto imediato
Sem alteração imediata de saldo
</Typography>
) : (
<Stack spacing={1} marginTop={1}>
{impactoSaldo.map((impacto, index) => (
<Box
key={`${impacto.idbancos}-${index}`}
sx={{
padding: 1.25,
borderRadius: 2,
backgroundColor: 'rgba(15,23,42,0.04)',
}}
>
<Typography variant="body2" fontWeight={900}>
{impacto.banco_descricao || `Banco ${impacto.idbancos}`}
</Typography>
{impactoSaldo.map((impacto, index) => {
const visual = getImpactoVisual(impacto);
const percentualFormatado = formatarPercentual(impacto.percentual_delta);
<Typography
variant="body2"
fontWeight={900}
color={impacto.valor_delta >= 0 ? 'success.main' : 'error.main'}
return (
<Box
key={`${impacto.idbancos}-${index}`}
sx={{
padding: 1.5,
borderRadius: 2.5,
backgroundColor: visual.backgroundColor,
border: '1px solid',
borderColor: visual.borderColor,
}}
>
{impacto.valor_delta >= 0 ? '+' : ''}
{formatarValorMoeda(impacto.valor_delta)}
</Typography>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-start"
gap={1}
>
<Box>
<Typography variant="body2" fontWeight={900}>
{impacto.banco_descricao || `Banco ${impacto.idbancos}`}
</Typography>
{impacto.saldo_anterior !== undefined && (
<Typography variant="caption" color="text.secondary">
{formatarValorMoeda(impacto.saldo_anterior)} {' '}
{formatarValorMoeda(impacto.saldo_posterior || 0)}
{impacto.descricao && (
<Typography variant="caption" color="text.secondary">
{impacto.descricao}
</Typography>
)}
</Box>
<Chip
size="small"
label={visual.chipLabel}
color={visual.color}
variant="outlined"
sx={{ fontWeight: 800 }}
/>
</Stack>
<Typography
variant="h6"
fontWeight={950}
color={visual.textColor}
sx={{ marginTop: 1 }}
>
{impacto.valor_delta >= 0 ? '+' : ''}
{formatarValorMoeda(impacto.valor_delta)}
</Typography>
)}
</Box>
))}
<Stack spacing={0.25} marginTop={0.75}>
{impacto.saldo_anterior !== undefined && (
<Typography variant="caption" color="text.secondary">
Saldo atual: {formatarValorMoeda(impacto.saldo_anterior)}
</Typography>
)}
{impacto.saldo_posterior !== undefined && (
<Typography variant="caption" color="text.secondary">
Saldo previsto: {formatarValorMoeda(impacto.saldo_posterior)}
</Typography>
)}
{percentualFormatado && (
<Typography
variant="caption"
fontWeight={800}
color={visual.textColor}
>
{percentualFormatado} em relação ao saldo atual
</Typography>
)}
</Stack>
</Box>
);
})}
</Stack>
)}
</Box>

View File

@ -77,10 +77,19 @@ export type AtualizarMovimentoRequest = CriarMovimentoRequest;
export type MovimentoImpactoSaldo = {
idbancos: number;
banco_descricao?: string | null;
banco_nome?: string | null;
usuario_nome?: string | null;
valor_delta: number;
saldo_anterior?: number;
saldo_posterior?: number;
percentual_delta?: number | null;
direcao?: 'alta' | 'queda' | 'neutro';
tipo_visual?: 'positivo' | 'negativo' | 'neutro';
descricao?: string | null;
};

View File

@ -20,12 +20,14 @@ import EventRepeatIcon from '@mui/icons-material/EventRepeat';
import SaveIcon from '@mui/icons-material/Save';
import { useNavigate } from 'react-router-dom';
import {
listarBancos,
listarMeusBancos,
listarCentrosCusto,
listarClientes,
} from '../../referencias/services/referenciasService';
import type {
Banco,
CentroCusto,
Cliente,
} from '../../referencias/types/referenciasTypes';
import {
atualizarMovimentoFixo,
@ -125,6 +127,7 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
const [bancos, setBancos] = useState<Banco[]>([]);
const [centrosCusto, setCentrosCusto] = useState<CentroCusto[]>([]);
const [clientes, setClientes] = useState<Cliente[]>([]);
const [loadingRefs, setLoadingRefs] = useState(true);
const [saving, setSaving] = useState(false);
@ -136,6 +139,7 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
const [valor, setValor] = useState('');
const [diaVencimento, setDiaVencimento] = useState('1');
const [idCentroCusto, setIdCentroCusto] = useState('');
const [idCliente, setIdCliente] = useState('');
const [idBanco, setIdBanco] = useState('');
const [habilitado, setHabilitado] = useState('1');
@ -151,6 +155,12 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
return 'Estorno recorrente';
}, [movimento]);
const clienteSelecionado = useMemo(() => {
if (!idCliente) return null;
return clientes.find((cliente) => String(cliente.id) === String(idCliente)) || null;
}, [clientes, idCliente]);
useEffect(() => {
if (!initialData) return;
@ -159,6 +169,7 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
setValor(String(initialData.valor || ''));
setDiaVencimento(String(initialData.dia_vencimento || 1));
setIdCentroCusto(initialData.idcentrodecustos ? String(initialData.idcentrodecustos) : '');
setIdCliente(initialData.idclientes ? String(initialData.idclientes) : '');
setIdBanco(initialData.idbancos ? String(initialData.idbancos) : '');
setHabilitado(String(initialData.habilitado ?? 1));
}, [initialData]);
@ -169,13 +180,15 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
setLoadingRefs(true);
setErro('');
const [bancosData, centrosData] = await Promise.all([
listarBancos(),
const [bancosData, centrosData, clientesData] = await Promise.all([
listarMeusBancos(),
listarCentrosCusto(),
listarClientes(),
]);
setBancos(bancosData);
setCentrosCusto(centrosData);
setClientes(clientesData);
} catch (error: any) {
const message =
error?.response?.data?.message ||
@ -196,6 +209,7 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
setValor('');
setDiaVencimento('1');
setIdCentroCusto('');
setIdCliente('');
setIdBanco('');
setHabilitado('1');
}
@ -231,6 +245,7 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
valor: Number(valor),
dia_vencimento: dia,
idcentrodecustos: toNumberOrNull(idCentroCusto),
idclientes: toNumberOrNull(idCliente),
habilitado: Number(habilitado || 1),
idbancos: toNumberOrNull(idBanco),
};
@ -400,7 +415,7 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
<FormSection
title="Classificação"
description="Associe banco/carteira e centro de custo ao movimento fixo."
description="Associe banco/carteira, centro de custo e cliente ao movimento fixo."
>
<TextField
select
@ -433,6 +448,22 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
</MenuItem>
))}
</TextField>
<TextField
select
label="Cliente"
value={idCliente}
onChange={(event) => setIdCliente(event.target.value)}
fullWidth
sx={fieldSx}
>
<MenuItem value="">Nenhum</MenuItem>
{clientes.map((cliente) => (
<MenuItem key={cliente.id} value={String(cliente.id)}>
{cliente.nome}
</MenuItem>
))}
</TextField>
</FormSection>
<Stack
@ -559,6 +590,15 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
{Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado'}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Cliente
</Typography>
<Typography fontWeight={800}>
{clienteSelecionado?.nome || 'Nenhum'}
</Typography>
</Box>
</Stack>
</Paper>
</Stack>

View File

@ -49,10 +49,13 @@ import type {
import {
listarBancos,
listarCentrosCusto,
listarClientes,
} from '../../referencias/services/referenciasService';
import type {
Banco,
CentroCusto,
Cliente,
} from '../../referencias/types/referenciasTypes';
const LIMITE_PADRAO = 20;
@ -87,7 +90,6 @@ function statusColor(habilitado: number) {
}
export function MovimentosFixosPage() {
const [movimentosFixos, setMovimentosFixos] = useState<MovimentoFixo[]>([]);
const [pagination, setPagination] = useState<MovimentosFixosPagination>({
total: 0,
limite: LIMITE_PADRAO,
@ -108,6 +110,8 @@ export function MovimentosFixosPage() {
const [bancos, setBancos] = useState<Banco[]>([]);
const [centrosCusto, setCentrosCusto] = useState<CentroCusto[]>([]);
const [movimentosFixos, setMovimentosFixos] = useState<MovimentoFixo[]>([]);
const [clientes, setClientes] = useState<Cliente[]>([]);
const [alterandoStatusId, setAlterandoStatusId] = useState<number | null>(null);
@ -121,6 +125,7 @@ export function MovimentosFixosPage() {
const [movimento, setMovimento] = useState<MovimentoFixoTipo | ''>('');
const [idBanco, setIdBanco] = useState<number | ''>('');
const [idCentroCusto, setIdCentroCusto] = useState<number | ''>('');
const [idCliente, setIdCliente] = useState<number | ''>('');
const [habilitado, setHabilitado] = useState<number | ''>('');
const [diaInicio, setDiaInicio] = useState<number | ''>('');
const [diaFim, setDiaFim] = useState<number | ''>('');
@ -137,6 +142,7 @@ export function MovimentosFixosPage() {
if (movimento) count += 1;
if (idBanco) count += 1;
if (idCentroCusto) count += 1;
if (idCliente) count += 1;
if (habilitado !== '') count += 1;
if (diaInicio !== '') count += 1;
if (diaFim !== '') count += 1;
@ -147,6 +153,7 @@ export function MovimentosFixosPage() {
movimento,
idBanco,
idCentroCusto,
idCliente,
habilitado,
diaInicio,
diaFim,
@ -156,13 +163,15 @@ export function MovimentosFixosPage() {
try {
setLoadingRefs(true);
const [bancosData, centrosData] = await Promise.all([
const [bancosData, centrosData, clientesData] = await Promise.all([
listarBancos(),
listarCentrosCusto(),
listarClientes(),
]);
setBancos(bancosData);
setCentrosCusto(centrosData);
setClientes(clientesData);
} finally {
setLoadingRefs(false);
}
@ -180,6 +189,7 @@ export function MovimentosFixosPage() {
movimento: movimento || undefined,
idbancos: idBanco || undefined,
idcentrodecustos: idCentroCusto || undefined,
idclientes: idCliente || undefined,
habilitado,
diaInicio,
diaFim,
@ -214,6 +224,7 @@ export function MovimentosFixosPage() {
setMovimento('');
setIdBanco('');
setIdCentroCusto('');
setIdCliente('');
setHabilitado('');
setDiaInicio('');
setDiaFim('');
@ -492,6 +503,24 @@ export function MovimentosFixosPage() {
))}
</TextField>
<TextField
select
label="Cliente"
value={idCliente}
onChange={(event) =>
setIdCliente(event.target.value === '' ? '' : Number(event.target.value))
}
disabled={loadingRefs}
fullWidth
>
<MenuItem value="">Todos</MenuItem>
{clientes.map((cliente) => (
<MenuItem key={cliente.id} value={cliente.id}>
{cliente.nome}
</MenuItem>
))}
</TextField>
<TextField
select
label="Status"
@ -541,6 +570,7 @@ export function MovimentosFixosPage() {
<MenuItem value="movimento">Movimento</MenuItem>
<MenuItem value="banco_descricao">Banco</MenuItem>
<MenuItem value="centro_custo_descricao">Centro</MenuItem>
<MenuItem value="cliente_nome">Cliente</MenuItem>
<MenuItem value="habilitado">Status</MenuItem>
<MenuItem value="insert_date">Cadastro</MenuItem>
<MenuItem value="update_date">Atualização</MenuItem>
@ -667,6 +697,10 @@ export function MovimentosFixosPage() {
Centro: <strong>{item.centro_custo_descricao || '-'}</strong>
</Typography>
<Typography variant="body2" color="text.secondary">
Cliente: <strong>{item.cliente_nome || '-'}</strong>
</Typography>
<Box
display="flex"
justifyContent="flex-end"
@ -721,7 +755,7 @@ export function MovimentosFixosPage() {
<Table
size="small"
sx={{
minWidth: 1120,
minWidth: 1280,
'& th': {
whiteSpace: 'nowrap',
fontWeight: 900,
@ -739,6 +773,7 @@ export function MovimentosFixosPage() {
<TableCell sx={{ minWidth: 130 }} align="right">Valor</TableCell>
<TableCell sx={{ minWidth: 110 }} align="center">Dia venc.</TableCell>
<TableCell sx={{ minWidth: 180 }}>Centro</TableCell>
<TableCell sx={{ minWidth: 180 }}>Cliente</TableCell>
<TableCell sx={{ minWidth: 180 }}>Banco</TableCell>
<TableCell sx={{ minWidth: 130 }}>Status</TableCell>
<TableCell sx={{ minWidth: 130 }}>Cadastro</TableCell>
@ -801,6 +836,16 @@ export function MovimentosFixosPage() {
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body2"
noWrap
title={item.cliente_nome || '-'}
>
{item.cliente_nome || '-'}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body2"

View File

@ -17,6 +17,7 @@ export type ListarMovimentosFixosParams = {
movimento?: string;
idbancos?: number | '';
idcentrodecustos?: number | '';
idclientes?: number | '';
habilitado?: number | '';
diaInicio?: number | '';
diaFim?: number | '';

View File

@ -7,6 +7,7 @@ export type MovimentoFixo = {
valor: number;
dia_vencimento: number;
idcentrodecustos: number | null;
idclientes?: number | null;
habilitado: number;
idbancos: number | null;
insert_date: string | null;
@ -14,6 +15,7 @@ export type MovimentoFixo = {
banco_descricao?: string | null;
centro_custo_descricao?: string | null;
cliente_nome?: string | null;
};
export type CriarMovimentoFixoRequest = {
@ -22,6 +24,7 @@ export type CriarMovimentoFixoRequest = {
valor: number;
dia_vencimento: number;
idcentrodecustos: number | null;
idclientes?: number | null;
habilitado: number;
idbancos: number | null;
};

View File

@ -11,6 +11,11 @@ export async function listarBancos(): Promise<Banco[]> {
return response.data.data;
}
export async function listarMeusBancos() {
const response = await api.get('/referencias/bancos/meus');
return response.data.data;
}
export async function listarCentrosCusto(): Promise<CentroCusto[]> {
const response = await api.get<ApiListResponse<CentroCusto>>('/referencias/centros-custo');
return response.data.data;

View File

@ -0,0 +1,757 @@
import { useMemo, useState } from 'react';
import type { ReactNode } from 'react';
import {
Alert,
Box,
Button,
Card,
CardContent,
Chip,
CircularProgress,
Divider,
IconButton,
MenuItem,
Paper,
Stack,
TextField,
Tooltip,
Typography,
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import DeleteIcon from '@mui/icons-material/Delete';
import EditNoteIcon from '@mui/icons-material/EditNote';
import RefreshIcon from '@mui/icons-material/Refresh';
import SavingsIcon from '@mui/icons-material/Savings';
import TrendingDownIcon from '@mui/icons-material/TrendingDown';
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import AccountBalanceIcon from '@mui/icons-material/AccountBalance';
import { buscarBaseSimulacaoMensal } from '../services/simulacaoMensalService';
import type {
SimulacaoMensalBase,
SimulacaoMensalItem,
SimulacaoMensalOrigemItem,
SimulacaoMensalResumo,
SimulacaoMensalTipoItem,
} from '../types/simulacaoMensalTypes';
const meses = [
{ value: 1, label: 'Janeiro' },
{ value: 2, label: 'Fevereiro' },
{ value: 3, label: 'Março' },
{ value: 4, label: 'Abril' },
{ value: 5, label: 'Maio' },
{ value: 6, label: 'Junho' },
{ value: 7, label: 'Julho' },
{ value: 8, label: 'Agosto' },
{ value: 9, label: 'Setembro' },
{ value: 10, label: 'Outubro' },
{ value: 11, label: 'Novembro' },
{ value: 12, label: 'Dezembro' },
];
function mesAtual() {
return new Date().getMonth() + 1;
}
function anoAtual() {
return new Date().getFullYear();
}
function formatarValor(valor: number) {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(Number(valor || 0));
}
function formatarData(data?: string | null) {
if (!data) return '-';
return new Date(`${String(data).slice(0, 10)}T00:00:00`).toLocaleDateString('pt-BR');
}
function formatarPercentual(valor?: number | null) {
if (valor === null || valor === undefined || !Number.isFinite(Number(valor))) {
return '-';
}
return `${Number(valor).toFixed(2)}%`;
}
function calcularPercentual(valor: number, totalEntradas: number) {
if (!totalEntradas) return null;
return Number(((Number(valor || 0) / totalEntradas) * 100).toFixed(2));
}
function calcularResumoLocal(
entradas: SimulacaoMensalItem[],
saidas: SimulacaoMensalItem[]
): SimulacaoMensalResumo {
const totalEntradas = entradas.reduce((total, item) => total + Number(item.valor || 0), 0);
const totalSaidas = saidas.reduce((total, item) => total + Number(item.valor || 0), 0);
const totalInvestimentos = saidas
.filter((item) => Number(item.investimento) === 1)
.reduce((total, item) => total + Number(item.valor || 0), 0);
const totalGastos = totalSaidas - totalInvestimentos;
const resultado = totalEntradas - totalSaidas;
return {
totalEntradas,
totalSaidas,
totalGastos,
totalInvestimentos,
resultado,
percentualEntradas: totalEntradas > 0 ? 100 : null,
percentualSaidas: calcularPercentual(totalSaidas, totalEntradas),
percentualGastos: calcularPercentual(totalGastos, totalEntradas),
percentualInvestimentos: calcularPercentual(totalInvestimentos, totalEntradas),
percentualResultado: calcularPercentual(resultado, totalEntradas),
percentualComprometido: calcularPercentual(totalSaidas, totalEntradas),
quantidadeEntradas: entradas.length,
quantidadeSaidas: saidas.length,
quantidadeInvestimentos: saidas.filter((item) => Number(item.investimento) === 1).length,
};
}
function origemLabel(origem: string) {
const labels: Record<string, string> = {
movimento_existente: 'Movimento',
movimento_fixo_previsto: 'Fixo previsto',
saldo_banco: 'Saldo banco',
cartao_credito_agrupado: 'Cartão',
limite_centro_custo: 'Limite centro',
manual: 'Manual',
};
return labels[origem] || origem;
}
function origemColor(origem: string) {
if (origem === 'manual') return 'secondary';
if (origem === 'saldo_banco') return 'success';
if (origem === 'cartao_credito_agrupado') return 'warning';
if (origem === 'movimento_fixo_previsto') return 'info';
if (origem === 'limite_centro_custo') return 'error';
return 'default';
}
function criarItemManual(tipo: SimulacaoMensalTipoItem): SimulacaoMensalItem {
const id = `manual:${tipo}:${Date.now()}:${Math.random().toString(16).slice(2)}`;
return {
id,
tipo,
origem: 'manual',
descricao: tipo === 'entrada' ? 'Nova entrada' : 'Nova saída',
valor: 0,
valor_original: 0,
movimento: tipo === 'entrada' ? 'Entrada' : 'Saida',
status: 'Simulado',
data: null,
editavel: true,
editado: true,
};
}
type SimulacaoListaProps = {
titulo: string;
tipo: SimulacaoMensalTipoItem;
itens: SimulacaoMensalItem[];
total: number;
onAlterarItem: (tipo: SimulacaoMensalTipoItem, id: string, patch: Partial<SimulacaoMensalItem>) => void;
onRemoverItem: (tipo: SimulacaoMensalTipoItem, id: string) => void;
onAdicionarItem: (tipo: SimulacaoMensalTipoItem) => void;
};
function SimulacaoLista({
titulo,
tipo,
itens,
total,
onAlterarItem,
onRemoverItem,
onAdicionarItem,
}: SimulacaoListaProps) {
const isEntrada = tipo === 'entrada';
return (
<Paper
elevation={0}
sx={{
borderRadius: 4,
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden',
backgroundColor: '#FFFFFF',
}}
>
<Box
sx={{
padding: 2,
background: isEntrada
? 'linear-gradient(135deg, rgba(34,197,94,0.10), rgba(255,255,255,0.9))'
: 'linear-gradient(135deg, rgba(239,68,68,0.10), rgba(255,255,255,0.9))',
}}
>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={1.5}
>
<Stack direction="row" alignItems="center" spacing={1}>
{isEntrada ? <TrendingUpIcon color="success" /> : <TrendingDownIcon color="error" />}
<Box>
<Typography variant="h6" fontWeight={950}>
{titulo}
</Typography>
<Typography variant="body2" color="text.secondary">
{itens.length} item(ns)
</Typography>
</Box>
</Stack>
<Stack direction="row" spacing={1} alignItems="center">
<Chip
label={formatarValor(total)}
color={isEntrada ? 'success' : 'error'}
sx={{ fontWeight: 900 }}
/>
<Button
size="small"
variant="outlined"
startIcon={<AddIcon />}
onClick={() => onAdicionarItem(tipo)}
>
Adicionar
</Button>
</Stack>
</Stack>
</Box>
<Divider />
<Stack spacing={1.25} padding={1.5}>
{itens.length === 0 ? (
<Box padding={2}>
<Typography fontWeight={800}>
Nenhum item.
</Typography>
<Typography variant="body2" color="text.secondary">
Adicione manualmente ou preencha a previsão pela API.
</Typography>
</Box>
) : (
itens.map((item) => (
<Paper
key={item.id}
elevation={0}
sx={{
padding: 1.5,
borderRadius: 2.5,
border: '1px solid',
borderColor: item.editado ? 'primary.light' : 'divider',
backgroundColor: item.editado ? 'rgba(59,130,246,0.05)' : 'rgba(248,250,252,0.75)',
}}
>
<Stack spacing={1.25}>
<Stack
direction={{ xs: 'column', md: 'row' }}
spacing={1}
alignItems={{ xs: 'stretch', md: 'center' }}
>
<TextField
label="Descrição"
value={item.descricao}
size="small"
fullWidth
onChange={(event) =>
onAlterarItem(tipo, item.id, {
descricao: event.target.value,
editado: true,
})
}
/>
<TextField
label="Valor"
type="number"
value={item.valor}
size="small"
sx={{ width: { xs: '100%', md: 170 } }}
inputProps={{
min: 0,
step: '0.01',
}}
onChange={(event) =>
onAlterarItem(tipo, item.id, {
valor: Number(event.target.value || 0),
editado: true,
})
}
/>
<Tooltip title="Remover da simulação">
<IconButton
color="error"
onClick={() => onRemoverItem(tipo, item.id)}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</Stack>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
<Chip
size="small"
label={origemLabel(item.origem)}
color={origemColor(item.origem) as any}
variant="outlined"
/>
{item.editado && (
<Chip
size="small"
label="Editado"
color="primary"
icon={<EditNoteIcon />}
variant="outlined"
/>
)}
{item.data && (
<Chip
size="small"
label={formatarData(item.data)}
variant="outlined"
/>
)}
{item.banco_descricao && (
<Chip
size="small"
label={item.banco_descricao}
variant="outlined"
/>
)}
{item.centro_custo_descricao && (
<Chip
size="small"
label={item.centro_custo_descricao}
variant="outlined"
/>
)}
{item.cliente_nome && (
<Chip
size="small"
label={item.cliente_nome}
variant="outlined"
/>
)}
{Number(item.valor_original || 0) !== Number(item.valor || 0) && (
<Chip
size="small"
label={`Original ${formatarValor(item.valor_original)}`}
color="warning"
variant="outlined"
/>
)}
</Stack>
</Stack>
</Paper>
))
)}
</Stack>
</Paper>
);
}
export function SimulacaoMensalPage() {
const [ano, setAno] = useState(anoAtual());
const [mes, setMes] = useState(mesAtual());
const [incluirQuitadas, setIncluirQuitadas] = useState(false);
const [incluirFixosVencidos, setIncluirFixosVencidos] = useState(false);
const [base, setBase] = useState<SimulacaoMensalBase | null>(null);
const [entradas, setEntradas] = useState<SimulacaoMensalItem[]>([]);
const [saidas, setSaidas] = useState<SimulacaoMensalItem[]>([]);
const [loading, setLoading] = useState(false);
const [erro, setErro] = useState('');
const [sucesso, setSucesso] = useState('');
const resumo = useMemo(() => {
return calcularResumoLocal(entradas, saidas);
}, [entradas, saidas]);
async function carregarBase() {
try {
setLoading(true);
setErro('');
setSucesso('');
const data = await buscarBaseSimulacaoMensal({
ano,
mes,
incluirQuitadas,
incluirFixosVencidosDoMesAtual: incluirFixosVencidos,
});
setBase(data);
setEntradas(data.entradas || []);
setSaidas(data.saidas || []);
setSucesso('Previsão preenchida com sucesso.');
} catch (error: any) {
const message =
error?.response?.data?.message ||
'Não foi possível montar a simulação mensal.';
setErro(message);
} finally {
setLoading(false);
}
}
function limparSimulacao() {
setBase(null);
setEntradas([]);
setSaidas([]);
setErro('');
setSucesso('');
}
function alterarItem(
tipo: SimulacaoMensalTipoItem,
id: string,
patch: Partial<SimulacaoMensalItem>
) {
const atualizar = (lista: SimulacaoMensalItem[]) =>
lista.map((item) =>
item.id === id
? {
...item,
...patch,
}
: item
);
if (tipo === 'entrada') {
setEntradas((listaAtual) => atualizar(listaAtual));
} else {
setSaidas((listaAtual) => atualizar(listaAtual));
}
}
function removerItem(tipo: SimulacaoMensalTipoItem, id: string) {
if (tipo === 'entrada') {
setEntradas((listaAtual) => listaAtual.filter((item) => item.id !== id));
} else {
setSaidas((listaAtual) => listaAtual.filter((item) => item.id !== id));
}
}
function adicionarItem(tipo: SimulacaoMensalTipoItem) {
const novoItem = criarItemManual(tipo);
if (tipo === 'entrada') {
setEntradas((listaAtual) => [novoItem, ...listaAtual]);
} else {
setSaidas((listaAtual) => [novoItem, ...listaAtual]);
}
}
const resultadoColor =
resumo.resultado > 0
? 'success.main'
: resumo.resultado < 0
? 'error.main'
: 'text.primary';
function ResumoCard({
icon,
label,
valor,
percentual,
color,
}: {
icon: ReactNode;
label: string;
valor: number;
percentual?: number | null;
color: string;
}) {
return (
<Card>
<CardContent>
<Stack direction="row" spacing={1} alignItems="center">
{icon}
<Typography variant="body2" color="text.secondary">
{label}
</Typography>
</Stack>
<Typography variant="h5" fontWeight={950} color={color}>
{formatarValor(valor)}
</Typography>
<Typography variant="caption" color="text.secondary">
{formatarPercentual(percentual)} das entradas
</Typography>
</CardContent>
</Card>
);
}
return (
<Box>
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', md: 'center' }}
spacing={2}
marginBottom={{ xs: 3, md: 4 }}
>
<Box>
<Chip
icon={<AutoAwesomeIcon />}
label="Laboratório financeiro"
color="primary"
variant="outlined"
sx={{ marginBottom: 1.25, fontWeight: 700 }}
/>
<Typography variant="h4" fontWeight={950}>
Simulação mensal
</Typography>
<Typography color="text.secondary" sx={{ marginTop: 0.5 }}>
Monte uma previsão do mês, ajuste valores livremente e veja quanto sobra.
</Typography>
</Box>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5}>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={limparSimulacao}
disabled={loading}
>
Limpar
</Button>
<Button
variant="contained"
startIcon={
loading
? <CircularProgress size={18} color="inherit" />
: <AutoAwesomeIcon />
}
onClick={carregarBase}
disabled={loading}
sx={{ boxShadow: 3 }}
>
{loading ? 'Preenchendo...' : 'Preencher previsão'}
</Button>
</Stack>
</Stack>
{erro && (
<Alert severity="error" sx={{ marginBottom: 2.5 }}>
{erro}
</Alert>
)}
{sucesso && (
<Alert severity="success" sx={{ marginBottom: 2.5 }}>
{sucesso}
</Alert>
)}
<Card sx={{ marginBottom: 3 }}>
<CardContent sx={{ padding: { xs: 2, md: 3 } }}>
<Box
display="grid"
gridTemplateColumns={{
xs: '1fr',
sm: '1fr 1fr',
md: 'repeat(4, 1fr)',
}}
gap={2}
>
<TextField
select
label="Mês"
value={mes}
onChange={(event) => setMes(Number(event.target.value))}
fullWidth
>
{meses.map((item) => (
<MenuItem key={item.value} value={item.value}>
{item.label}
</MenuItem>
))}
</TextField>
<TextField
label="Ano"
type="number"
value={ano}
onChange={(event) => setAno(Number(event.target.value))}
fullWidth
/>
<TextField
select
label="Incluir já quitadas?"
value={incluirQuitadas ? 1 : 0}
onChange={(event) => setIncluirQuitadas(Number(event.target.value) === 1)}
fullWidth
>
<MenuItem value={0}>Não, em aberto</MenuItem>
<MenuItem value={1}>Sim, incluir quitadas</MenuItem>
</TextField>
<TextField
select
label="Fixos vencidos no mês atual"
value={incluirFixosVencidos ? 1 : 0}
onChange={(event) => setIncluirFixosVencidos(Number(event.target.value) === 1)}
fullWidth
>
<MenuItem value={0}>Ignorar vencidos</MenuItem>
<MenuItem value={1}>Incluir vencidos</MenuItem>
</TextField>
</Box>
</CardContent>
</Card>
<Box
display="grid"
gridTemplateColumns={{
xs: '1fr',
sm: '1fr 1fr',
lg: 'repeat(5, 1fr)',
}}
gap={2}
marginBottom={3}
>
<ResumoCard
icon={<TrendingUpIcon color="success" />}
label="Entradas"
valor={resumo.totalEntradas}
percentual={resumo.percentualEntradas}
color="success.main"
/>
<ResumoCard
icon={<TrendingDownIcon color="error" />}
label="Gastos"
valor={resumo.totalGastos || 0}
percentual={resumo.percentualGastos}
color="error.main"
/>
<ResumoCard
icon={<AccountBalanceIcon color="info" />}
label="Investimentos"
valor={resumo.totalInvestimentos || 0}
percentual={resumo.percentualInvestimentos}
color="info.main"
/>
<ResumoCard
icon={<TrendingDownIcon color="warning" />}
label="Saídas totais"
valor={resumo.totalSaidas}
percentual={resumo.percentualSaidas}
color="warning.main"
/>
<ResumoCard
icon={<SavingsIcon color={resumo.resultado >= 0 ? 'success' : 'error'} />}
label="Resultado"
valor={resumo.resultado}
percentual={resumo.percentualResultado}
color={resultadoColor}
/>
</Box>
{base?.avisos && base.avisos.length > 0 && (
<Stack spacing={1} marginBottom={3}>
{base.avisos.map((aviso, index) => (
<Alert key={`${aviso.tipo}-${index}`} severity="warning">
{aviso.mensagem}
</Alert>
))}
</Stack>
)}
{base && (
<Paper
elevation={0}
sx={{
padding: 2,
marginBottom: 3,
borderRadius: 3,
border: '1px solid',
borderColor: 'divider',
backgroundColor: 'rgba(255,255,255,0.75)',
}}
>
<Stack direction="row" flexWrap="wrap" spacing={1} useFlexGap>
<Chip label={`Competência ${base.competencia}`} variant="outlined" />
<Chip label={`${base.fontes.movimentosExistentes} movimentos existentes`} variant="outlined" />
<Chip label={`${base.fontes.movimentosFixos} fixos analisados`} variant="outlined" />
<Chip label={`${base.fontes.bancos} bancos`} variant="outlined" />
<Chip label={`${base.fontes.centrosSimulaveis} centros simuláveis`} variant="outlined" />
</Stack>
</Paper>
)}
<Box
display="grid"
gridTemplateColumns={{
xs: '1fr',
xl: '1fr 1fr',
}}
gap={3}
>
<SimulacaoLista
titulo="Entradas previstas"
tipo="entrada"
itens={entradas}
total={resumo.totalEntradas}
onAlterarItem={alterarItem}
onRemoverItem={removerItem}
onAdicionarItem={adicionarItem}
/>
<SimulacaoLista
titulo="Saídas previstas"
tipo="saida"
itens={saidas}
total={resumo.totalSaidas}
onAlterarItem={alterarItem}
onRemoverItem={removerItem}
onAdicionarItem={adicionarItem}
/>
</Box>
</Box>
);
}

View File

@ -0,0 +1,20 @@
import { api } from '../../../services/api';
import type {
BuscarBaseSimulacaoMensalParams,
SimulacaoMensalBase,
} from '../types/simulacaoMensalTypes';
export async function buscarBaseSimulacaoMensal(
params: BuscarBaseSimulacaoMensalParams
): Promise<SimulacaoMensalBase> {
const response = await api.get('/simulacao-mensal/base', {
params: {
ano: params.ano,
mes: params.mes,
incluirQuitadas: params.incluirQuitadas ? 1 : 0,
incluirFixosVencidosDoMesAtual: params.incluirFixosVencidosDoMesAtual ? 1 : 0,
},
});
return response.data.data;
}

View File

@ -0,0 +1,101 @@
export type SimulacaoMensalTipoItem = 'entrada' | 'saida';
export type SimulacaoMensalOrigemItem =
| 'movimento_existente'
| 'movimento_fixo_previsto'
| 'saldo_banco'
| 'cartao_credito_agrupado'
| 'limite_centro_custo'
| 'manual';
export type SimulacaoMensalItem = {
id: string;
tipo: SimulacaoMensalTipoItem;
origem: SimulacaoMensalOrigemItem | string;
descricao: string;
valor: number;
valor_original: number;
movimento?: string | null;
status?: string | null;
data?: string | null;
idcontasapagar?: number | null;
idmovimentosfixos?: number | null;
idbancos?: number | null;
banco_descricao?: string | null;
banco_debito?: number | null;
idcentrodecustos?: number | null;
centro_custo_descricao?: string | null;
investimento?: number | null;
idclientes?: number | null;
cliente_nome?: string | null;
editavel: boolean;
editado: boolean;
detalhes?: any;
};
export type SimulacaoMensalResumo = {
totalEntradas: number;
totalSaidas: number;
totalGastos?: number;
totalInvestimentos?: number;
resultado: number;
percentualEntradas?: number | null;
percentualSaidas?: number | null;
percentualGastos?: number | null;
percentualInvestimentos?: number | null;
percentualResultado?: number | null;
percentualComprometido: number | null;
quantidadeEntradas: number;
quantidadeSaidas: number;
quantidadeInvestimentos?: number;
};
export type SimulacaoMensalAviso = {
tipo: string;
mensagem: string;
[key: string]: any;
};
export type SimulacaoMensalBase = {
competencia: string;
ano: number;
mes: number;
dataInicio: string;
dataFim: string;
opcoes: {
incluirQuitadas: boolean;
incluirFixosVencidosDoMesAtual: boolean;
};
entradas: SimulacaoMensalItem[];
saidas: SimulacaoMensalItem[];
resumo: SimulacaoMensalResumo;
fontes: {
movimentosExistentes: number;
movimentosFixos: number;
bancos: number;
centrosSimulaveis: number;
};
avisos: SimulacaoMensalAviso[];
};
export type BuscarBaseSimulacaoMensalParams = {
ano: number;
mes: number;
incluirQuitadas?: boolean;
incluirFixosVencidosDoMesAtual?: boolean;
};