adicionado extrato e simulação
This commit is contained in:
parent
66287cff29
commit
759e642567
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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 />,
|
||||
}
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export type ListarMovimentosFixosParams = {
|
|||
movimento?: string;
|
||||
idbancos?: number | '';
|
||||
idcentrodecustos?: number | '';
|
||||
idclientes?: number | '';
|
||||
habilitado?: number | '';
|
||||
diaInicio?: number | '';
|
||||
diaFim?: number | '';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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, só 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
Loading…
Reference in New Issue