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 usuariosRoutes = require('./modules/usuarios/routes/usuarios.routes');
|
||||||
const quitacoesCreditoRoutes = require('./modules/quitacoesCredito/routes/quitacoesCredito.routes');
|
const quitacoesCreditoRoutes = require('./modules/quitacoesCredito/routes/quitacoesCredito.routes');
|
||||||
const dashboardRoutes = require('./modules/dashboard/routes/dashboard.routes');
|
const dashboardRoutes = require('./modules/dashboard/routes/dashboard.routes');
|
||||||
|
const simulacaoMensalRoutes = require('./modules/simulacaoMensal/routes/simulacaoMensal.routes');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
|
@ -48,6 +49,7 @@ app.use('/api/movimentos-fixos', movimentosFixosRoutes);
|
||||||
app.use('/api/usuarios', usuariosRoutes);
|
app.use('/api/usuarios', usuariosRoutes);
|
||||||
app.use('/api/quitacoes-credito', quitacoesCreditoRoutes);
|
app.use('/api/quitacoes-credito', quitacoesCreditoRoutes);
|
||||||
app.use('/api/dashboard', dashboardRoutes);
|
app.use('/api/dashboard', dashboardRoutes);
|
||||||
|
app.use('/api/simulacao-mensal', simulacaoMensalRoutes);
|
||||||
|
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
return res.status(404).json({
|
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({
|
return res.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
|
||||||
|
|
@ -791,6 +791,7 @@ async function criarMovimento(idUsuario, dados) {
|
||||||
tipo_operacao: 'movimento_aplicado',
|
tipo_operacao: 'movimento_aplicado',
|
||||||
origem: movimentoCriado.origem || 'movimento',
|
origem: movimentoCriado.origem || 'movimento',
|
||||||
idusuarios: idUsuario,
|
idusuarios: idUsuario,
|
||||||
|
permitirBancoDestinoGlobal: movimentoCriado.movimento === 'Sangria',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -882,6 +883,7 @@ async function atualizarMovimento(id, idUsuario, dados) {
|
||||||
tipo_operacao: 'movimento_revertido',
|
tipo_operacao: 'movimento_revertido',
|
||||||
origem: 'edicao_movimento',
|
origem: 'edicao_movimento',
|
||||||
idusuarios: idUsuario,
|
idusuarios: idUsuario,
|
||||||
|
permitirBancoDestinoGlobal: movimentoAntigo.movimento === 'Sangria',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -902,6 +904,7 @@ async function atualizarMovimento(id, idUsuario, dados) {
|
||||||
tipo_operacao: 'movimento_aplicado',
|
tipo_operacao: 'movimento_aplicado',
|
||||||
origem: 'edicao_movimento',
|
origem: 'edicao_movimento',
|
||||||
idusuarios: idUsuario,
|
idusuarios: idUsuario,
|
||||||
|
permitirBancoDestinoGlobal: movimentoNovo.movimento === 'Sangria',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -977,6 +980,7 @@ async function deletarMovimento(id, idUsuario) {
|
||||||
tipo_operacao: 'movimento_revertido',
|
tipo_operacao: 'movimento_revertido',
|
||||||
origem: 'exclusao_movimento',
|
origem: 'exclusao_movimento',
|
||||||
idusuarios: idUsuario,
|
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;
|
let movimentoAntigo = null;
|
||||||
|
|
||||||
if (idcontasapagar) {
|
if (idcontasapagar) {
|
||||||
movimentoAntigo = await buscarMovimentoPorId(idcontasapagar);
|
movimentoAntigo = await buscarMovimentoPorId(idcontasapagar, {
|
||||||
|
idUsuario,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const movimentoNovo = normalizarMovimentoParaSalvar(null, dados, {
|
const movimentoNovo = normalizarMovimentoParaSalvar(idUsuario, dados, {
|
||||||
idusuarios_cad: dados.idusuarios_cad || null,
|
idusuarios_cad: dados.idusuarios_cad || idUsuario || null,
|
||||||
grupo_parcelamento: dados.grupo_parcelamento || null,
|
grupo_parcelamento: dados.grupo_parcelamento || null,
|
||||||
origem: dados.origem || 'manual',
|
origem: dados.origem || 'manual',
|
||||||
});
|
});
|
||||||
|
|
||||||
return movimentosSaldoService.preverImpactoSaldo(movimentoNovo, movimentoAntigo);
|
return movimentosSaldoService.preverImpactoSaldo(
|
||||||
|
movimentoNovo,
|
||||||
|
movimentoAntigo,
|
||||||
|
{
|
||||||
|
idusuarios: idUsuario || null,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,70 @@ function bancoEhCredito(banco) {
|
||||||
return Number(banco?.debito) === 0;
|
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) {
|
async function buscarBanco(connection, idbancos, idusuarios = null) {
|
||||||
if (!idbancos) return null;
|
if (!idbancos) return null;
|
||||||
|
|
||||||
|
|
@ -19,22 +83,27 @@ async function buscarBanco(connection, idbancos, idusuarios = null) {
|
||||||
let filtroUsuario = '';
|
let filtroUsuario = '';
|
||||||
|
|
||||||
if (idusuarios) {
|
if (idusuarios) {
|
||||||
filtroUsuario = 'AND idusuarios = ?';
|
filtroUsuario = 'AND b.idusuarios = ?';
|
||||||
params.push(idusuarios);
|
params.push(idusuarios);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [rows] = await connection.query(
|
const [rows] = await connection.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
idbancos,
|
b.idbancos,
|
||||||
descricao,
|
b.descricao,
|
||||||
saldo,
|
b.descricao AS banco_nome,
|
||||||
debito,
|
b.saldo,
|
||||||
investimento,
|
b.debito,
|
||||||
idusuarios
|
b.investimento,
|
||||||
FROM bancos
|
b.idusuarios,
|
||||||
WHERE idbancos = ?
|
u.nome AS usuario_nome
|
||||||
${filtroUsuario}
|
FROM bancos b
|
||||||
|
LEFT JOIN usuarios u
|
||||||
|
ON u.idusuarios = b.idusuarios
|
||||||
|
WHERE b.idbancos = ?
|
||||||
|
AND b.habilitado = 1
|
||||||
|
${filtroUsuario}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`,
|
`,
|
||||||
params
|
params
|
||||||
|
|
@ -50,22 +119,27 @@ async function buscarBancoParaUpdate(connection, idbancos, idusuarios = null) {
|
||||||
let filtroUsuario = '';
|
let filtroUsuario = '';
|
||||||
|
|
||||||
if (idusuarios) {
|
if (idusuarios) {
|
||||||
filtroUsuario = 'AND idusuarios = ?';
|
filtroUsuario = 'AND b.idusuarios = ?';
|
||||||
params.push(idusuarios);
|
params.push(idusuarios);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [rows] = await connection.query(
|
const [rows] = await connection.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
idbancos,
|
b.idbancos,
|
||||||
descricao,
|
b.descricao,
|
||||||
saldo,
|
b.descricao AS banco_nome,
|
||||||
debito,
|
b.saldo,
|
||||||
investimento,
|
b.debito,
|
||||||
idusuarios
|
b.investimento,
|
||||||
FROM bancos
|
b.idusuarios,
|
||||||
WHERE idbancos = ?
|
u.nome AS usuario_nome
|
||||||
${filtroUsuario}
|
FROM bancos b
|
||||||
|
LEFT JOIN usuarios u
|
||||||
|
ON u.idusuarios = b.idusuarios
|
||||||
|
WHERE b.idbancos = ?
|
||||||
|
AND b.habilitado = 1
|
||||||
|
${filtroUsuario}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
`,
|
`,
|
||||||
|
|
@ -168,8 +242,27 @@ function calcularDeltasParaMovimento(movimento, bancoOrigem, bancoDestino = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function calcularImpactoMovimento(connection, movimento, contexto = {}) {
|
async function calcularImpactoMovimento(connection, movimento, contexto = {}) {
|
||||||
const bancoOrigem = await buscarBanco(connection, movimento.idbancos, contexto.idusuarios);
|
const bancoOrigem = await buscarBanco(
|
||||||
const bancoDestino = await buscarBanco(connection, movimento.idbancos_p, contexto.idusuarios);
|
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);
|
return calcularDeltasParaMovimento(movimento, bancoOrigem, bancoDestino);
|
||||||
}
|
}
|
||||||
|
|
@ -186,15 +279,26 @@ async function aplicarDeltasSaldo(connection, deltas, contexto = {}) {
|
||||||
const impactos = [];
|
const impactos = [];
|
||||||
|
|
||||||
for (const delta of deltas) {
|
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) {
|
if (!banco) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const saldoAnterior = Number(banco.saldo || 0);
|
const impacto = montarImpactoSaldo(delta, banco);
|
||||||
const valorDelta = Number(delta.valor_delta || 0);
|
|
||||||
const saldoPosterior = saldoAnterior + valorDelta;
|
|
||||||
|
|
||||||
await connection.query(
|
await connection.query(
|
||||||
`
|
`
|
||||||
|
|
@ -205,7 +309,7 @@ async function aplicarDeltasSaldo(connection, deltas, contexto = {}) {
|
||||||
WHERE idbancos = ?
|
WHERE idbancos = ?
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
saldoPosterior,
|
impacto.saldo_posterior,
|
||||||
banco.idbancos,
|
banco.idbancos,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
@ -232,42 +336,65 @@ async function aplicarDeltasSaldo(connection, deltas, contexto = {}) {
|
||||||
contexto.idcontasquitadas || null,
|
contexto.idcontasquitadas || null,
|
||||||
contexto.tipo_operacao || null,
|
contexto.tipo_operacao || null,
|
||||||
contexto.origem || null,
|
contexto.origem || null,
|
||||||
valorDelta,
|
impacto.valor_delta,
|
||||||
saldoAnterior,
|
impacto.saldo_anterior,
|
||||||
saldoPosterior,
|
impacto.saldo_posterior,
|
||||||
delta.descricao || null,
|
delta.descricao || null,
|
||||||
contexto.idusuarios || null,
|
contexto.idusuarios || null,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
impactos.push({
|
impactos.push(impacto);
|
||||||
idbancos: banco.idbancos,
|
|
||||||
banco_descricao: banco.descricao,
|
|
||||||
valor_delta: valorDelta,
|
|
||||||
saldo_anterior: saldoAnterior,
|
|
||||||
saldo_posterior: saldoPosterior,
|
|
||||||
descricao: delta.descricao || null,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return impactos;
|
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();
|
const connection = await pool.getConnection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const deltas = [];
|
const deltas = [];
|
||||||
|
|
||||||
if (movimentoAntigo) {
|
if (movimentoAntigo) {
|
||||||
const deltasAntigos = await calcularImpactoMovimento(connection, movimentoAntigo);
|
const deltasAntigos = await calcularImpactoMovimento(
|
||||||
|
connection,
|
||||||
|
movimentoAntigo,
|
||||||
|
{
|
||||||
|
idusuarios: contexto.idusuarios || null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
deltas.push(...inverterDeltas(deltasAntigos));
|
deltas.push(...inverterDeltas(deltasAntigos));
|
||||||
}
|
}
|
||||||
|
|
||||||
const deltasNovos = await calcularImpactoMovimento(connection, dados);
|
const deltasNovos = await calcularImpactoMovimento(
|
||||||
|
connection,
|
||||||
|
dados,
|
||||||
|
{
|
||||||
|
idusuarios: contexto.idusuarios || null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
deltas.push(...deltasNovos);
|
deltas.push(...deltasNovos);
|
||||||
|
|
||||||
return deltas;
|
return enriquecerDeltasComoPrevisao(connection, deltas);
|
||||||
} finally {
|
} finally {
|
||||||
connection.release();
|
connection.release();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
const movimentosFixosService = require('../services/movimentosFixos.service');
|
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) {
|
function validarDadosMovimentoFixo(dados) {
|
||||||
if (!dados.movimento) {
|
if (!dados.movimento) {
|
||||||
return 'Movimento é obrigatório.';
|
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 !== '') {
|
if (dados.idbancos !== undefined && dados.idbancos !== null && dados.idbancos !== '') {
|
||||||
const idbancos = Number(dados.idbancos);
|
const idbancos = Number(dados.idbancos);
|
||||||
|
|
||||||
|
|
@ -64,6 +82,8 @@ function validarDadosMovimentoFixo(dados) {
|
||||||
|
|
||||||
async function listar(req, res) {
|
async function listar(req, res) {
|
||||||
try {
|
try {
|
||||||
|
const idUsuario = extrairIdUsuarioLogado(req);
|
||||||
|
|
||||||
const resultado = await movimentosFixosService.listarMovimentosFixos({
|
const resultado = await movimentosFixosService.listarMovimentosFixos({
|
||||||
limite: req.query.limite,
|
limite: req.query.limite,
|
||||||
offset: req.query.offset,
|
offset: req.query.offset,
|
||||||
|
|
@ -72,11 +92,13 @@ async function listar(req, res) {
|
||||||
movimento: req.query.movimento,
|
movimento: req.query.movimento,
|
||||||
idbancos: req.query.idbancos,
|
idbancos: req.query.idbancos,
|
||||||
idcentrodecustos: req.query.idcentrodecustos,
|
idcentrodecustos: req.query.idcentrodecustos,
|
||||||
|
idclientes: req.query.idclientes,
|
||||||
habilitado: req.query.habilitado,
|
habilitado: req.query.habilitado,
|
||||||
diaInicio: req.query.diaInicio,
|
diaInicio: req.query.diaInicio,
|
||||||
diaFim: req.query.diaFim,
|
diaFim: req.query.diaFim,
|
||||||
orderBy: req.query.orderBy,
|
orderBy: req.query.orderBy,
|
||||||
orderDirection: req.query.orderDirection,
|
orderDirection: req.query.orderDirection,
|
||||||
|
idUsuario,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
@ -97,6 +119,8 @@ async function listar(req, res) {
|
||||||
|
|
||||||
async function buscarPorId(req, res) {
|
async function buscarPorId(req, res) {
|
||||||
try {
|
try {
|
||||||
|
const idUsuario = extrairIdUsuarioLogado(req);
|
||||||
|
|
||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
|
|
||||||
if (!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) {
|
if (!movimentoFixo) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
@ -131,6 +155,8 @@ async function buscarPorId(req, res) {
|
||||||
|
|
||||||
async function criar(req, res) {
|
async function criar(req, res) {
|
||||||
try {
|
try {
|
||||||
|
const idUsuario = extrairIdUsuarioLogado(req);
|
||||||
|
|
||||||
const erroValidacao = validarDadosMovimentoFixo(req.body);
|
const erroValidacao = validarDadosMovimentoFixo(req.body);
|
||||||
|
|
||||||
if (erroValidacao) {
|
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({
|
return res.status(201).json({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
@ -159,6 +185,8 @@ async function criar(req, res) {
|
||||||
|
|
||||||
async function atualizar(req, res) {
|
async function atualizar(req, res) {
|
||||||
try {
|
try {
|
||||||
|
const idUsuario = extrairIdUsuarioLogado(req);
|
||||||
|
|
||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
|
|
||||||
if (!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) {
|
if (!movimentoAtualizado) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
@ -203,6 +231,8 @@ async function atualizar(req, res) {
|
||||||
|
|
||||||
async function alterarHabilitado(req, res) {
|
async function alterarHabilitado(req, res) {
|
||||||
try {
|
try {
|
||||||
|
const idUsuario = extrairIdUsuarioLogado(req);
|
||||||
|
|
||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
|
@ -222,7 +252,7 @@ async function alterarHabilitado(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const movimentoAtualizado =
|
const movimentoAtualizado =
|
||||||
await movimentosFixosService.alterarHabilitadoMovimentoFixo(id, habilitado);
|
await movimentosFixosService.alterarHabilitadoMovimentoFixo(id, habilitado, idUsuario);
|
||||||
|
|
||||||
if (!movimentoAtualizado) {
|
if (!movimentoAtualizado) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
@ -250,6 +280,8 @@ async function alterarHabilitado(req, res) {
|
||||||
|
|
||||||
async function deletar(req, res) {
|
async function deletar(req, res) {
|
||||||
try {
|
try {
|
||||||
|
const idUsuario = extrairIdUsuarioLogado(req);
|
||||||
|
|
||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
|
|
||||||
if (!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) {
|
if (!deletado) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,21 @@ const CAMPOS_MOVIMENTO_FIXO_SELECT = `
|
||||||
mf.valor,
|
mf.valor,
|
||||||
mf.dia_vencimento,
|
mf.dia_vencimento,
|
||||||
mf.idcentrodecustos,
|
mf.idcentrodecustos,
|
||||||
|
mf.idclientes,
|
||||||
mf.habilitado,
|
mf.habilitado,
|
||||||
mf.idbancos,
|
mf.idbancos,
|
||||||
mf.insert_date,
|
mf.insert_date,
|
||||||
mf.update_date,
|
mf.update_date,
|
||||||
b.descricao AS banco_descricao,
|
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 = `
|
const FROM_MOVIMENTO_FIXO_JOIN = `
|
||||||
FROM movimentosfixos mf
|
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 centrodecustos cc ON cc.idcentrodecustos = mf.idcentrodecustos
|
||||||
|
LEFT JOIN clientes c ON c.idclientes = mf.idclientes
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function normalizarTextoOuNull(valor) {
|
function normalizarTextoOuNull(valor) {
|
||||||
|
|
@ -91,12 +94,14 @@ function resolverOrdenacao(orderBy) {
|
||||||
valor: 'mf.valor',
|
valor: 'mf.valor',
|
||||||
dia_vencimento: 'mf.dia_vencimento',
|
dia_vencimento: 'mf.dia_vencimento',
|
||||||
idcentrodecustos: 'mf.idcentrodecustos',
|
idcentrodecustos: 'mf.idcentrodecustos',
|
||||||
|
idclientes: 'mf.idclientes',
|
||||||
idbancos: 'mf.idbancos',
|
idbancos: 'mf.idbancos',
|
||||||
habilitado: 'mf.habilitado',
|
habilitado: 'mf.habilitado',
|
||||||
insert_date: 'mf.insert_date',
|
insert_date: 'mf.insert_date',
|
||||||
update_date: 'mf.update_date',
|
update_date: 'mf.update_date',
|
||||||
banco_descricao: 'b.descricao',
|
banco_descricao: 'b.descricao',
|
||||||
centro_custo_descricao: 'cc.descricao',
|
centro_custo_descricao: 'cc.descricao',
|
||||||
|
cliente_nome: 'c.nome',
|
||||||
};
|
};
|
||||||
|
|
||||||
return camposPermitidos[orderBy] || 'mf.dia_vencimento';
|
return camposPermitidos[orderBy] || 'mf.dia_vencimento';
|
||||||
|
|
@ -110,6 +115,11 @@ function montarWhereMovimentosFixos(filtros = {}) {
|
||||||
const where = [];
|
const where = [];
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
|
if (filtros.idUsuario) {
|
||||||
|
where.push('b.idusuarios = ?');
|
||||||
|
params.push(Number(filtros.idUsuario));
|
||||||
|
}
|
||||||
|
|
||||||
if (filtros.movimento) {
|
if (filtros.movimento) {
|
||||||
where.push('mf.movimento = ?');
|
where.push('mf.movimento = ?');
|
||||||
params.push(filtros.movimento);
|
params.push(filtros.movimento);
|
||||||
|
|
@ -125,6 +135,11 @@ function montarWhereMovimentosFixos(filtros = {}) {
|
||||||
params.push(Number(filtros.idcentrodecustos));
|
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 !== '') {
|
if (filtros.habilitado !== undefined && filtros.habilitado !== null && filtros.habilitado !== '') {
|
||||||
where.push('mf.habilitado = ?');
|
where.push('mf.habilitado = ?');
|
||||||
params.push(Number(filtros.habilitado));
|
params.push(Number(filtros.habilitado));
|
||||||
|
|
@ -147,6 +162,7 @@ function montarWhereMovimentosFixos(filtros = {}) {
|
||||||
OR mf.movimento LIKE ?
|
OR mf.movimento LIKE ?
|
||||||
OR b.descricao LIKE ?
|
OR b.descricao LIKE ?
|
||||||
OR cc.descricao LIKE ?
|
OR cc.descricao LIKE ?
|
||||||
|
OR c.nome LIKE ?
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|
@ -156,6 +172,7 @@ function montarWhereMovimentosFixos(filtros = {}) {
|
||||||
termo,
|
termo,
|
||||||
termo,
|
termo,
|
||||||
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 = {}) {
|
async function listarMovimentosFixos(filtros = {}) {
|
||||||
const limite = limitarNumero(filtros.limite, 20, 1, 100);
|
const limite = limitarNumero(filtros.limite, 20, 1, 100);
|
||||||
const page = limitarNumero(filtros.page, 1, 1, 999999);
|
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(
|
const [rows] = await pool.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
${CAMPOS_MOVIMENTO_FIXO_SELECT}
|
${CAMPOS_MOVIMENTO_FIXO_SELECT}
|
||||||
${FROM_MOVIMENTO_FIXO_JOIN}
|
${FROM_MOVIMENTO_FIXO_JOIN}
|
||||||
WHERE mf.idmovimentosfixos = ?
|
WHERE mf.idmovimentosfixos = ?
|
||||||
|
${filtroUsuario}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`,
|
`,
|
||||||
[id]
|
params
|
||||||
);
|
);
|
||||||
|
|
||||||
return rows[0] || null;
|
return rows[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function criarMovimentoFixo(dados) {
|
async function criarMovimentoFixo(dados, idUsuario) {
|
||||||
const {
|
const {
|
||||||
movimento,
|
movimento,
|
||||||
descricao,
|
descricao,
|
||||||
valor,
|
valor,
|
||||||
dia_vencimento,
|
dia_vencimento,
|
||||||
idcentrodecustos,
|
idcentrodecustos,
|
||||||
|
idclientes,
|
||||||
habilitado,
|
habilitado,
|
||||||
idbancos,
|
idbancos,
|
||||||
} = dados;
|
} = 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(
|
const [result] = await pool.query(
|
||||||
`
|
`
|
||||||
INSERT INTO movimentosfixos (
|
INSERT INTO movimentosfixos (
|
||||||
|
|
@ -277,11 +338,12 @@ async function criarMovimentoFixo(dados) {
|
||||||
valor,
|
valor,
|
||||||
dia_vencimento,
|
dia_vencimento,
|
||||||
idcentrodecustos,
|
idcentrodecustos,
|
||||||
|
idclientes,
|
||||||
habilitado,
|
habilitado,
|
||||||
idbancos,
|
idbancos,
|
||||||
insert_date,
|
insert_date,
|
||||||
update_date
|
update_date
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
normalizarTextoOuNull(movimento),
|
normalizarTextoOuNull(movimento),
|
||||||
|
|
@ -289,16 +351,17 @@ async function criarMovimentoFixo(dados) {
|
||||||
normalizarNumero(valor, 0),
|
normalizarNumero(valor, 0),
|
||||||
normalizarNumero(dia_vencimento, 1),
|
normalizarNumero(dia_vencimento, 1),
|
||||||
normalizarNumeroOuNull(idcentrodecustos),
|
normalizarNumeroOuNull(idcentrodecustos),
|
||||||
|
normalizarNumeroOuNull(idclientes),
|
||||||
normalizarFlag(habilitado, 1),
|
normalizarFlag(habilitado, 1),
|
||||||
normalizarNumeroOuNull(idbancos),
|
idBancoNormalizado,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return buscarMovimentoFixoPorId(result.insertId);
|
return buscarMovimentoFixoPorId(result.insertId, idUsuario);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function atualizarMovimentoFixo(id, dados) {
|
async function atualizarMovimentoFixo(id, dados, idUsuario) {
|
||||||
const movimentoAtual = await buscarMovimentoFixoPorId(id);
|
const movimentoAtual = await buscarMovimentoFixoPorId(id, idUsuario);
|
||||||
|
|
||||||
if (!movimentoAtual) {
|
if (!movimentoAtual) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -310,10 +373,27 @@ async function atualizarMovimentoFixo(id, dados) {
|
||||||
valor,
|
valor,
|
||||||
dia_vencimento,
|
dia_vencimento,
|
||||||
idcentrodecustos,
|
idcentrodecustos,
|
||||||
|
idclientes,
|
||||||
habilitado,
|
habilitado,
|
||||||
idbancos,
|
idbancos,
|
||||||
} = dados;
|
} = 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(
|
await pool.query(
|
||||||
`
|
`
|
||||||
UPDATE movimentosfixos
|
UPDATE movimentosfixos
|
||||||
|
|
@ -323,6 +403,7 @@ async function atualizarMovimentoFixo(id, dados) {
|
||||||
valor = ?,
|
valor = ?,
|
||||||
dia_vencimento = ?,
|
dia_vencimento = ?,
|
||||||
idcentrodecustos = ?,
|
idcentrodecustos = ?,
|
||||||
|
idclientes = ?,
|
||||||
habilitado = ?,
|
habilitado = ?,
|
||||||
idbancos = ?,
|
idbancos = ?,
|
||||||
update_date = NOW()
|
update_date = NOW()
|
||||||
|
|
@ -334,17 +415,18 @@ async function atualizarMovimentoFixo(id, dados) {
|
||||||
normalizarNumero(valor, 0),
|
normalizarNumero(valor, 0),
|
||||||
normalizarNumero(dia_vencimento, 1),
|
normalizarNumero(dia_vencimento, 1),
|
||||||
normalizarNumeroOuNull(idcentrodecustos),
|
normalizarNumeroOuNull(idcentrodecustos),
|
||||||
|
normalizarNumeroOuNull(idclientes),
|
||||||
normalizarFlag(habilitado, 1),
|
normalizarFlag(habilitado, 1),
|
||||||
normalizarNumeroOuNull(idbancos),
|
idBancoNormalizado,
|
||||||
id,
|
id,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return buscarMovimentoFixoPorId(id);
|
return buscarMovimentoFixoPorId(id, idUsuario);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function alterarHabilitadoMovimentoFixo(id, habilitado) {
|
async function alterarHabilitadoMovimentoFixo(id, habilitado, idUsuario) {
|
||||||
const movimentoAtual = await buscarMovimentoFixoPorId(id);
|
const movimentoAtual = await buscarMovimentoFixoPorId(id, idUsuario);
|
||||||
|
|
||||||
if (!movimentoAtual) {
|
if (!movimentoAtual) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -364,11 +446,11 @@ async function alterarHabilitadoMovimentoFixo(id, habilitado) {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return buscarMovimentoFixoPorId(id);
|
return buscarMovimentoFixoPorId(id, idUsuario);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deletarMovimentoFixo(id) {
|
async function deletarMovimentoFixo(id, idUsuario) {
|
||||||
const movimentoAtual = await buscarMovimentoFixoPorId(id);
|
const movimentoAtual = await buscarMovimentoFixoPorId(id, idUsuario);
|
||||||
|
|
||||||
if (!movimentoAtual) {
|
if (!movimentoAtual) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ async function buscarFixosDoUsuario(connection, idUsuario) {
|
||||||
mf.valor,
|
mf.valor,
|
||||||
mf.dia_vencimento,
|
mf.dia_vencimento,
|
||||||
mf.idcentrodecustos,
|
mf.idcentrodecustos,
|
||||||
|
mf.idclientes,
|
||||||
mf.idbancos
|
mf.idbancos
|
||||||
FROM movimentosfixos mf
|
FROM movimentosfixos mf
|
||||||
INNER JOIN bancos b ON b.idbancos = mf.idbancos
|
INNER JOIN bancos b ON b.idbancos = mf.idbancos
|
||||||
|
|
@ -132,7 +133,7 @@ async function jaExisteMovimentoFixo(connection, fixo, competencia) {
|
||||||
const [rowsGeracao] = await connection.query(
|
const [rowsGeracao] = await connection.query(
|
||||||
`
|
`
|
||||||
SELECT idmovimentosfixosgeracoes, idcontasapagar, status
|
SELECT idmovimentosfixosgeracoes, idcontasapagar, status
|
||||||
FROM movimentosfixosgeracoes
|
FROM movimentosfixos_geracoes
|
||||||
WHERE idmovimentosfixos = ?
|
WHERE idmovimentosfixos = ?
|
||||||
AND competencia = ?
|
AND competencia = ?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
|
|
@ -161,7 +162,7 @@ async function jaExisteMovimentoFixo(connection, fixo, competencia) {
|
||||||
if (rowsMovimentoVinculado.length > 0) {
|
if (rowsMovimentoVinculado.length > 0) {
|
||||||
await connection.query(
|
await connection.query(
|
||||||
`
|
`
|
||||||
INSERT INTO movimentosfixosgeracoes (
|
INSERT INTO movimentosfixos_geracoes (
|
||||||
idmovimentosfixos,
|
idmovimentosfixos,
|
||||||
competencia,
|
competencia,
|
||||||
idcontasapagar,
|
idcontasapagar,
|
||||||
|
|
@ -230,7 +231,7 @@ async function jaExisteMovimentoFixo(connection, fixo, competencia) {
|
||||||
|
|
||||||
await connection.query(
|
await connection.query(
|
||||||
`
|
`
|
||||||
INSERT INTO movimentosfixosgeracoes (
|
INSERT INTO movimentosfixos_geracoes (
|
||||||
idmovimentosfixos,
|
idmovimentosfixos,
|
||||||
competencia,
|
competencia,
|
||||||
idcontasapagar,
|
idcontasapagar,
|
||||||
|
|
@ -288,7 +289,7 @@ async function inserirMovimentoFixoGerado(connection, fixo, competencia, idUsuar
|
||||||
insert_date,
|
insert_date,
|
||||||
update_date,
|
update_date,
|
||||||
deleted_at
|
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,
|
fixo.movimento,
|
||||||
|
|
@ -302,6 +303,7 @@ async function inserirMovimentoFixoGerado(connection, fixo, competencia, idUsuar
|
||||||
fixo.idbancos || null,
|
fixo.idbancos || null,
|
||||||
idUsuario,
|
idUsuario,
|
||||||
dataBaixa ? idUsuario : null,
|
dataBaixa ? idUsuario : null,
|
||||||
|
fixo.idclientes || null,
|
||||||
fixo.idmovimentosfixos,
|
fixo.idmovimentosfixos,
|
||||||
competencia,
|
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) {
|
async function listarCentrosCusto(req, res) {
|
||||||
try {
|
try {
|
||||||
const centrosCusto = await referenciasService.listarCentrosCusto();
|
const centrosCusto = await referenciasService.listarCentrosCusto();
|
||||||
|
|
@ -56,6 +74,7 @@ async function listarClientes(req, res) {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
listarBancos,
|
listarBancos,
|
||||||
|
listarMeusBancos,
|
||||||
listarCentrosCusto,
|
listarCentrosCusto,
|
||||||
listarClientes,
|
listarClientes,
|
||||||
};
|
};
|
||||||
|
|
@ -7,6 +7,8 @@ const router = express.Router();
|
||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|
||||||
router.get('/bancos', referenciasController.listarBancos);
|
router.get('/bancos', referenciasController.listarBancos);
|
||||||
|
router.get('/bancos/meus', referenciasController.listarMeusBancos);
|
||||||
|
|
||||||
router.get('/centros-custo', referenciasController.listarCentrosCusto);
|
router.get('/centros-custo', referenciasController.listarCentrosCusto);
|
||||||
router.get('/clientes', referenciasController.listarClientes);
|
router.get('/clientes', referenciasController.listarClientes);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
const pool = require('../../../database/mysql');
|
const pool = require('../../../database/mysql');
|
||||||
|
|
||||||
|
function extrairIdUsuarioLogado(usuario) {
|
||||||
|
return (
|
||||||
|
usuario?.idusuarios ||
|
||||||
|
usuario?.idusuario ||
|
||||||
|
usuario?.id ||
|
||||||
|
usuario?.userId ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function listarBancos() {
|
async function listarBancos() {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`
|
`
|
||||||
|
|
@ -23,14 +33,46 @@ async function listarBancos() {
|
||||||
ON u.idusuarios = b.idusuarios
|
ON u.idusuarios = b.idusuarios
|
||||||
WHERE b.habilitado = 1
|
WHERE b.habilitado = 1
|
||||||
ORDER BY
|
ORDER BY
|
||||||
b.descricao ASC,
|
COALESCE(u.nome, 'Usuário sem nome') ASC,
|
||||||
u.nome ASC
|
b.descricao ASC
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
return rows;
|
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() {
|
async function listarCentrosCusto() {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`
|
`
|
||||||
|
|
@ -72,6 +114,7 @@ async function listarClientes() {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
listarBancos,
|
listarBancos,
|
||||||
|
listarMeusBancos,
|
||||||
listarCentrosCusto,
|
listarCentrosCusto,
|
||||||
listarClientes,
|
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 { NovoUsuarioPage } from '../features/usuarios/pages/NovoUsuarioPage';
|
||||||
import { EditarUsuarioPage } from '../features/usuarios/pages/EditarUsuarioPage';
|
import { EditarUsuarioPage } from '../features/usuarios/pages/EditarUsuarioPage';
|
||||||
import { QuitacoesCreditoPage } from '../features/quitacoesCredito/pages/QuitacoesCreditoPage';
|
import { QuitacoesCreditoPage } from '../features/quitacoesCredito/pages/QuitacoesCreditoPage';
|
||||||
|
import { SimulacaoMensalPage } from '../features/simulacaoMensal/pages/SimulacaoMensalPage';
|
||||||
|
|
||||||
export const routes: RouteObject[] = [
|
export const routes: RouteObject[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -129,6 +130,10 @@ export const routes: RouteObject[] = [
|
||||||
{
|
{
|
||||||
path: '/quitacoes-credito',
|
path: '/quitacoes-credito',
|
||||||
element: <QuitacoesCreditoPage />,
|
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 EventRepeatIcon from '@mui/icons-material/EventRepeat';
|
||||||
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
|
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
|
||||||
import CreditCardIcon from '@mui/icons-material/CreditCard';
|
import CreditCardIcon from '@mui/icons-material/CreditCard';
|
||||||
|
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
type SidebarProps = {
|
type SidebarProps = {
|
||||||
|
|
@ -75,6 +76,11 @@ const menuItems = [
|
||||||
path: '/relatorios',
|
path: '/relatorios',
|
||||||
icon: <AssessmentIcon />,
|
icon: <AssessmentIcon />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Simulação mensal',
|
||||||
|
path: '/simulacao-mensal',
|
||||||
|
icon: <AutoAwesomeIcon />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Configurações',
|
label: 'Configurações',
|
||||||
path: '/configuracoes',
|
path: '/configuracoes',
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import {
|
||||||
listarBancos,
|
listarBancos,
|
||||||
listarCentrosCusto,
|
listarCentrosCusto,
|
||||||
listarClientes,
|
listarClientes,
|
||||||
|
listarMeusBancos,
|
||||||
} from '../../referencias/services/referenciasService';
|
} from '../../referencias/services/referenciasService';
|
||||||
import type {
|
import type {
|
||||||
Banco,
|
Banco,
|
||||||
|
|
@ -118,6 +119,68 @@ function formatarValorMoeda(valor: number) {
|
||||||
}).format(Number(valor || 0));
|
}).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) {
|
function FormSection({ title, description, children }: FormSectionProps) {
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
|
|
@ -174,7 +237,8 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
|
|
||||||
const isEdit = mode === 'edit';
|
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 [centrosCusto, setCentrosCusto] = useState<CentroCusto[]>([]);
|
||||||
const [clientes, setClientes] = useState<Cliente[]>([]);
|
const [clientes, setClientes] = useState<Cliente[]>([]);
|
||||||
|
|
||||||
|
|
@ -215,8 +279,8 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
const bancoSelecionado = useMemo(() => {
|
const bancoSelecionado = useMemo(() => {
|
||||||
if (!idBanco) return null;
|
if (!idBanco) return null;
|
||||||
|
|
||||||
return bancos.find((banco) => String(banco.id) === String(idBanco)) || null;
|
return meusBancos.find((banco) => String(banco.id) === String(idBanco)) || null;
|
||||||
}, [bancos, idBanco]);
|
}, [meusBancos, idBanco]);
|
||||||
|
|
||||||
const statusDisponiveis = useMemo<MovimentoStatus[]>(() => {
|
const statusDisponiveis = useMemo<MovimentoStatus[]>(() => {
|
||||||
if (movimento === 'Entrada') return ['Recebido', 'A receber'];
|
if (movimento === 'Entrada') return ['Recebido', 'A receber'];
|
||||||
|
|
@ -261,6 +325,11 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
if (movimento === 'Sangria') {
|
if (movimento === 'Sangria') {
|
||||||
setReferenciaTipo('banco');
|
setReferenciaTipo('banco');
|
||||||
setStatus('Pago');
|
setStatus('Pago');
|
||||||
|
setDataBaixa((dataAtual) => dataAtual || hojeISO());
|
||||||
|
setParcelas('1');
|
||||||
|
setParcela('1');
|
||||||
|
setGerarParcelas(false);
|
||||||
|
setIdCentroCusto('19');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,13 +345,15 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
setLoadingRefs(true);
|
setLoadingRefs(true);
|
||||||
setErro('');
|
setErro('');
|
||||||
|
|
||||||
const [bancosData, centrosData, clientesData] = await Promise.all([
|
const [meusBancosData, todosBancosData, centrosData, clientesData] = await Promise.all([
|
||||||
|
listarMeusBancos(),
|
||||||
listarBancos(),
|
listarBancos(),
|
||||||
listarCentrosCusto(),
|
listarCentrosCusto(),
|
||||||
listarClientes(),
|
listarClientes(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setBancos(bancosData);
|
setMeusBancos(meusBancosData);
|
||||||
|
setTodosBancos(todosBancosData);
|
||||||
setCentrosCusto(centrosData);
|
setCentrosCusto(centrosData);
|
||||||
setClientes(clientesData);
|
setClientes(clientesData);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -494,22 +565,33 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function montarPayload(confirmarDuplicado = false): CriarMovimentoRequest {
|
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 {
|
return {
|
||||||
movimento,
|
movimento,
|
||||||
descricao: descricao.trim(),
|
descricao: descricao.trim(),
|
||||||
valor: Number(valor),
|
valor: Number(valor),
|
||||||
parcela: Number(parcela || 1),
|
parcela: parcelaFinal,
|
||||||
parcelas: Number(parcelas || 1),
|
parcelas: parcelasFinal,
|
||||||
status,
|
status: statusFinal,
|
||||||
dataentrada: dataEntrada,
|
dataentrada: dataEntrada,
|
||||||
datavencimento: dataVencimento || null,
|
datavencimento: dataVencimento || null,
|
||||||
databaixa: status === 'Pago' || status === 'Recebido' ? dataBaixa || null : null,
|
databaixa: dataBaixaFinal,
|
||||||
idcentrodecustos: toNumberOrNull(idCentroCusto),
|
idcentrodecustos: toNumberOrNull(idCentroCusto),
|
||||||
idbancos: toNumberOrNull(idBanco),
|
idbancos: toNumberOrNull(idBanco),
|
||||||
idusuarios_baixa: null,
|
idusuarios_baixa: null,
|
||||||
idclientes: referenciaTipo === 'cliente' ? toNumberOrNull(idCliente) : null,
|
idclientes: referenciaTipoFinal === 'cliente' ? toNumberOrNull(idCliente) : null,
|
||||||
idveiculosdetalhes: null,
|
idveiculosdetalhes: null,
|
||||||
idbancos_p: referenciaTipo === 'banco' ? toNumberOrNull(idBancoReferencia) : null,
|
idbancos_p: referenciaTipoFinal === 'banco' ? toNumberOrNull(idBancoReferencia) : null,
|
||||||
|
|
||||||
idmovimentosfixos: null,
|
idmovimentosfixos: null,
|
||||||
competencia: extrairCompetencia(dataVencimento || dataEntrada),
|
competencia: extrairCompetencia(dataVencimento || dataEntrada),
|
||||||
|
|
@ -519,8 +601,12 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
|
|
||||||
observacao: observacao.trim() || null,
|
observacao: observacao.trim() || null,
|
||||||
|
|
||||||
gerarParcelas: temParcelamento ? gerarParcelas : false,
|
gerarParcelas: isSangria ? false : temParcelamento ? gerarParcelas : false,
|
||||||
modoParcelamento: temParcelamento ? modoParcelamento : 'valor_parcela',
|
modoParcelamento: isSangria
|
||||||
|
? 'valor_parcela'
|
||||||
|
: temParcelamento
|
||||||
|
? modoParcelamento
|
||||||
|
: 'valor_parcela',
|
||||||
confirmarDuplicado,
|
confirmarDuplicado,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -645,6 +731,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
label="Situação"
|
label="Situação"
|
||||||
value={status}
|
value={status}
|
||||||
onChange={(event) => setStatus(event.target.value as MovimentoStatus)}
|
onChange={(event) => setStatus(event.target.value as MovimentoStatus)}
|
||||||
|
disabled={movimento === 'Sangria'}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={fieldSx}
|
sx={fieldSx}
|
||||||
>
|
>
|
||||||
|
|
@ -685,7 +772,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
sx={fieldSx}
|
sx={fieldSx}
|
||||||
>
|
>
|
||||||
<MenuItem value="">Nenhum</MenuItem>
|
<MenuItem value="">Nenhum</MenuItem>
|
||||||
{bancos.map((banco) => (
|
{meusBancos.map((banco) => (
|
||||||
<MenuItem key={banco.id} value={String(banco.id)}>
|
<MenuItem key={banco.id} value={String(banco.id)}>
|
||||||
{banco.descricao}
|
{banco.descricao}
|
||||||
{Number(banco.debito) === 0 ? ' · Crédito' : ''}
|
{Number(banco.debito) === 0 ? ' · Crédito' : ''}
|
||||||
|
|
@ -698,25 +785,35 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
title="Parcelas e datas"
|
title="Parcelas e datas"
|
||||||
description="Controle vencimento, baixa e parcelamento."
|
description="Controle vencimento, baixa e parcelamento."
|
||||||
>
|
>
|
||||||
<TextField
|
{movimento !== 'Sangria' && (
|
||||||
label="Parcela"
|
<>
|
||||||
type="number"
|
<TextField
|
||||||
value={parcela}
|
label="Parcela"
|
||||||
onChange={(event) => setParcela(event.target.value)}
|
type="number"
|
||||||
inputProps={{ min: '1' }}
|
value={parcela}
|
||||||
fullWidth
|
onChange={(event) => setParcela(event.target.value)}
|
||||||
sx={fieldSx}
|
inputProps={{ min: '1' }}
|
||||||
/>
|
fullWidth
|
||||||
|
sx={fieldSx}
|
||||||
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Total de parcelas"
|
label="Total de parcelas"
|
||||||
type="number"
|
type="number"
|
||||||
value={parcelas}
|
value={parcelas}
|
||||||
onChange={(event) => setParcelas(event.target.value)}
|
onChange={(event) => setParcelas(event.target.value)}
|
||||||
inputProps={{ min: '1' }}
|
inputProps={{ min: '1' }}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={fieldSx}
|
sx={fieldSx}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{temParcelamento && (
|
||||||
|
<Box sx={{ gridColumn: { xs: 'auto', md: '1 / -1' } }}>
|
||||||
|
...
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{temParcelamento && (
|
{temParcelamento && (
|
||||||
<Box sx={{ gridColumn: { xs: 'auto', md: '1 / -1' } }}>
|
<Box sx={{ gridColumn: { xs: 'auto', md: '1 / -1' } }}>
|
||||||
|
|
@ -815,6 +912,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setReferenciaTipo(event.target.value as 'nenhum' | 'cliente' | 'banco')
|
setReferenciaTipo(event.target.value as 'nenhum' | 'cliente' | 'banco')
|
||||||
}
|
}
|
||||||
|
disabled={movimento === 'Sangria'}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={fieldSx}
|
sx={fieldSx}
|
||||||
>
|
>
|
||||||
|
|
@ -853,7 +951,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
sx={fieldSx}
|
sx={fieldSx}
|
||||||
>
|
>
|
||||||
<MenuItem value="">Selecione</MenuItem>
|
<MenuItem value="">Selecione</MenuItem>
|
||||||
{bancos.map((banco) => (
|
{todosBancos.map((banco) => (
|
||||||
<MenuItem key={banco.id} value={String(banco.id)}>
|
<MenuItem key={banco.id} value={String(banco.id)}>
|
||||||
{banco.descricao}
|
{banco.descricao}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
@ -980,7 +1078,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
Impacto previsto no saldo
|
Impacto previsto no saldo dos bancos
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{loadingImpacto ? (
|
{loadingImpacto ? (
|
||||||
|
|
@ -990,40 +1088,88 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
||||||
</Box>
|
</Box>
|
||||||
) : impactoSaldo.length === 0 ? (
|
) : impactoSaldo.length === 0 ? (
|
||||||
<Typography fontWeight={700} marginTop={0.5}>
|
<Typography fontWeight={700} marginTop={0.5}>
|
||||||
Sem impacto imediato
|
Sem alteração imediata de saldo
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Stack spacing={1} marginTop={1}>
|
<Stack spacing={1} marginTop={1}>
|
||||||
{impactoSaldo.map((impacto, index) => (
|
{impactoSaldo.map((impacto, index) => {
|
||||||
<Box
|
const visual = getImpactoVisual(impacto);
|
||||||
key={`${impacto.idbancos}-${index}`}
|
const percentualFormatado = formatarPercentual(impacto.percentual_delta);
|
||||||
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>
|
|
||||||
|
|
||||||
<Typography
|
return (
|
||||||
variant="body2"
|
<Box
|
||||||
fontWeight={900}
|
key={`${impacto.idbancos}-${index}`}
|
||||||
color={impacto.valor_delta >= 0 ? 'success.main' : 'error.main'}
|
sx={{
|
||||||
|
padding: 1.5,
|
||||||
|
borderRadius: 2.5,
|
||||||
|
backgroundColor: visual.backgroundColor,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: visual.borderColor,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{impacto.valor_delta >= 0 ? '+' : ''}
|
<Stack
|
||||||
{formatarValorMoeda(impacto.valor_delta)}
|
direction="row"
|
||||||
</Typography>
|
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 && (
|
{impacto.descricao && (
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{formatarValorMoeda(impacto.saldo_anterior)} →{' '}
|
{impacto.descricao}
|
||||||
{formatarValorMoeda(impacto.saldo_posterior || 0)}
|
</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>
|
</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>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -77,10 +77,19 @@ export type AtualizarMovimentoRequest = CriarMovimentoRequest;
|
||||||
|
|
||||||
export type MovimentoImpactoSaldo = {
|
export type MovimentoImpactoSaldo = {
|
||||||
idbancos: number;
|
idbancos: number;
|
||||||
|
|
||||||
banco_descricao?: string | null;
|
banco_descricao?: string | null;
|
||||||
|
banco_nome?: string | null;
|
||||||
|
usuario_nome?: string | null;
|
||||||
|
|
||||||
valor_delta: number;
|
valor_delta: number;
|
||||||
saldo_anterior?: number;
|
saldo_anterior?: number;
|
||||||
saldo_posterior?: number;
|
saldo_posterior?: number;
|
||||||
|
|
||||||
|
percentual_delta?: number | null;
|
||||||
|
direcao?: 'alta' | 'queda' | 'neutro';
|
||||||
|
tipo_visual?: 'positivo' | 'negativo' | 'neutro';
|
||||||
|
|
||||||
descricao?: string | null;
|
descricao?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,14 @@ import EventRepeatIcon from '@mui/icons-material/EventRepeat';
|
||||||
import SaveIcon from '@mui/icons-material/Save';
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
listarBancos,
|
listarMeusBancos,
|
||||||
listarCentrosCusto,
|
listarCentrosCusto,
|
||||||
|
listarClientes,
|
||||||
} from '../../referencias/services/referenciasService';
|
} from '../../referencias/services/referenciasService';
|
||||||
import type {
|
import type {
|
||||||
Banco,
|
Banco,
|
||||||
CentroCusto,
|
CentroCusto,
|
||||||
|
Cliente,
|
||||||
} from '../../referencias/types/referenciasTypes';
|
} from '../../referencias/types/referenciasTypes';
|
||||||
import {
|
import {
|
||||||
atualizarMovimentoFixo,
|
atualizarMovimentoFixo,
|
||||||
|
|
@ -125,6 +127,7 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
|
||||||
|
|
||||||
const [bancos, setBancos] = useState<Banco[]>([]);
|
const [bancos, setBancos] = useState<Banco[]>([]);
|
||||||
const [centrosCusto, setCentrosCusto] = useState<CentroCusto[]>([]);
|
const [centrosCusto, setCentrosCusto] = useState<CentroCusto[]>([]);
|
||||||
|
const [clientes, setClientes] = useState<Cliente[]>([]);
|
||||||
|
|
||||||
const [loadingRefs, setLoadingRefs] = useState(true);
|
const [loadingRefs, setLoadingRefs] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
@ -136,6 +139,7 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
|
||||||
const [valor, setValor] = useState('');
|
const [valor, setValor] = useState('');
|
||||||
const [diaVencimento, setDiaVencimento] = useState('1');
|
const [diaVencimento, setDiaVencimento] = useState('1');
|
||||||
const [idCentroCusto, setIdCentroCusto] = useState('');
|
const [idCentroCusto, setIdCentroCusto] = useState('');
|
||||||
|
const [idCliente, setIdCliente] = useState('');
|
||||||
const [idBanco, setIdBanco] = useState('');
|
const [idBanco, setIdBanco] = useState('');
|
||||||
const [habilitado, setHabilitado] = useState('1');
|
const [habilitado, setHabilitado] = useState('1');
|
||||||
|
|
||||||
|
|
@ -151,6 +155,12 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
|
||||||
return 'Estorno recorrente';
|
return 'Estorno recorrente';
|
||||||
}, [movimento]);
|
}, [movimento]);
|
||||||
|
|
||||||
|
const clienteSelecionado = useMemo(() => {
|
||||||
|
if (!idCliente) return null;
|
||||||
|
|
||||||
|
return clientes.find((cliente) => String(cliente.id) === String(idCliente)) || null;
|
||||||
|
}, [clientes, idCliente]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialData) return;
|
if (!initialData) return;
|
||||||
|
|
||||||
|
|
@ -159,6 +169,7 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
|
||||||
setValor(String(initialData.valor || ''));
|
setValor(String(initialData.valor || ''));
|
||||||
setDiaVencimento(String(initialData.dia_vencimento || 1));
|
setDiaVencimento(String(initialData.dia_vencimento || 1));
|
||||||
setIdCentroCusto(initialData.idcentrodecustos ? String(initialData.idcentrodecustos) : '');
|
setIdCentroCusto(initialData.idcentrodecustos ? String(initialData.idcentrodecustos) : '');
|
||||||
|
setIdCliente(initialData.idclientes ? String(initialData.idclientes) : '');
|
||||||
setIdBanco(initialData.idbancos ? String(initialData.idbancos) : '');
|
setIdBanco(initialData.idbancos ? String(initialData.idbancos) : '');
|
||||||
setHabilitado(String(initialData.habilitado ?? 1));
|
setHabilitado(String(initialData.habilitado ?? 1));
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
@ -169,13 +180,15 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
|
||||||
setLoadingRefs(true);
|
setLoadingRefs(true);
|
||||||
setErro('');
|
setErro('');
|
||||||
|
|
||||||
const [bancosData, centrosData] = await Promise.all([
|
const [bancosData, centrosData, clientesData] = await Promise.all([
|
||||||
listarBancos(),
|
listarMeusBancos(),
|
||||||
listarCentrosCusto(),
|
listarCentrosCusto(),
|
||||||
|
listarClientes(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setBancos(bancosData);
|
setBancos(bancosData);
|
||||||
setCentrosCusto(centrosData);
|
setCentrosCusto(centrosData);
|
||||||
|
setClientes(clientesData);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message =
|
const message =
|
||||||
error?.response?.data?.message ||
|
error?.response?.data?.message ||
|
||||||
|
|
@ -196,6 +209,7 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
|
||||||
setValor('');
|
setValor('');
|
||||||
setDiaVencimento('1');
|
setDiaVencimento('1');
|
||||||
setIdCentroCusto('');
|
setIdCentroCusto('');
|
||||||
|
setIdCliente('');
|
||||||
setIdBanco('');
|
setIdBanco('');
|
||||||
setHabilitado('1');
|
setHabilitado('1');
|
||||||
}
|
}
|
||||||
|
|
@ -231,6 +245,7 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
|
||||||
valor: Number(valor),
|
valor: Number(valor),
|
||||||
dia_vencimento: dia,
|
dia_vencimento: dia,
|
||||||
idcentrodecustos: toNumberOrNull(idCentroCusto),
|
idcentrodecustos: toNumberOrNull(idCentroCusto),
|
||||||
|
idclientes: toNumberOrNull(idCliente),
|
||||||
habilitado: Number(habilitado || 1),
|
habilitado: Number(habilitado || 1),
|
||||||
idbancos: toNumberOrNull(idBanco),
|
idbancos: toNumberOrNull(idBanco),
|
||||||
};
|
};
|
||||||
|
|
@ -400,7 +415,7 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
|
||||||
|
|
||||||
<FormSection
|
<FormSection
|
||||||
title="Classificação"
|
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
|
<TextField
|
||||||
select
|
select
|
||||||
|
|
@ -433,6 +448,22 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</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>
|
</FormSection>
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
|
|
@ -559,6 +590,15 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
|
||||||
{Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado'}
|
{Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Cliente
|
||||||
|
</Typography>
|
||||||
|
<Typography fontWeight={800}>
|
||||||
|
{clienteSelecionado?.nome || 'Nenhum'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,13 @@ import type {
|
||||||
import {
|
import {
|
||||||
listarBancos,
|
listarBancos,
|
||||||
listarCentrosCusto,
|
listarCentrosCusto,
|
||||||
|
listarClientes,
|
||||||
} from '../../referencias/services/referenciasService';
|
} from '../../referencias/services/referenciasService';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Banco,
|
Banco,
|
||||||
CentroCusto,
|
CentroCusto,
|
||||||
|
Cliente,
|
||||||
} from '../../referencias/types/referenciasTypes';
|
} from '../../referencias/types/referenciasTypes';
|
||||||
|
|
||||||
const LIMITE_PADRAO = 20;
|
const LIMITE_PADRAO = 20;
|
||||||
|
|
@ -87,7 +90,6 @@ function statusColor(habilitado: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MovimentosFixosPage() {
|
export function MovimentosFixosPage() {
|
||||||
const [movimentosFixos, setMovimentosFixos] = useState<MovimentoFixo[]>([]);
|
|
||||||
const [pagination, setPagination] = useState<MovimentosFixosPagination>({
|
const [pagination, setPagination] = useState<MovimentosFixosPagination>({
|
||||||
total: 0,
|
total: 0,
|
||||||
limite: LIMITE_PADRAO,
|
limite: LIMITE_PADRAO,
|
||||||
|
|
@ -108,6 +110,8 @@ export function MovimentosFixosPage() {
|
||||||
|
|
||||||
const [bancos, setBancos] = useState<Banco[]>([]);
|
const [bancos, setBancos] = useState<Banco[]>([]);
|
||||||
const [centrosCusto, setCentrosCusto] = useState<CentroCusto[]>([]);
|
const [centrosCusto, setCentrosCusto] = useState<CentroCusto[]>([]);
|
||||||
|
const [movimentosFixos, setMovimentosFixos] = useState<MovimentoFixo[]>([]);
|
||||||
|
const [clientes, setClientes] = useState<Cliente[]>([]);
|
||||||
|
|
||||||
const [alterandoStatusId, setAlterandoStatusId] = useState<number | null>(null);
|
const [alterandoStatusId, setAlterandoStatusId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
|
@ -121,6 +125,7 @@ export function MovimentosFixosPage() {
|
||||||
const [movimento, setMovimento] = useState<MovimentoFixoTipo | ''>('');
|
const [movimento, setMovimento] = useState<MovimentoFixoTipo | ''>('');
|
||||||
const [idBanco, setIdBanco] = useState<number | ''>('');
|
const [idBanco, setIdBanco] = useState<number | ''>('');
|
||||||
const [idCentroCusto, setIdCentroCusto] = useState<number | ''>('');
|
const [idCentroCusto, setIdCentroCusto] = useState<number | ''>('');
|
||||||
|
const [idCliente, setIdCliente] = useState<number | ''>('');
|
||||||
const [habilitado, setHabilitado] = useState<number | ''>('');
|
const [habilitado, setHabilitado] = useState<number | ''>('');
|
||||||
const [diaInicio, setDiaInicio] = useState<number | ''>('');
|
const [diaInicio, setDiaInicio] = useState<number | ''>('');
|
||||||
const [diaFim, setDiaFim] = useState<number | ''>('');
|
const [diaFim, setDiaFim] = useState<number | ''>('');
|
||||||
|
|
@ -137,6 +142,7 @@ export function MovimentosFixosPage() {
|
||||||
if (movimento) count += 1;
|
if (movimento) count += 1;
|
||||||
if (idBanco) count += 1;
|
if (idBanco) count += 1;
|
||||||
if (idCentroCusto) count += 1;
|
if (idCentroCusto) count += 1;
|
||||||
|
if (idCliente) count += 1;
|
||||||
if (habilitado !== '') count += 1;
|
if (habilitado !== '') count += 1;
|
||||||
if (diaInicio !== '') count += 1;
|
if (diaInicio !== '') count += 1;
|
||||||
if (diaFim !== '') count += 1;
|
if (diaFim !== '') count += 1;
|
||||||
|
|
@ -147,6 +153,7 @@ export function MovimentosFixosPage() {
|
||||||
movimento,
|
movimento,
|
||||||
idBanco,
|
idBanco,
|
||||||
idCentroCusto,
|
idCentroCusto,
|
||||||
|
idCliente,
|
||||||
habilitado,
|
habilitado,
|
||||||
diaInicio,
|
diaInicio,
|
||||||
diaFim,
|
diaFim,
|
||||||
|
|
@ -156,13 +163,15 @@ export function MovimentosFixosPage() {
|
||||||
try {
|
try {
|
||||||
setLoadingRefs(true);
|
setLoadingRefs(true);
|
||||||
|
|
||||||
const [bancosData, centrosData] = await Promise.all([
|
const [bancosData, centrosData, clientesData] = await Promise.all([
|
||||||
listarBancos(),
|
listarBancos(),
|
||||||
listarCentrosCusto(),
|
listarCentrosCusto(),
|
||||||
|
listarClientes(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setBancos(bancosData);
|
setBancos(bancosData);
|
||||||
setCentrosCusto(centrosData);
|
setCentrosCusto(centrosData);
|
||||||
|
setClientes(clientesData);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingRefs(false);
|
setLoadingRefs(false);
|
||||||
}
|
}
|
||||||
|
|
@ -180,6 +189,7 @@ export function MovimentosFixosPage() {
|
||||||
movimento: movimento || undefined,
|
movimento: movimento || undefined,
|
||||||
idbancos: idBanco || undefined,
|
idbancos: idBanco || undefined,
|
||||||
idcentrodecustos: idCentroCusto || undefined,
|
idcentrodecustos: idCentroCusto || undefined,
|
||||||
|
idclientes: idCliente || undefined,
|
||||||
habilitado,
|
habilitado,
|
||||||
diaInicio,
|
diaInicio,
|
||||||
diaFim,
|
diaFim,
|
||||||
|
|
@ -214,6 +224,7 @@ export function MovimentosFixosPage() {
|
||||||
setMovimento('');
|
setMovimento('');
|
||||||
setIdBanco('');
|
setIdBanco('');
|
||||||
setIdCentroCusto('');
|
setIdCentroCusto('');
|
||||||
|
setIdCliente('');
|
||||||
setHabilitado('');
|
setHabilitado('');
|
||||||
setDiaInicio('');
|
setDiaInicio('');
|
||||||
setDiaFim('');
|
setDiaFim('');
|
||||||
|
|
@ -492,6 +503,24 @@ export function MovimentosFixosPage() {
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</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
|
<TextField
|
||||||
select
|
select
|
||||||
label="Status"
|
label="Status"
|
||||||
|
|
@ -541,6 +570,7 @@ export function MovimentosFixosPage() {
|
||||||
<MenuItem value="movimento">Movimento</MenuItem>
|
<MenuItem value="movimento">Movimento</MenuItem>
|
||||||
<MenuItem value="banco_descricao">Banco</MenuItem>
|
<MenuItem value="banco_descricao">Banco</MenuItem>
|
||||||
<MenuItem value="centro_custo_descricao">Centro</MenuItem>
|
<MenuItem value="centro_custo_descricao">Centro</MenuItem>
|
||||||
|
<MenuItem value="cliente_nome">Cliente</MenuItem>
|
||||||
<MenuItem value="habilitado">Status</MenuItem>
|
<MenuItem value="habilitado">Status</MenuItem>
|
||||||
<MenuItem value="insert_date">Cadastro</MenuItem>
|
<MenuItem value="insert_date">Cadastro</MenuItem>
|
||||||
<MenuItem value="update_date">Atualização</MenuItem>
|
<MenuItem value="update_date">Atualização</MenuItem>
|
||||||
|
|
@ -667,6 +697,10 @@ export function MovimentosFixosPage() {
|
||||||
Centro: <strong>{item.centro_custo_descricao || '-'}</strong>
|
Centro: <strong>{item.centro_custo_descricao || '-'}</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Cliente: <strong>{item.cliente_nome || '-'}</strong>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
justifyContent="flex-end"
|
justifyContent="flex-end"
|
||||||
|
|
@ -721,7 +755,7 @@ export function MovimentosFixosPage() {
|
||||||
<Table
|
<Table
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 1120,
|
minWidth: 1280,
|
||||||
'& th': {
|
'& th': {
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
|
|
@ -739,6 +773,7 @@ export function MovimentosFixosPage() {
|
||||||
<TableCell sx={{ minWidth: 130 }} align="right">Valor</TableCell>
|
<TableCell sx={{ minWidth: 130 }} align="right">Valor</TableCell>
|
||||||
<TableCell sx={{ minWidth: 110 }} align="center">Dia venc.</TableCell>
|
<TableCell sx={{ minWidth: 110 }} align="center">Dia venc.</TableCell>
|
||||||
<TableCell sx={{ minWidth: 180 }}>Centro</TableCell>
|
<TableCell sx={{ minWidth: 180 }}>Centro</TableCell>
|
||||||
|
<TableCell sx={{ minWidth: 180 }}>Cliente</TableCell>
|
||||||
<TableCell sx={{ minWidth: 180 }}>Banco</TableCell>
|
<TableCell sx={{ minWidth: 180 }}>Banco</TableCell>
|
||||||
<TableCell sx={{ minWidth: 130 }}>Status</TableCell>
|
<TableCell sx={{ minWidth: 130 }}>Status</TableCell>
|
||||||
<TableCell sx={{ minWidth: 130 }}>Cadastro</TableCell>
|
<TableCell sx={{ minWidth: 130 }}>Cadastro</TableCell>
|
||||||
|
|
@ -801,6 +836,16 @@ export function MovimentosFixosPage() {
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
noWrap
|
||||||
|
title={item.cliente_nome || '-'}
|
||||||
|
>
|
||||||
|
{item.cliente_nome || '-'}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export type ListarMovimentosFixosParams = {
|
||||||
movimento?: string;
|
movimento?: string;
|
||||||
idbancos?: number | '';
|
idbancos?: number | '';
|
||||||
idcentrodecustos?: number | '';
|
idcentrodecustos?: number | '';
|
||||||
|
idclientes?: number | '';
|
||||||
habilitado?: number | '';
|
habilitado?: number | '';
|
||||||
diaInicio?: number | '';
|
diaInicio?: number | '';
|
||||||
diaFim?: number | '';
|
diaFim?: number | '';
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export type MovimentoFixo = {
|
||||||
valor: number;
|
valor: number;
|
||||||
dia_vencimento: number;
|
dia_vencimento: number;
|
||||||
idcentrodecustos: number | null;
|
idcentrodecustos: number | null;
|
||||||
|
idclientes?: number | null;
|
||||||
habilitado: number;
|
habilitado: number;
|
||||||
idbancos: number | null;
|
idbancos: number | null;
|
||||||
insert_date: string | null;
|
insert_date: string | null;
|
||||||
|
|
@ -14,6 +15,7 @@ export type MovimentoFixo = {
|
||||||
|
|
||||||
banco_descricao?: string | null;
|
banco_descricao?: string | null;
|
||||||
centro_custo_descricao?: string | null;
|
centro_custo_descricao?: string | null;
|
||||||
|
cliente_nome?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CriarMovimentoFixoRequest = {
|
export type CriarMovimentoFixoRequest = {
|
||||||
|
|
@ -22,6 +24,7 @@ export type CriarMovimentoFixoRequest = {
|
||||||
valor: number;
|
valor: number;
|
||||||
dia_vencimento: number;
|
dia_vencimento: number;
|
||||||
idcentrodecustos: number | null;
|
idcentrodecustos: number | null;
|
||||||
|
idclientes?: number | null;
|
||||||
habilitado: number;
|
habilitado: number;
|
||||||
idbancos: number | null;
|
idbancos: number | null;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ export async function listarBancos(): Promise<Banco[]> {
|
||||||
return response.data.data;
|
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[]> {
|
export async function listarCentrosCusto(): Promise<CentroCusto[]> {
|
||||||
const response = await api.get<ApiListResponse<CentroCusto>>('/referencias/centros-custo');
|
const response = await api.get<ApiListResponse<CentroCusto>>('/referencias/centros-custo');
|
||||||
return response.data.data;
|
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