From 36aba1c64666e4e8462947d0d0a2f00ee4607371 Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Tue, 9 Jun 2026 21:45:46 -0300 Subject: [PATCH] ajustado movimentos fixos sangria --- .../controllers/movimentosFixos.controller.js | 95 +++-- .../services/movimentosFixos.service.js | 266 +++++++++++--- .../movimentosFixosGeracao.service.js | 130 ++++--- .../services/simulacaoMensal.service.js | 55 ++- .../movimentos/components/MovimentoForm.tsx | 30 +- .../components/MovimentoFIxoForm.tsx | 337 +++++++++++++++--- .../types/MovimentoFixoTypes.ts | 65 ++-- .../pages/SimulacaoMensalPage.tsx | 56 ++- 8 files changed, 826 insertions(+), 208 deletions(-) diff --git a/financeiro-api/src/modules/movimentosFixos/controllers/movimentosFixos.controller.js b/financeiro-api/src/modules/movimentosFixos/controllers/movimentosFixos.controller.js index 3b2d12c..9b363e8 100644 --- a/financeiro-api/src/modules/movimentosFixos/controllers/movimentosFixos.controller.js +++ b/financeiro-api/src/modules/movimentosFixos/controllers/movimentosFixos.controller.js @@ -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.', }); } } diff --git a/financeiro-api/src/modules/movimentosFixos/services/movimentosFixos.service.js b/financeiro-api/src/modules/movimentosFixos/services/movimentosFixos.service.js index d7698d6..df91339 100644 --- a/financeiro-api/src/modules/movimentosFixos/services/movimentosFixos.service.js +++ b/financeiro-api/src/modules/movimentosFixos/services/movimentosFixos.service.js @@ -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) { diff --git a/financeiro-api/src/modules/movimentosFixos/services/movimentosFixosGeracao.service.js b/financeiro-api/src/modules/movimentosFixos/services/movimentosFixosGeracao.service.js index efa7163..5504543 100644 --- a/financeiro-api/src/modules/movimentosFixos/services/movimentosFixosGeracao.service.js +++ b/financeiro-api/src/modules/movimentosFixos/services/movimentosFixosGeracao.service.js @@ -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, ] diff --git a/financeiro-api/src/modules/simulacaoMensal/services/simulacaoMensal.service.js b/financeiro-api/src/modules/simulacaoMensal/services/simulacaoMensal.service.js index 7f0821e..9a9d828 100644 --- a/financeiro-api/src/modules/simulacaoMensal/services/simulacaoMensal.service.js +++ b/financeiro-api/src/modules/simulacaoMensal/services/simulacaoMensal.service.js @@ -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, }) diff --git a/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx b/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx index 7014266..8d8678e 100644 --- a/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx +++ b/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx @@ -283,9 +283,13 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) { }, [meusBancos, idBanco]); const statusDisponiveis = useMemo(() => { - 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} > diff --git a/financeiro-web/src/features/movimentosFixos/components/MovimentoFIxoForm.tsx b/financeiro-web/src/features/movimentosFixos/components/MovimentoFIxoForm.tsx index 81a1e10..624fd46 100644 --- a/financeiro-web/src/features/movimentosFixos/components/MovimentoFIxoForm.tsx +++ b/financeiro-web/src/features/movimentosFixos/components/MovimentoFIxoForm.tsx @@ -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([]); + const [meusBancos, setMeusBancos] = useState([]); + const [todosBancos, setTodosBancos] = useState([]); const [centrosCusto, setCentrosCusto] = useState([]); const [clientes, setClientes] = useState([]); @@ -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) { @@ -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) Desabilitado + + 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} + > + + Gerar automaticamente + + + + Não gerar automaticamente + + + setIdBanco(event.target.value)} + helperText={ + isSangria + ? 'Conta da qual o valor será retirado.' + : 'Conta ou carteira associada ao movimento.' + } fullWidth sx={fieldSx} > - Nenhum - {bancos.map((banco) => ( - + {meusBancos.map((banco) => ( + {banco.descricao} ))} @@ -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} > Nenhum + {centrosCusto.map((centro) => ( - + {centro.descricao} ))} - setIdCliente(event.target.value)} - fullWidth - sx={fieldSx} - > - Nenhum - {clientes.map((cliente) => ( - - {cliente.nome} - - ))} - + {usaCliente && ( + + setIdCliente(event.target.value) + } + helperText="Cliente relacionado ao movimento recorrente." + fullWidth + sx={fieldSx} + > + Selecione + + {clientes.map((cliente) => ( + + {cliente.nome} + + ))} + + )} + + {isSangria && ( + + setIdBancoReferencia(event.target.value) + } + helperText="Conta que receberá o valor da sangria." + fullWidth + sx={fieldSx} + > + Selecione + + {todosBancos + .filter( + (banco) => + String(banco.id) !== String(idBanco) + ) + .map((banco) => ( + + {banco.descricao} + + ))} + + )} - - Cliente + + Geração - - {clienteSelecionado?.nome || 'Nenhum'} + + + {Number(gerarAutomatico) === 1 + ? 'Automática' + : 'Manual'} + + + + {isSangria ? 'Banco de origem' : 'Banco/Carteira'} + + + + {bancoOrigemSelecionado?.descricao || 'Não selecionado'} + + + + {usaCliente && ( + + + Cliente + + + + {clienteSelecionado?.nome || 'Não selecionado'} + + + )} + + {isSangria && ( + + + Banco de destino + + + + {bancoDestinoSelecionado?.descricao || 'Não selecionado'} + + + )} diff --git a/financeiro-web/src/features/movimentosFixos/types/MovimentoFixoTypes.ts b/financeiro-web/src/features/movimentosFixos/types/MovimentoFixoTypes.ts index 9847f06..96a5017 100644 --- a/financeiro-web/src/features/movimentosFixos/types/MovimentoFixoTypes.ts +++ b/financeiro-web/src/features/movimentosFixos/types/MovimentoFixoTypes.ts @@ -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 = { - ok: boolean; - message?: string; - data: T; -}; - -export type MovimentosFixosListResponse = { - ok: boolean; - data: MovimentoFixo[]; - pagination: MovimentosFixosPagination; - summary: MovimentosFixosSummary; -}; \ No newline at end of file +export type AtualizarMovimentoFixoRequest = + CriarMovimentoFixoRequest; \ No newline at end of file diff --git a/financeiro-web/src/features/simulacaoMensal/pages/SimulacaoMensalPage.tsx b/financeiro-web/src/features/simulacaoMensal/pages/SimulacaoMensalPage.tsx index 23cb0e9..fd0f163 100644 --- a/financeiro-web/src/features/simulacaoMensal/pages/SimulacaoMensalPage.tsx +++ b/financeiro-web/src/features/simulacaoMensal/pages/SimulacaoMensalPage.tsx @@ -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 && ( + + onAlterarItem(tipo, item.id, { + investimento: Number(event.target.value), + editado: true, + }) + } + > + Não + Sim + + )} + )} + + {!isEntrada && Number(item.investimento) === 1 && ( + } + variant="outlined" + /> + )}