diff --git a/financeiro-api/src/app.js b/financeiro-api/src/app.js
index ed0be74..c7072c1 100644
--- a/financeiro-api/src/app.js
+++ b/financeiro-api/src/app.js
@@ -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({
diff --git a/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js b/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js
index cade1e6..2ede1e3 100644
--- a/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js
+++ b/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js
@@ -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,
diff --git a/financeiro-api/src/modules/movimentos/services/movimentos.service.js b/financeiro-api/src/modules/movimentos/services/movimentos.service.js
index dd9aca8..320b702 100644
--- a/financeiro-api/src/modules/movimentos/services/movimentos.service.js
+++ b/financeiro-api/src/modules/movimentos/services/movimentos.service.js
@@ -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 = {
diff --git a/financeiro-api/src/modules/movimentos/services/movimentosSaldo.service.js b/financeiro-api/src/modules/movimentos/services/movimentosSaldo.service.js
index d18cac3..e7a95cc 100644
--- a/financeiro-api/src/modules/movimentos/services/movimentosSaldo.service.js
+++ b/financeiro-api/src/modules/movimentos/services/movimentosSaldo.service.js
@@ -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();
}
diff --git a/financeiro-api/src/modules/movimentosFixos/controllers/movimentosFixos.controller.js b/financeiro-api/src/modules/movimentosFixos/controllers/movimentosFixos.controller.js
index d93637f..3b2d12c 100644
--- a/financeiro-api/src/modules/movimentosFixos/controllers/movimentosFixos.controller.js
+++ b/financeiro-api/src/modules/movimentosFixos/controllers/movimentosFixos.controller.js
@@ -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({
diff --git a/financeiro-api/src/modules/movimentosFixos/services/movimentosFixos.service.js b/financeiro-api/src/modules/movimentosFixos/services/movimentosFixos.service.js
index 9fa9456..d7698d6 100644
--- a/financeiro-api/src/modules/movimentosFixos/services/movimentosFixos.service.js
+++ b/financeiro-api/src/modules/movimentosFixos/services/movimentosFixos.service.js
@@ -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;
diff --git a/financeiro-api/src/modules/movimentosFixos/services/movimentosFixosGeracao.service.js b/financeiro-api/src/modules/movimentosFixos/services/movimentosFixosGeracao.service.js
index 27575c6..efa7163 100644
--- a/financeiro-api/src/modules/movimentosFixos/services/movimentosFixosGeracao.service.js
+++ b/financeiro-api/src/modules/movimentosFixos/services/movimentosFixosGeracao.service.js
@@ -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,
]
diff --git a/financeiro-api/src/modules/referencias/controllers/referencias.controller.js b/financeiro-api/src/modules/referencias/controllers/referencias.controller.js
index e0205ec..54aee70 100644
--- a/financeiro-api/src/modules/referencias/controllers/referencias.controller.js
+++ b/financeiro-api/src/modules/referencias/controllers/referencias.controller.js
@@ -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,
};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/referencias/routes/referencias.routes.js b/financeiro-api/src/modules/referencias/routes/referencias.routes.js
index 04b296f..de9cd57 100644
--- a/financeiro-api/src/modules/referencias/routes/referencias.routes.js
+++ b/financeiro-api/src/modules/referencias/routes/referencias.routes.js
@@ -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);
diff --git a/financeiro-api/src/modules/referencias/services/referencias.service.js b/financeiro-api/src/modules/referencias/services/referencias.service.js
index bbff766..827132f 100644
--- a/financeiro-api/src/modules/referencias/services/referencias.service.js
+++ b/financeiro-api/src/modules/referencias/services/referencias.service.js
@@ -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,
};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/simulacaoMensal/controllers/simulacaoMensal.controller.js b/financeiro-api/src/modules/simulacaoMensal/controllers/simulacaoMensal.controller.js
new file mode 100644
index 0000000..e35baf1
--- /dev/null
+++ b/financeiro-api/src/modules/simulacaoMensal/controllers/simulacaoMensal.controller.js
@@ -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,
+};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/simulacaoMensal/routes/simulacaoMensal.routes.js b/financeiro-api/src/modules/simulacaoMensal/routes/simulacaoMensal.routes.js
new file mode 100644
index 0000000..6885e04
--- /dev/null
+++ b/financeiro-api/src/modules/simulacaoMensal/routes/simulacaoMensal.routes.js
@@ -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;
\ No newline at end of file
diff --git a/financeiro-api/src/modules/simulacaoMensal/services/simulacaoMensal.service.js b/financeiro-api/src/modules/simulacaoMensal/services/simulacaoMensal.service.js
new file mode 100644
index 0000000..1b7bc3e
--- /dev/null
+++ b/financeiro-api/src/modules/simulacaoMensal/services/simulacaoMensal.service.js
@@ -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,
+};
\ No newline at end of file
diff --git a/financeiro-web/src/app/router.tsx b/financeiro-web/src/app/router.tsx
index d7d7571..1910291 100644
--- a/financeiro-web/src/app/router.tsx
+++ b/financeiro-web/src/app/router.tsx
@@ -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: ,
+ },
+ {
+ path: 'simulacao-mensal',
+ element: ,
}
],
},
diff --git a/financeiro-web/src/components/layout/Sidebar.tsx b/financeiro-web/src/components/layout/Sidebar.tsx
index 4d95b80..77d508b 100644
--- a/financeiro-web/src/components/layout/Sidebar.tsx
+++ b/financeiro-web/src/components/layout/Sidebar.tsx
@@ -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: ,
},
+ {
+ label: 'Simulação mensal',
+ path: '/simulacao-mensal',
+ icon: ,
+ },
{
label: 'Configurações',
path: '/configuracoes',
diff --git a/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx b/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx
index e85a027..7014266 100644
--- a/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx
+++ b/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx
@@ -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 (
([]);
+ const [meusBancos, setMeusBancos] = useState([]);
+ const [todosBancos, setTodosBancos] = useState([]);
const [centrosCusto, setCentrosCusto] = useState([]);
const [clientes, setClientes] = useState([]);
@@ -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(() => {
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}
>
- {bancos.map((banco) => (
+ {meusBancos.map((banco) => (