ajustado movimentos fixos sangria

This commit is contained in:
Diego Freitas 2026-06-09 21:45:46 -03:00
parent 12dcc8506f
commit 36aba1c646
8 changed files with 826 additions and 208 deletions

View File

@ -69,6 +69,18 @@ function validarDadosMovimentoFixo(dados) {
}
}
if (
dados.idbancos_p !== undefined &&
dados.idbancos_p !== null &&
dados.idbancos_p !== ''
) {
const idbancosP = Number(dados.idbancos_p);
if (!Number.isInteger(idbancosP) || idbancosP <= 0) {
return 'Banco de destino inválido.';
}
}
if (dados.habilitado !== undefined && dados.habilitado !== null && dados.habilitado !== '') {
const habilitado = Number(dados.habilitado);
@ -77,6 +89,41 @@ function validarDadosMovimentoFixo(dados) {
}
}
if (
dados.gerar_automatico !== undefined &&
dados.gerar_automatico !== null &&
dados.gerar_automatico !== ''
) {
const gerarAutomatico =
Number(dados.gerar_automatico);
if (![0, 1].includes(gerarAutomatico)) {
return 'Configuração de geração automática inválida.';
}
}
if (dados.movimento === 'Sangria') {
const idBancoOrigem = Number(dados.idbancos);
const idBancoDestino = Number(dados.idbancos_p);
if (!idBancoDestino) {
return 'Banco de destino é obrigatório para sangria.';
}
if (idBancoOrigem === idBancoDestino) {
return 'Banco de origem e banco de destino não podem ser iguais.';
}
}
if (
dados.movimento !== 'Sangria' &&
dados.idbancos_p !== undefined &&
dados.idbancos_p !== null &&
dados.idbancos_p !== ''
) {
return 'Banco de destino somente pode ser informado em uma sangria.';
}
return null;
}
@ -84,22 +131,24 @@ async function listar(req, res) {
try {
const idUsuario = extrairIdUsuarioLogado(req);
const resultado = await movimentosFixosService.listarMovimentosFixos({
limite: req.query.limite,
offset: req.query.offset,
page: req.query.page,
busca: req.query.busca,
movimento: req.query.movimento,
idbancos: req.query.idbancos,
idcentrodecustos: req.query.idcentrodecustos,
idclientes: req.query.idclientes,
habilitado: req.query.habilitado,
diaInicio: req.query.diaInicio,
diaFim: req.query.diaFim,
orderBy: req.query.orderBy,
orderDirection: req.query.orderDirection,
idUsuario,
});
const resultado =
await movimentosFixosService.listarMovimentosFixos({
limite: req.query.limite,
offset: req.query.offset,
page: req.query.page,
busca: req.query.busca,
movimento: req.query.movimento,
idbancos: req.query.idbancos,
idcentrodecustos: req.query.idcentrodecustos,
idclientes: req.query.idclientes,
habilitado: req.query.habilitado,
gerar_automatico: req.query.gerar_automatico,
diaInicio: req.query.diaInicio,
diaFim: req.query.diaFim,
orderBy: req.query.orderBy,
orderDirection: req.query.orderDirection,
idUsuario,
});
return res.json({
ok: true,
@ -176,9 +225,12 @@ async function criar(req, res) {
} catch (error) {
console.error('Erro ao criar movimento fixo:', error);
return res.status(500).json({
return res.status(error.statusCode || 500).json({
ok: false,
message: 'Erro ao criar movimento fixo.',
message:
error.statusCode
? error.message
: 'Erro ao criar movimento fixo.',
});
}
}
@ -222,9 +274,12 @@ async function atualizar(req, res) {
} catch (error) {
console.error('Erro ao atualizar movimento fixo:', error);
return res.status(500).json({
return res.status(error.statusCode || 500).json({
ok: false,
message: 'Erro ao atualizar movimento fixo.',
message:
error.statusCode
? error.message
: 'Erro ao atualizar movimento fixo.',
});
}
}

View File

@ -9,19 +9,31 @@ const CAMPOS_MOVIMENTO_FIXO_SELECT = `
mf.idcentrodecustos,
mf.idclientes,
mf.habilitado,
mf.gerar_automatico,
mf.ultimo_mes_gerado,
mf.idbancos,
mf.idbancos_p,
mf.insert_date,
mf.update_date,
b.descricao AS banco_descricao,
bp.descricao AS banco_destino_descricao,
cc.descricao AS centro_custo_descricao,
c.nome AS cliente_nome
`;
const FROM_MOVIMENTO_FIXO_JOIN = `
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
INNER JOIN bancos b
ON b.idbancos = mf.idbancos
LEFT JOIN bancos bp
ON bp.idbancos = mf.idbancos_p
LEFT JOIN centrodecustos cc
ON cc.idcentrodecustos = mf.idcentrodecustos
LEFT JOIN clientes c
ON c.idclientes = mf.idclientes
`;
function normalizarTextoOuNull(valor) {
@ -96,10 +108,14 @@ function resolverOrdenacao(orderBy) {
idcentrodecustos: 'mf.idcentrodecustos',
idclientes: 'mf.idclientes',
idbancos: 'mf.idbancos',
idbancos_p: 'mf.idbancos_p',
habilitado: 'mf.habilitado',
gerar_automatico: 'mf.gerar_automatico',
ultimo_mes_gerado: 'mf.ultimo_mes_gerado',
insert_date: 'mf.insert_date',
update_date: 'mf.update_date',
banco_descricao: 'b.descricao',
banco_destino_descricao: 'bp.descricao',
centro_custo_descricao: 'cc.descricao',
cliente_nome: 'c.nome',
};
@ -145,6 +161,15 @@ function montarWhereMovimentosFixos(filtros = {}) {
params.push(Number(filtros.habilitado));
}
if (
filtros.gerar_automatico !== undefined &&
filtros.gerar_automatico !== null &&
filtros.gerar_automatico !== ''
) {
where.push('mf.gerar_automatico = ?');
params.push(Number(filtros.gerar_automatico));
}
if (filtros.diaInicio) {
where.push('mf.dia_vencimento >= ?');
params.push(Number(filtros.diaInicio));
@ -161,6 +186,7 @@ function montarWhereMovimentosFixos(filtros = {}) {
mf.descricao LIKE ?
OR mf.movimento LIKE ?
OR b.descricao LIKE ?
OR bp.descricao LIKE ?
OR cc.descricao LIKE ?
OR c.nome LIKE ?
)
@ -173,6 +199,7 @@ function montarWhereMovimentosFixos(filtros = {}) {
termo,
termo,
termo,
termo,
termo
);
}
@ -203,6 +230,110 @@ async function bancoPertenceAoUsuario(idbancos, idUsuario) {
return rows.length > 0;
}
async function bancoExiste(idbancos) {
if (!idbancos) return false;
const [rows] = await pool.query(
`
SELECT idbancos
FROM bancos
WHERE idbancos = ?
AND habilitado = 1
LIMIT 1
`,
[Number(idbancos)]
);
return rows.length > 0;
}
async function normalizarReferenciasMovimentoFixo(
dados,
idUsuario
) {
const movimento =
normalizarTextoOuNull(dados.movimento);
const idBanco =
normalizarNumeroOuNull(dados.idbancos);
const idBancoDestinoRecebido =
normalizarNumeroOuNull(dados.idbancos_p);
const idClienteRecebido =
normalizarNumeroOuNull(dados.idclientes);
const isSangria = movimento === 'Sangria';
if (!idBanco) {
const error = new Error(
'Banco/carteira é obrigatório para movimento fixo.'
);
error.statusCode = 400;
throw error;
}
const bancoPrincipalValido =
await bancoPertenceAoUsuario(
idBanco,
idUsuario
);
if (!bancoPrincipalValido) {
const error = new Error(
'Banco/carteira não pertence ao usuário logado.'
);
error.statusCode = 403;
throw error;
}
if (isSangria) {
if (!idBancoDestinoRecebido) {
const error = new Error(
'Banco de destino é obrigatório para sangria.'
);
error.statusCode = 400;
throw error;
}
if (idBanco === idBancoDestinoRecebido) {
const error = new Error(
'Banco de origem e banco de destino não podem ser iguais.'
);
error.statusCode = 400;
throw error;
}
const bancoDestinoValido =
await bancoExiste(idBancoDestinoRecebido);
if (!bancoDestinoValido) {
const error = new Error(
'Banco de destino inválido ou desabilitado.'
);
error.statusCode = 400;
throw error;
}
return {
idbancos: idBanco,
idclientes: null,
idbancos_p: idBancoDestinoRecebido,
};
}
return {
idbancos: idBanco,
idclientes: idClienteRecebido,
idbancos_p: null,
};
}
async function listarMovimentosFixos(filtros = {}) {
const limite = limitarNumero(filtros.limite, 20, 1, 100);
const page = limitarNumero(filtros.page, 1, 1, 999999);
@ -241,10 +372,10 @@ async function listarMovimentosFixos(filtros = {}) {
SELECT
COUNT(*) AS quantidade,
COALESCE(SUM(mf.valor), 0) AS valorTotal,
COALESCE(SUM(CASE WHEN mf.movimento = 'Entrada' THEN mf.valor ELSE 0 END), 0) AS totalEntradas,
COALESCE(SUM(CASE WHEN mf.movimento = 'Saida' THEN mf.valor ELSE 0 END), 0) AS totalSaidas,
COALESCE(SUM(CASE WHEN mf.movimento = 'Sangria' THEN mf.valor ELSE 0 END), 0) AS totalSangrias,
COALESCE(SUM(CASE WHEN mf.movimento = 'Estorno' THEN mf.valor ELSE 0 END), 0) AS totalEstornos,
COALESCE(SUM(CASE WHEN mf.movimento = 'Entrada' and mf.habilitado = 1 THEN mf.valor ELSE 0 END), 0) AS totalEntradas,
COALESCE(SUM(CASE WHEN mf.movimento = 'Saida' and mf.habilitado = 1 THEN mf.valor ELSE 0 END), 0) AS totalSaidas,
COALESCE(SUM(CASE WHEN mf.movimento = 'Sangria' and mf.habilitado = 1 THEN mf.valor ELSE 0 END), 0) AS totalSangrias,
COALESCE(SUM(CASE WHEN mf.movimento = 'Estorno' and mf.habilitado = 1 THEN mf.valor ELSE 0 END), 0) AS totalEstornos,
COALESCE(SUM(CASE WHEN mf.habilitado = 1 THEN 1 ELSE 0 END), 0) AS habilitados,
COALESCE(SUM(CASE WHEN mf.habilitado = 0 THEN 1 ELSE 0 END), 0) AS desabilitados
${FROM_MOVIMENTO_FIXO_JOIN}
@ -309,26 +440,34 @@ async function criarMovimentoFixo(dados, idUsuario) {
valor,
dia_vencimento,
idcentrodecustos,
idclientes,
habilitado,
idbancos,
gerar_automatico,
} = dados;
const idBancoNormalizado = normalizarNumeroOuNull(idbancos);
const movimentoNormalizado =
normalizarTextoOuNull(movimento);
const movimentosPermitidos = [
'Entrada',
'Saida',
'Sangria',
'Estorno',
];
if (!movimentosPermitidos.includes(movimentoNormalizado)) {
const error = new Error(
'Tipo de movimento fixo inválido.'
);
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 referencias =
await normalizarReferenciasMovimentoFixo(
dados,
idUsuario
);
const [result] = await pool.query(
`
@ -340,28 +479,46 @@ async function criarMovimentoFixo(dados, idUsuario) {
idcentrodecustos,
idclientes,
habilitado,
gerar_automatico,
ultimo_mes_gerado,
idbancos,
idbancos_p,
insert_date,
update_date
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, NOW(), NOW()
)
`,
[
normalizarTextoOuNull(movimento),
movimentoNormalizado,
normalizarTextoOuNull(descricao),
normalizarNumero(valor, 0),
normalizarNumero(dia_vencimento, 1),
normalizarNumeroOuNull(idcentrodecustos),
normalizarNumeroOuNull(idclientes),
referencias.idclientes,
normalizarFlag(habilitado, 1),
idBancoNormalizado,
normalizarFlag(gerar_automatico, 1),
referencias.idbancos,
referencias.idbancos_p,
]
);
return buscarMovimentoFixoPorId(result.insertId, idUsuario);
return buscarMovimentoFixoPorId(
result.insertId,
idUsuario
);
}
async function atualizarMovimentoFixo(id, dados, idUsuario) {
const movimentoAtual = await buscarMovimentoFixoPorId(id, idUsuario);
async function atualizarMovimentoFixo(
id,
dados,
idUsuario
) {
const movimentoAtual =
await buscarMovimentoFixoPorId(
id,
idUsuario
);
if (!movimentoAtual) {
return null;
@ -373,26 +530,46 @@ async function atualizarMovimentoFixo(id, dados, idUsuario) {
valor,
dia_vencimento,
idcentrodecustos,
idclientes,
habilitado,
idbancos,
gerar_automatico,
} = dados;
const idBancoNormalizado = normalizarNumeroOuNull(idbancos);
const movimentoNormalizado =
normalizarTextoOuNull(movimento);
const movimentosPermitidos = [
'Entrada',
'Saida',
'Sangria',
'Estorno',
];
if (!movimentosPermitidos.includes(movimentoNormalizado)) {
const error = new Error(
'Tipo de movimento fixo inválido.'
);
if (!idBancoNormalizado) {
const error = new Error('Banco/carteira é obrigatório para movimento fixo.');
error.statusCode = 400;
throw error;
}
const bancoValido = await bancoPertenceAoUsuario(idBancoNormalizado, idUsuario);
const referencias =
await normalizarReferenciasMovimentoFixo(
dados,
idUsuario
);
if (!bancoValido) {
const error = new Error('Banco/carteira não pertence ao usuário logado.');
error.statusCode = 403;
throw error;
}
const gerarAutomaticoNormalizado =
gerar_automatico === undefined ||
gerar_automatico === null ||
gerar_automatico === ''
? Number(
movimentoAtual.gerar_automatico ?? 1
)
: normalizarFlag(
gerar_automatico,
1
);
await pool.query(
`
@ -405,24 +582,31 @@ async function atualizarMovimentoFixo(id, dados, idUsuario) {
idcentrodecustos = ?,
idclientes = ?,
habilitado = ?,
gerar_automatico = ?,
idbancos = ?,
idbancos_p = ?,
update_date = NOW()
WHERE idmovimentosfixos = ?
`,
[
normalizarTextoOuNull(movimento),
movimentoNormalizado,
normalizarTextoOuNull(descricao),
normalizarNumero(valor, 0),
normalizarNumero(dia_vencimento, 1),
normalizarNumeroOuNull(idcentrodecustos),
normalizarNumeroOuNull(idclientes),
referencias.idclientes,
normalizarFlag(habilitado, 1),
idBancoNormalizado,
gerarAutomaticoNormalizado,
referencias.idbancos,
referencias.idbancos_p,
id,
]
);
return buscarMovimentoFixoPorId(id, idUsuario);
return buscarMovimentoFixoPorId(
id,
idUsuario
);
}
async function alterarHabilitadoMovimentoFixo(id, habilitado, idUsuario) {

View File

@ -85,26 +85,21 @@ function statusInicialPorMovimento(movimento) {
return 'A receber';
}
if (movimento === 'Saida') {
if (movimento === 'Saida' || movimento === 'Sangria') {
return 'A pagar';
}
if (movimento === 'Sangria') {
return 'Pago';
}
return 'A pagar';
}
function dataBaixaInicialPorMovimento(movimento, dataVencimento) {
if (movimento === 'Sangria') {
return dataVencimento;
}
function dataBaixaInicialPorMovimento() {
return null;
}
async function buscarFixosDoUsuario(connection, idUsuario) {
async function buscarFixosDoUsuario(
connection,
idUsuario
) {
const [rows] = await connection.query(
`
SELECT
@ -115,11 +110,19 @@ async function buscarFixosDoUsuario(connection, idUsuario) {
mf.dia_vencimento,
mf.idcentrodecustos,
mf.idclientes,
mf.idbancos
mf.idbancos,
mf.idbancos_p
FROM movimentosfixos mf
INNER JOIN bancos b ON b.idbancos = mf.idbancos
INNER JOIN bancos b
ON b.idbancos = mf.idbancos
WHERE mf.habilitado = 1
AND mf.gerar_automatico = 1
AND mf.movimento IN (
'Entrada',
'Saida',
'Sangria',
'Estorno'
)
AND b.idusuarios = ?
ORDER BY mf.idmovimentosfixos ASC
`,
@ -183,35 +186,53 @@ async function jaExisteMovimentoFixo(connection, fixo, competencia) {
return true;
}
const [rowsMovimentoLegado] = await connection.query(
`
SELECT idcontasapagar
FROM contasapagar
WHERE deleted_at IS NULL
AND idmovimentosfixos IS NULL
AND competencia IS NULL
AND movimento = ?
AND descricao = ?
AND idbancos = ?
AND (
(? IS NULL AND idcentrodecustos IS NULL)
OR idcentrodecustos = ?
)
AND DATE(datavencimento) BETWEEN ? AND ?
ORDER BY idcontasapagar DESC
LIMIT 1
FOR UPDATE
`,
[
fixo.movimento,
fixo.descricao,
fixo.idbancos,
fixo.idcentrodecustos || null,
fixo.idcentrodecustos || null,
`${competencia}-01`,
`${competencia}-31`,
]
);
const [rowsMovimentoLegado] =
await connection.query(
`
SELECT idcontasapagar
FROM contasapagar
WHERE deleted_at IS NULL
AND idmovimentosfixos IS NULL
AND competencia IS NULL
AND movimento = ?
AND descricao = ?
AND idbancos = ?
AND (
(? IS NULL AND idcentrodecustos IS NULL)
OR idcentrodecustos = ?
)
AND (
(? IS NULL AND idclientes IS NULL)
OR idclientes = ?
)
AND (
(? IS NULL AND idbancos_p IS NULL)
OR idbancos_p = ?
)
AND DATE(datavencimento)
BETWEEN ? AND ?
ORDER BY idcontasapagar DESC
LIMIT 1
FOR UPDATE
`,
[
fixo.movimento,
fixo.descricao,
fixo.idbancos,
fixo.idcentrodecustos || null,
fixo.idcentrodecustos || null,
fixo.idclientes || null,
fixo.idclientes || null,
fixo.idbancos_p || null,
fixo.idbancos_p || null,
`${competencia}-01`,
`${competencia}-31`,
]
);
if (rowsMovimentoLegado.length > 0) {
const idContasPagar = rowsMovimentoLegado[0].idcontasapagar;
@ -289,7 +310,20 @@ async function inserirMovimentoFixoGerado(connection, fixo, competencia, idUsuar
insert_date,
update_date,
deleted_at
) VALUES (?, ?, ?, ?, ?, ?, 1, 1, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, NULL, 'movimento_fixo', 0, NULL, NOW(), NOW(), NULL)
) VALUES (
?, ?, ?, ?, ?, ?,
1, 1,
?, ?, ?, ?, ?, ?,
NULL,
?, ?, ?,
NULL,
'movimento_fixo',
0,
NULL,
NOW(),
NOW(),
NULL
)
`,
[
fixo.movimento,
@ -303,7 +337,15 @@ async function inserirMovimentoFixoGerado(connection, fixo, competencia, idUsuar
fixo.idbancos || null,
idUsuario,
dataBaixa ? idUsuario : null,
fixo.idclientes || null,
fixo.movimento === 'Sangria'
? null
: fixo.idclientes || null,
fixo.movimento === 'Sangria'
? fixo.idbancos_p || null
: null,
fixo.idmovimentosfixos,
competencia,
]

View File

@ -26,6 +26,19 @@ function pad2(valor) {
return String(valor).padStart(2, '0');
}
function resolverInvestimentoMovimento(movimento) {
const centroEhInvestimento =
Number(movimento.centro_custo_investimento) === 1;
const destinoEhInvestimento =
movimento.movimento === 'Sangria' &&
Number(movimento.banco_referencia_investimento) === 1;
return centroEhInvestimento || destinoEhInvestimento
? 1
: 0;
}
function ultimoDiaDoMes(ano, mes) {
return new Date(Number(ano), Number(mes), 0).getDate();
}
@ -218,7 +231,8 @@ async function buscarMovimentosDoMes(connection, { idUsuario, dataInicio, dataFi
cc.descricao AS centro_custo_descricao,
cc.investimento AS centro_custo_investimento,
c.nome AS cliente_nome,
bp.descricao AS banco_referencia_descricao
bp.descricao AS banco_referencia_descricao,
bp.investimento AS banco_referencia_investimento
FROM contasapagar cp
INNER JOIN bancos b
ON b.idbancos = cp.idbancos
@ -287,14 +301,23 @@ async function buscarFixosUsuario(connection, idUsuario) {
mf.idcentrodecustos,
mf.idclientes,
mf.idbancos,
mf.idbancos_p,
b.descricao AS banco_descricao,
b.debito AS banco_debito,
bp.descricao AS banco_referencia_descricao,
bp.investimento AS banco_referencia_investimento,
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 bancos bp
ON bp.idbancos = mf.idbancos_p
LEFT JOIN centrodecustos cc
ON cc.idcentrodecustos = mf.idcentrodecustos
LEFT JOIN clientes c
@ -302,7 +325,9 @@ async function buscarFixosUsuario(connection, idUsuario) {
WHERE mf.habilitado = 1
AND b.idusuarios = ?
AND COALESCE(mf.gerar_automatico, 1) = 1
ORDER BY mf.dia_vencimento ASC, mf.descricao ASC
ORDER BY
mf.dia_vencimento ASC,
mf.descricao ASC
`,
[idUsuario]
);
@ -363,8 +388,14 @@ function montarItensMovimentosExistentes(movimentosDoMes, incluirQuitadas) {
const saidas = [];
for (const movimento of movimentosDoMes) {
const bancoDebito = Number(movimento.banco_debito) === 1;
const bancoCredito = Number(movimento.banco_debito) === 0;
const bancoDebito =
Number(movimento.banco_debito) === 1;
const bancoCredito =
Number(movimento.banco_debito) === 0;
const investimento =
resolverInvestimentoMovimento(movimento);
if (bancoDebito) {
if (!incluirQuitadas && !statusEmAberto(movimento.status)) {
@ -387,7 +418,7 @@ function montarItensMovimentosExistentes(movimentosDoMes, incluirQuitadas) {
banco_debito: movimento.banco_debito,
idcentrodecustos: movimento.idcentrodecustos,
centro_custo_descricao: movimento.centro_custo_descricao,
investimento: movimento.centro_custo_investimento,
investimento: investimento,
idclientes: movimento.idclientes,
cliente_nome: movimento.cliente_nome,
detalhes: {
@ -415,7 +446,7 @@ function montarItensMovimentosExistentes(movimentosDoMes, incluirQuitadas) {
banco_debito: movimento.banco_debito,
idcentrodecustos: movimento.idcentrodecustos,
centro_custo_descricao: movimento.centro_custo_descricao,
investimento: movimento.centro_custo_investimento,
investimento: investimento,
idclientes: movimento.idclientes,
cliente_nome: movimento.cliente_nome,
detalhes: {
@ -443,7 +474,7 @@ function montarItensMovimentosExistentes(movimentosDoMes, incluirQuitadas) {
banco_debito: movimento.banco_debito,
idcentrodecustos: movimento.idcentrodecustos,
centro_custo_descricao: movimento.centro_custo_descricao,
investimento: movimento.centro_custo_investimento,
investimento: investimento,
idclientes: movimento.idclientes,
cliente_nome: movimento.cliente_nome,
})
@ -553,6 +584,7 @@ function montarItensFixosPrevistos({
}
const dataVencimento = montarDataVencimento(ano, mes, fixo.dia_vencimento);
const investimento = resolverInvestimentoMovimento(fixo);
if (movimentoEhEntrada(fixo.movimento)) {
entradas.push(
@ -570,7 +602,7 @@ function montarItensFixosPrevistos({
banco_debito: fixo.banco_debito,
idcentrodecustos: fixo.idcentrodecustos,
centro_custo_descricao: fixo.centro_custo_descricao,
investimento: fixo.centro_custo_investimento,
investimento: investimento,
idclientes: fixo.idclientes,
cliente_nome: fixo.cliente_nome,
})
@ -585,15 +617,16 @@ function montarItensFixosPrevistos({
descricao: fixo.descricao,
valor: fixo.valor,
movimento: fixo.movimento,
status: fixo.movimento === 'Sangria' ? 'Pago' : 'A pagar',
status: '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,
centro_custo_descricao:
fixo.centro_custo_descricao,
investimento,
idclientes: fixo.idclientes,
cliente_nome: fixo.cliente_nome,
})

View File

@ -283,9 +283,13 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
}, [meusBancos, idBanco]);
const statusDisponiveis = useMemo<MovimentoStatus[]>(() => {
if (movimento === 'Entrada') return ['Recebido', 'A receber'];
if (movimento === 'Saida') return ['Pago', 'A pagar'];
if (movimento === 'Sangria') return ['Pago'];
if (movimento === 'Entrada') {
return ['Recebido', 'A receber'];
}
if (movimento === 'Saida' || movimento === 'Sangria') {
return ['Pago', 'A pagar'];
}
return ['Pago', 'Recebido'];
}, [movimento]);
@ -324,8 +328,6 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
if (movimento === 'Sangria') {
setReferenciaTipo('banco');
setStatus('Pago');
setDataBaixa((dataAtual) => dataAtual || hojeISO());
setParcelas('1');
setParcela('1');
setGerarParcelas(false);
@ -415,6 +417,17 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
status,
]);
useEffect(() => {
if (status === 'A pagar' || status === 'A receber') {
setDataBaixa('');
return;
}
if (!dataBaixa) {
setDataBaixa(hojeISO());
}
}, [status]);
function limparFormulario() {
setDescricao('');
setValor('');
@ -567,8 +580,10 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
function montarPayload(confirmarDuplicado = false): CriarMovimentoRequest {
const isSangria = movimento === 'Sangria';
const statusFinal = isSangria ? 'Pago' : status;
const referenciaTipoFinal = isSangria ? 'banco' : referenciaTipo;
const statusFinal = status;
const referenciaTipoFinal = isSangria
? 'banco'
: referenciaTipo;
const parcelaFinal = isSangria ? 1 : Number(parcela || 1);
const parcelasFinal = isSangria ? 1 : Number(parcelas || 1);
const dataBaixaFinal =
@ -731,7 +746,6 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
label="Situação"
value={status}
onChange={(event) => setStatus(event.target.value as MovimentoStatus)}
disabled={movimento === 'Sangria'}
fullWidth
sx={fieldSx}
>

View File

@ -20,6 +20,7 @@ import EventRepeatIcon from '@mui/icons-material/EventRepeat';
import SaveIcon from '@mui/icons-material/Save';
import { useNavigate } from 'react-router-dom';
import {
listarBancos,
listarMeusBancos,
listarCentrosCusto,
listarClientes,
@ -123,9 +124,8 @@ function movimentoLabel(movimento: MovimentoFixoTipo) {
export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps) {
const navigate = useNavigate();
const isEdit = mode === 'edit';
const [bancos, setBancos] = useState<Banco[]>([]);
const [meusBancos, setMeusBancos] = useState<Banco[]>([]);
const [todosBancos, setTodosBancos] = useState<Banco[]>([]);
const [centrosCusto, setCentrosCusto] = useState<CentroCusto[]>([]);
const [clientes, setClientes] = useState<Cliente[]>([]);
@ -141,7 +141,16 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
const [idCentroCusto, setIdCentroCusto] = useState('');
const [idCliente, setIdCliente] = useState('');
const [idBanco, setIdBanco] = useState('');
const [idBancoReferencia, setIdBancoReferencia] = useState('');
const [habilitado, setHabilitado] = useState('1');
const [gerarAutomatico, setGerarAutomatico] = useState('1');
const isEdit = mode === 'edit';
const isSangria = movimento === 'Sangria';
const usaCliente =
movimento === 'Entrada' ||
movimento === 'Saida' ||
movimento === 'Estorno';
const titulo = isEdit ? 'Editar movimento fixo' : 'Novo movimento fixo';
const subtitulo = isEdit
@ -149,10 +158,19 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
: 'Cadastre lançamentos recorrentes para automatizar o financeiro depois.';
const movimentoDescricao = useMemo(() => {
if (movimento === 'Entrada') return 'Entrada recorrente';
if (movimento === 'Saida') return 'Saída recorrente';
if (movimento === 'Sangria') return 'Sangria recorrente';
return 'Estorno recorrente';
if (movimento === 'Entrada') {
return 'Receita recorrente a receber';
}
if (movimento === 'Saida') {
return 'Despesa recorrente a pagar';
}
if (movimento === 'Sangria') {
return 'Retirada recorrente a efetivar';
}
return 'Estorno recorrente a receber';
}, [movimento]);
const clienteSelecionado = useMemo(() => {
@ -161,6 +179,28 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
return clientes.find((cliente) => String(cliente.id) === String(idCliente)) || null;
}, [clientes, idCliente]);
const bancoOrigemSelecionado = useMemo(() => {
if (!idBanco) return null;
return (
meusBancos.find(
(banco) =>
String(banco.id) === String(idBanco)
) || null
);
}, [meusBancos, idBanco]);
const bancoDestinoSelecionado = useMemo(() => {
if (!idBancoReferencia) return null;
return (
todosBancos.find(
(banco) =>
String(banco.id) === String(idBancoReferencia)
) || null
);
}, [todosBancos, idBancoReferencia]);
useEffect(() => {
if (!initialData) return;
@ -168,10 +208,38 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
setDescricao(initialData.descricao || '');
setValor(String(initialData.valor || ''));
setDiaVencimento(String(initialData.dia_vencimento || 1));
setIdCentroCusto(initialData.idcentrodecustos ? String(initialData.idcentrodecustos) : '');
setIdCliente(initialData.idclientes ? String(initialData.idclientes) : '');
setIdBanco(initialData.idbancos ? String(initialData.idbancos) : '');
setHabilitado(String(initialData.habilitado ?? 1));
setIdCentroCusto(
initialData.idcentrodecustos
? String(initialData.idcentrodecustos)
: ''
);
setIdCliente(
initialData.idclientes
? String(initialData.idclientes)
: ''
);
setIdBanco(
initialData.idbancos
? String(initialData.idbancos)
: ''
);
setIdBancoReferencia(
initialData.idbancos_p
? String(initialData.idbancos_p)
: ''
);
setHabilitado(
String(initialData.habilitado ?? 1)
);
setGerarAutomatico(
String(initialData.gerar_automatico ?? 1)
);
}, [initialData]);
useEffect(() => {
@ -180,13 +248,20 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
setLoadingRefs(true);
setErro('');
const [bancosData, centrosData, clientesData] = await Promise.all([
const [
meusBancosData,
todosBancosData,
centrosData,
clientesData,
] = await Promise.all([
listarMeusBancos(),
listarBancos(),
listarCentrosCusto(),
listarClientes(),
]);
setBancos(bancosData);
setMeusBancos(meusBancosData);
setTodosBancos(todosBancosData);
setCentrosCusto(centrosData);
setClientes(clientesData);
} catch (error: any) {
@ -203,6 +278,17 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
carregarReferencias();
}, []);
useEffect(() => {
if (movimento === 'Sangria') {
// Sangria usa banco de destino e nunca cliente.
setIdCliente('');
return;
}
// Outros movimentos usam cliente e nunca banco de destino.
setIdBancoReferencia('');
}, [movimento]);
function limparFormulario() {
setMovimento('Saida');
setDescricao('');
@ -211,7 +297,9 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
setIdCentroCusto('');
setIdCliente('');
setIdBanco('');
setIdBancoReferencia('');
setHabilitado('1');
setGerarAutomatico('1');
}
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
@ -234,6 +322,28 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
return;
}
if (!idBanco) {
setErro('Selecione o banco ou carteira do movimento fixo.');
return;
}
if (usaCliente && !idCliente) {
setErro('Selecione o cliente de referência.');
return;
}
if (isSangria) {
if (!idBancoReferencia) {
setErro('Selecione o banco de destino da sangria.');
return;
}
if (String(idBanco) === String(idBancoReferencia)) {
setErro('Banco de origem e banco de destino não podem ser iguais.');
return;
}
}
try {
setSaving(true);
setErro('');
@ -244,10 +354,23 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
descricao: descricao.trim(),
valor: Number(valor),
dia_vencimento: dia,
idcentrodecustos: toNumberOrNull(idCentroCusto),
idclientes: toNumberOrNull(idCliente),
habilitado: Number(habilitado || 1),
idbancos: toNumberOrNull(idBanco),
idclientes:
usaCliente
? toNumberOrNull(idCliente)
: null,
idbancos: Number(idBanco),
idbancos_p:
isSangria
? toNumberOrNull(idBancoReferencia)
: null,
habilitado: Number(habilitado),
gerar_automatico: Number(gerarAutomatico),
};
if (isEdit) {
@ -377,6 +500,30 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
<MenuItem value="0">Desabilitado</MenuItem>
</TextField>
<TextField
select
label="Geração automática"
value={gerarAutomatico}
onChange={(event) =>
setGerarAutomatico(event.target.value)
}
helperText={
Number(gerarAutomatico) === 1
? 'O movimento será gerado automaticamente em cada competência.'
: 'O cadastro ficará salvo, mas não será gerado automaticamente.'
}
fullWidth
sx={fieldSx}
>
<MenuItem value="1">
Gerar automaticamente
</MenuItem>
<MenuItem value="0">
Não gerar automaticamente
</MenuItem>
</TextField>
<TextField
label="Descrição"
value={descricao}
@ -415,19 +562,31 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
<FormSection
title="Classificação"
description="Associe banco/carteira, centro de custo e cliente ao movimento fixo."
description={
isSangria
? 'Associe o banco de origem, o banco de destino e o centro de custo.'
: 'Associe banco/carteira, centro de custo e cliente ao movimento fixo.'
}
>
<TextField
select
label="Banco/Carteira"
required
label={isSangria ? 'Banco de origem' : 'Banco/Carteira'}
value={idBanco}
onChange={(event) => setIdBanco(event.target.value)}
helperText={
isSangria
? 'Conta da qual o valor será retirado.'
: 'Conta ou carteira associada ao movimento.'
}
fullWidth
sx={fieldSx}
>
<MenuItem value="">Nenhum</MenuItem>
{bancos.map((banco) => (
<MenuItem key={banco.id} value={String(banco.id)}>
{meusBancos.map((banco) => (
<MenuItem
key={banco.id}
value={String(banco.id)}
>
{banco.descricao}
</MenuItem>
))}
@ -437,33 +596,80 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
select
label="Centro de custo"
value={idCentroCusto}
onChange={(event) => setIdCentroCusto(event.target.value)}
onChange={(event) =>
setIdCentroCusto(event.target.value)
}
fullWidth
sx={fieldSx}
>
<MenuItem value="">Nenhum</MenuItem>
{centrosCusto.map((centro) => (
<MenuItem key={centro.id} value={String(centro.id)}>
<MenuItem
key={centro.id}
value={String(centro.id)}
>
{centro.descricao}
</MenuItem>
))}
</TextField>
<TextField
select
label="Cliente"
value={idCliente}
onChange={(event) => setIdCliente(event.target.value)}
fullWidth
sx={fieldSx}
>
<MenuItem value="">Nenhum</MenuItem>
{clientes.map((cliente) => (
<MenuItem key={cliente.id} value={String(cliente.id)}>
{cliente.nome}
</MenuItem>
))}
</TextField>
{usaCliente && (
<TextField
select
required
label="Cliente"
value={idCliente}
onChange={(event) =>
setIdCliente(event.target.value)
}
helperText="Cliente relacionado ao movimento recorrente."
fullWidth
sx={fieldSx}
>
<MenuItem value="">Selecione</MenuItem>
{clientes.map((cliente) => (
<MenuItem
key={cliente.id}
value={String(cliente.id)}
>
{cliente.nome}
</MenuItem>
))}
</TextField>
)}
{isSangria && (
<TextField
select
required
label="Banco de destino"
value={idBancoReferencia}
onChange={(event) =>
setIdBancoReferencia(event.target.value)
}
helperText="Conta que receberá o valor da sangria."
fullWidth
sx={fieldSx}
>
<MenuItem value="">Selecione</MenuItem>
{todosBancos
.filter(
(banco) =>
String(banco.id) !== String(idBanco)
)
.map((banco) => (
<MenuItem
key={banco.id}
value={String(banco.id)}
>
{banco.descricao}
</MenuItem>
))}
</TextField>
)}
</FormSection>
<Stack
@ -592,13 +798,60 @@ export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps)
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Cliente
<Typography
variant="caption"
color="text.secondary"
>
Geração
</Typography>
<Typography fontWeight={800}>
{clienteSelecionado?.nome || 'Nenhum'}
<Typography
fontWeight={800}
color={
Number(gerarAutomatico) === 1
? 'primary.main'
: 'text.secondary'
}
>
{Number(gerarAutomatico) === 1
? 'Automática'
: 'Manual'}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
{isSangria ? 'Banco de origem' : 'Banco/Carteira'}
</Typography>
<Typography fontWeight={800}>
{bancoOrigemSelecionado?.descricao || 'Não selecionado'}
</Typography>
</Box>
{usaCliente && (
<Box>
<Typography variant="caption" color="text.secondary">
Cliente
</Typography>
<Typography fontWeight={800}>
{clienteSelecionado?.nome || 'Não selecionado'}
</Typography>
</Box>
)}
{isSangria && (
<Box>
<Typography variant="caption" color="text.secondary">
Banco de destino
</Typography>
<Typography fontWeight={800}>
{bancoDestinoSelecionado?.descricao || 'Não selecionado'}
</Typography>
</Box>
)}
</Stack>
</Paper>
</Stack>

View File

@ -1,4 +1,8 @@
export type MovimentoFixoTipo = 'Entrada' | 'Saida' | 'Sangria' | 'Estorno';
export type MovimentoFixoTipo =
| 'Entrada'
| 'Saida'
| 'Sangria'
| 'Estorno';
export type MovimentoFixo = {
idmovimentosfixos: number;
@ -6,14 +10,25 @@ export type MovimentoFixo = {
descricao: string;
valor: number;
dia_vencimento: number;
idcentrodecustos: number | null;
idclientes?: number | null;
idclientes: number | null;
// Banco principal/origem do movimento.
idbancos: number;
// Banco de destino utilizado apenas em Sangria.
idbancos_p: number | null;
habilitado: number;
idbancos: number | null;
gerar_automatico: number;
ultimo_mes_gerado: string | null;
insert_date: string | null;
update_date: string | null;
banco_descricao?: string | null;
banco_destino_descricao?: string | null;
centro_custo_descricao?: string | null;
cliente_nome?: string | null;
};
@ -23,42 +38,16 @@ export type CriarMovimentoFixoRequest = {
descricao: string;
valor: number;
dia_vencimento: number;
idcentrodecustos: number | null;
idclientes?: number | null;
idclientes: number | null;
idbancos: number;
idbancos_p: number | null;
habilitado: number;
idbancos: number | null;
gerar_automatico: number;
};
export type AtualizarMovimentoFixoRequest = CriarMovimentoFixoRequest;
export type MovimentosFixosPagination = {
total: number;
limite: number;
offset: number;
page: number;
totalPages: number;
};
export type MovimentosFixosSummary = {
quantidade: number;
valorTotal: number;
totalEntradas: number;
totalSaidas: number;
totalSangrias: number;
totalEstornos: number;
habilitados: number;
desabilitados: number;
};
export type ApiResponse<T> = {
ok: boolean;
message?: string;
data: T;
};
export type MovimentosFixosListResponse = {
ok: boolean;
data: MovimentoFixo[];
pagination: MovimentosFixosPagination;
summary: MovimentosFixosSummary;
};
export type AtualizarMovimentoFixoRequest =
CriarMovimentoFixoRequest;

View File

@ -144,19 +144,33 @@ function origemColor(origem: string) {
return 'default';
}
function criarItemManual(tipo: SimulacaoMensalTipoItem): SimulacaoMensalItem {
const id = `manual:${tipo}:${Date.now()}:${Math.random().toString(16).slice(2)}`;
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',
descricao:
tipo === 'entrada'
? 'Nova entrada'
: 'Nova saída',
valor: 0,
valor_original: 0,
movimento: tipo === 'entrada' ? 'Entrada' : 'Saida',
movimento:
tipo === 'entrada'
? 'Entrada'
: 'Saida',
status: 'Simulado',
data: null,
investimento: 0,
editavel: true,
editado: true,
};
@ -303,6 +317,30 @@ function SimulacaoLista({
}
/>
{!isEntrada && (
<TextField
select
label="Investimento"
value={Number(item.investimento || 0)}
size="small"
sx={{
width: {
xs: '100%',
md: 165,
},
}}
onChange={(event) =>
onAlterarItem(tipo, item.id, {
investimento: Number(event.target.value),
editado: true,
})
}
>
<MenuItem value={0}>Não</MenuItem>
<MenuItem value={1}>Sim</MenuItem>
</TextField>
)}
<Tooltip title="Remover da simulação">
<IconButton
color="error"
@ -396,6 +434,16 @@ function SimulacaoLista({
variant="outlined"
/>
)}
{!isEntrada && Number(item.investimento) === 1 && (
<Chip
size="small"
label="Investimento"
color="info"
icon={<AccountBalanceIcon />}
variant="outlined"
/>
)}
</Stack>
</Stack>
</Paper>