From 0731328492d974b357cea6519729e7195c8eef40 Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Mon, 1 Jun 2026 22:30:10 -0300 Subject: [PATCH] adicionado tela de rendimentos e filtrado bancos por usuario --- financeiro-api/src/app.js | 2 + .../bancos/controllers/bancos.controller.js | 30 +- .../bancosRendimentos.controller.js | 128 ++ .../bancos/routes/bancosRendimentos.routes.js | 13 + .../modules/bancos/services/bancos.service.js | 70 +- .../services/bancosRendimentos.service.js | 726 ++++++++++ .../controllers/dashboard.controller.js | 15 +- .../dashboard/services/dashboard.service.js | 203 ++- .../controllers/movimentos.controller.js | 8 +- .../movimentos/services/movimentos.service.js | 45 +- .../services/movimentosSaldo.service.js | 42 +- .../quitacoesCredito.controller.js | 70 +- .../services/quitacoesCredito.service.js | 91 +- .../services/referencias.service.js | 29 +- financeiro-web/src/app/router.tsx | 5 + .../src/components/layout/Sidebar.tsx | 6 + .../features/bancos/components/BancoForm.tsx | 41 +- .../src/features/bancos/pages/BancosPage.tsx | 82 +- .../features/bancos/pages/RendimentosPage.tsx | 1216 +++++++++++++++++ .../features/bancos/services/bancosService.ts | 1 + .../bancos/services/rendimentosService.ts | 55 + .../src/features/bancos/types/bancoTypes.ts | 6 +- .../features/bancos/types/rendimentosTypes.ts | 155 +++ .../pages/QuitacoesCreditoPage.tsx | 2 +- .../types/quitacaoCreditoTypes.ts | 2 +- 25 files changed, 2873 insertions(+), 170 deletions(-) create mode 100644 financeiro-api/src/modules/bancos/controllers/bancosRendimentos.controller.js create mode 100644 financeiro-api/src/modules/bancos/routes/bancosRendimentos.routes.js create mode 100644 financeiro-api/src/modules/bancos/services/bancosRendimentos.service.js create mode 100644 financeiro-web/src/features/bancos/pages/RendimentosPage.tsx create mode 100644 financeiro-web/src/features/bancos/services/rendimentosService.ts create mode 100644 financeiro-web/src/features/bancos/types/rendimentosTypes.ts diff --git a/financeiro-api/src/app.js b/financeiro-api/src/app.js index 269b298..ed0be74 100644 --- a/financeiro-api/src/app.js +++ b/financeiro-api/src/app.js @@ -7,6 +7,7 @@ const referenciasRoutes = require('./modules/referencias/routes/referencias.rout const relatoriosRoutes = require('./modules/relatorios/routes/relatorios.routes'); const clientesRoutes = require('./modules/clientes/routes/clientes.routes'); const bancosRoutes = require('./modules/bancos/routes/bancos.routes'); +const bancosRendimentosRoutes = require('./modules/bancos/routes/bancosRendimentos.routes'); const centrosCustoRoutes = require('./modules/centrosCusto/routes/centrosCusto.routes'); const movimentosFixosRoutes = require('./modules/movimentosFixos/routes/movimentosFixos.routes'); const usuariosRoutes = require('./modules/usuarios/routes/usuarios.routes'); @@ -41,6 +42,7 @@ app.use('/api/referencias', referenciasRoutes); app.use('/api/relatorios', relatoriosRoutes); app.use('/api/clientes', clientesRoutes); app.use('/api/bancos', bancosRoutes); +app.use('/api/bancos/rendimentos', bancosRendimentosRoutes); app.use('/api/centros-custo', centrosCustoRoutes); app.use('/api/movimentos-fixos', movimentosFixosRoutes); app.use('/api/usuarios', usuariosRoutes); diff --git a/financeiro-api/src/modules/bancos/controllers/bancos.controller.js b/financeiro-api/src/modules/bancos/controllers/bancos.controller.js index 0663d5a..f06ab5b 100644 --- a/financeiro-api/src/modules/bancos/controllers/bancos.controller.js +++ b/financeiro-api/src/modules/bancos/controllers/bancos.controller.js @@ -1,6 +1,14 @@ const bancosService = require('../services/bancos.service'); function validarDadosBanco(dados) { + if (dados.idusuarios !== undefined && dados.idusuarios !== null && dados.idusuarios !== '') { + const idusuarios = Number(dados.idusuarios); + + if (!Number.isFinite(idusuarios)) { + return 'ID usuário inválido.'; + } + } + if (!dados.descricao || !String(dados.descricao).trim()) { return 'Descrição é obrigatória.'; } @@ -21,6 +29,14 @@ function validarDadosBanco(dados) { } } + if (dados.investimento !== undefined && dados.investimento !== null && dados.investimento !== '') { + const investimento = Number(dados.investimento); + + if (![0, 1].includes(investimento)) { + return 'Tipo investimento inválido.'; + } + } + if (dados.habilitado !== undefined && dados.habilitado !== null && dados.habilitado !== '') { const habilitado = Number(dados.habilitado); @@ -32,7 +48,7 @@ function validarDadosBanco(dados) { if (dados.dia_vencimento !== undefined && dados.dia_vencimento !== null && dados.dia_vencimento !== '') { const dia_vencimento = Number(dados.dia_vencimento); - if (!Number.isFinite(dia_vencimento) || dia_vencimento > 31) { + if (!Number.isFinite(dia_vencimento) || dia_vencimento < 1 || dia_vencimento > 31) { return 'Dia de Vencimeneto inválido.'; } } @@ -47,7 +63,9 @@ async function listar(req, res) { offset: req.query.offset, page: req.query.page, busca: req.query.busca, + idusuarios: req.user.id, debito: req.query.debito, + investimento: req.query.investimento, dia_vencimento: req.query.dia_vencimento, habilitado: req.query.habilitado, orderBy: req.query.orderBy, @@ -81,7 +99,7 @@ async function buscarPorId(req, res) { }); } - const banco = await bancosService.buscarBancoPorId(id); + const banco = await bancosService.buscarBancoPorId(id, req.user.id); if (!banco) { return res.status(404).json({ @@ -115,7 +133,7 @@ async function criar(req, res) { }); } - const novoBanco = await bancosService.criarBanco(req.body); + const novoBanco = await bancosService.criarBanco(req.user.id, req.body); return res.status(201).json({ ok: true, @@ -152,7 +170,7 @@ async function atualizar(req, res) { }); } - const bancoAtualizado = await bancosService.atualizarBanco(id, req.body); + const bancoAtualizado = await bancosService.atualizarBanco(id, req.user.id, req.body); if (!bancoAtualizado) { return res.status(404).json({ @@ -196,7 +214,7 @@ async function alterarHabilitado(req, res) { }); } - const bancoAtualizado = await bancosService.alterarHabilitadoBanco(id, habilitado); + const bancoAtualizado = await bancosService.alterarHabilitadoBanco(id, req.user.id, habilitado); if (!bancoAtualizado) { return res.status(404).json({ @@ -233,7 +251,7 @@ async function deletar(req, res) { }); } - const deletado = await bancosService.deletarBanco(id); + const deletado = await bancosService.deletarBanco(id, req.user.id); if (!deletado) { return res.status(404).json({ diff --git a/financeiro-api/src/modules/bancos/controllers/bancosRendimentos.controller.js b/financeiro-api/src/modules/bancos/controllers/bancosRendimentos.controller.js new file mode 100644 index 0000000..27e14c1 --- /dev/null +++ b/financeiro-api/src/modules/bancos/controllers/bancosRendimentos.controller.js @@ -0,0 +1,128 @@ +const bancosRendimentosService = require('../services/bancosRendimentos.service'); + +function resolverIdUsuario(req) { + return Number(req.user?.idusuarios || req.user?.id); +} + +async function dashboard(req, res) { + try { + const idusuarios = resolverIdUsuario(req); + + if (!idusuarios) { + return res.status(401).json({ + ok: false, + message: 'Usuário inválido.', + }); + } + + const resultado = await bancosRendimentosService.buscarDashboard(idusuarios, { + periodoMeses: req.query.periodoMeses, + dataInicio: req.query.dataInicio, + dataFim: req.query.dataFim, + mesesProjecao: req.query.mesesProjecao, + }); + + return res.json({ + ok: true, + data: resultado, + }); + } catch (error) { + console.error('Erro ao buscar dashboard de rendimentos:', error); + + return res.status(500).json({ + ok: false, + message: 'Erro ao buscar dashboard de rendimentos.', + }); + } +} + +async function historico(req, res) { + try { + const idusuarios = resolverIdUsuario(req); + + if (!idusuarios) { + return res.status(401).json({ + ok: false, + message: 'Usuário inválido.', + }); + } + + const resultado = await bancosRendimentosService.buscarHistorico(idusuarios, { + idbancos: req.query.idbancos, + periodoMeses: req.query.periodoMeses, + dataInicio: req.query.dataInicio, + dataFim: req.query.dataFim, + }); + + return res.json({ + ok: true, + data: resultado, + }); + } catch (error) { + console.error('Erro ao buscar histórico de rendimentos:', error); + + return res.status(500).json({ + ok: false, + message: 'Erro ao buscar histórico de rendimentos.', + }); + } +} + +async function computarRendimentos(req, res) { + try { + const idusuarios = resolverIdUsuario(req); + + if (!idusuarios) { + return res.status(401).json({ + ok: false, + message: 'Usuário inválido.', + }); + } + + const itens = Array.isArray(req.body?.itens) ? req.body.itens : []; + + if (itens.length === 0) { + return res.status(400).json({ + ok: false, + message: 'Informe ao menos um rendimento para computar.', + }); + } + + const resultado = await bancosRendimentosService.computarRendimentos( + idusuarios, + { + itens, + data: req.body?.data, + descricao: req.body?.descricao, + confirmarDuplicado: req.body?.confirmarDuplicado, + } + ); + + return res.status(201).json({ + ok: true, + message: 'Rendimentos computados com sucesso.', + data: resultado, + }); + } catch (error) { + console.error('Erro ao computar rendimentos:', error); + + if (error.code === 'DUPLICIDADE_MOVIMENTO') { + return res.status(409).json({ + ok: false, + message: error.message, + data: error.data, + }); + } + + return res.status(500).json({ + ok: false, + message: 'Erro ao computar rendimentos.', + }); + } +} + +module.exports = { + dashboard, + historico, + computarRendimentos, +}; \ No newline at end of file diff --git a/financeiro-api/src/modules/bancos/routes/bancosRendimentos.routes.js b/financeiro-api/src/modules/bancos/routes/bancosRendimentos.routes.js new file mode 100644 index 0000000..807ceba --- /dev/null +++ b/financeiro-api/src/modules/bancos/routes/bancosRendimentos.routes.js @@ -0,0 +1,13 @@ +const express = require('express'); +const authMiddleware = require('../../../middlewares/auth.middleware'); +const bancosRendimentosController = require('../controllers/bancosRendimentos.controller'); + +const router = express.Router(); + +router.use(authMiddleware); + +router.get('/dashboard', bancosRendimentosController.dashboard); +router.get('/historico', bancosRendimentosController.historico); +router.post('/computar', bancosRendimentosController.computarRendimentos); + +module.exports = router; \ No newline at end of file diff --git a/financeiro-api/src/modules/bancos/services/bancos.service.js b/financeiro-api/src/modules/bancos/services/bancos.service.js index dfcaf0f..25d1a22 100644 --- a/financeiro-api/src/modules/bancos/services/bancos.service.js +++ b/financeiro-api/src/modules/bancos/services/bancos.service.js @@ -2,9 +2,11 @@ const pool = require('../../../database/mysql'); const CAMPOS_BANCO_SELECT = ` idbancos, + idusuarios, descricao, saldo, debito, + investimento, habilitado, dia_vencimento, insert_date, @@ -53,10 +55,12 @@ function limitarNumero(valor, padrao, minimo, maximo) { function resolverOrdenacao(orderBy) { const camposPermitidos = { + idusuarios: 'idusuarios', idbancos: 'idbancos', descricao: 'descricao', saldo: 'saldo', debito: 'debito', + investimento: 'investimento', habilitado: 'habilitado', dia_vencimento: 'dia_vencimento', insert_date: 'insert_date', @@ -74,11 +78,21 @@ function montarWhereBancos(filtros = {}) { const where = []; const params = []; + if (filtros.idusuarios !== undefined && filtros.idusuarios !== null && filtros.idusuarios !== '') { + where.push('idusuarios = ?'); + params.push(Number(filtros.idusuarios)); + } + if (filtros.debito !== undefined && filtros.debito !== null && filtros.debito !== '') { where.push('debito = ?'); params.push(Number(filtros.debito)); } + if (filtros.investimento !== undefined && filtros.investimento !== null && filtros.investimento !== '') { + where.push('investimento = ?'); + params.push(Number(filtros.investimento)); + } + if (filtros.habilitado !== undefined && filtros.habilitado !== null && filtros.habilitado !== '') { where.push('habilitado = ?'); params.push(Number(filtros.habilitado)); @@ -146,8 +160,9 @@ async function listarBancos(filtros = {}) { SELECT COUNT(*) AS quantidade, COALESCE(SUM(saldo), 0) AS saldoTotal, - COALESCE(SUM(CASE WHEN debito = 0 THEN saldo ELSE 0 END), 0) AS saldoCarteiras, - COALESCE(SUM(CASE WHEN debito = 1 THEN saldo ELSE 0 END), 0) AS saldoDebito + COALESCE(SUM(CASE WHEN debito = 0 THEN saldo ELSE 0 END), 0) AS saldoCredito, + COALESCE(SUM(CASE WHEN debito = 1 and investimento = 0 THEN saldo ELSE 0 END), 0) AS saldoDebito, + COALESCE(SUM(CASE WHEN debito = 1 and investimento = 1 THEN saldo ELSE 0 END), 0) AS saldoInvestimento FROM bancos ${whereSql} `, @@ -169,32 +184,36 @@ async function listarBancos(filtros = {}) { summary: { quantidade: Number(summaryRows[0]?.quantidade || 0), saldoTotal: Number(summaryRows[0]?.saldoTotal || 0), - saldoCarteiras: Number(summaryRows[0]?.saldoCarteiras || 0), + saldoCredito: Number(summaryRows[0]?.saldoCredito || 0), saldoDebito: Number(summaryRows[0]?.saldoDebito || 0), + saldoInvestimento: Number(summaryRows[0]?.saldoInvestimento || 0), }, }; } -async function buscarBancoPorId(id) { +async function buscarBancoPorId(id, idusuarios) { const [rows] = await pool.query( ` SELECT ${CAMPOS_BANCO_SELECT} FROM bancos WHERE idbancos = ? + AND idusuarios = ? LIMIT 1 `, - [id] + [id, idusuarios] ); return rows[0] || null; } -async function criarBanco(dados) { +async function criarBanco(_idusuarios, dados) { const { + idusuarios, descricao, saldo, debito, + investimento, habilitado, dia_vencimento, } = dados; @@ -202,19 +221,23 @@ async function criarBanco(dados) { const [result] = await pool.query( ` INSERT INTO bancos ( + idusuarios, descricao, saldo, debito, + investimento, habilitado, dia_vencimento, insert_date, update_date - ) VALUES (?, ?, ?, ?, ?, NOW(), NOW()) + ) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) `, [ + normalizarNumero(_idusuarios, 0), normalizarTextoOuNull(descricao), normalizarNumero(saldo, 0), Number(debito || 0), + Number(investimento || 0), habilitado === undefined || habilitado === null || habilitado === '' ? 1 : Number(habilitado), @@ -222,20 +245,22 @@ async function criarBanco(dados) { ] ); - return buscarBancoPorId(result.insertId); + return buscarBancoPorId(result.insertId, _idusuarios); } -async function atualizarBanco(id, dados) { - const bancoAtual = await buscarBancoPorId(id); +async function atualizarBanco(id, _idusuarios, dados) { + const bancoAtual = await buscarBancoPorId(id, _idusuarios); if (!bancoAtual) { return null; } const { + idusuarios, descricao, saldo, debito, + investimento, habilitado, dia_vencimento } = dados; @@ -244,31 +269,37 @@ async function atualizarBanco(id, dados) { ` UPDATE bancos SET + idusuarios = ?, descricao = ?, saldo = ?, debito = ?, + investimento = ?, habilitado = ?, dia_vencimento = ?, update_date = NOW() WHERE idbancos = ? + AND idusuarios = ? `, [ + normalizarNumero(_idusuarios, 0), normalizarTextoOuNull(descricao), normalizarNumero(saldo, 0), Number(debito || 0), + Number(investimento || 0), habilitado === undefined || habilitado === null || habilitado === '' ? 1 : Number(habilitado), normalizarNumero(dia_vencimento, null), - id + id, + _idusuarios ] ); - return buscarBancoPorId(id); + return buscarBancoPorId(id, _idusuarios); } -async function alterarHabilitadoBanco(id, habilitado) { - const bancoAtual = await buscarBancoPorId(id); +async function alterarHabilitadoBanco(id, idusuarios, habilitado) { + const bancoAtual = await buscarBancoPorId(id, idusuarios); if (!bancoAtual) { return null; @@ -281,18 +312,20 @@ async function alterarHabilitadoBanco(id, habilitado) { habilitado = ?, update_date = NOW() WHERE idbancos = ? + AND idusuarios = ? `, [ Number(habilitado), id, + idusuarios ] ); - return buscarBancoPorId(id); + return buscarBancoPorId(id, idusuarios); } -async function deletarBanco(id) { - const bancoAtual = await buscarBancoPorId(id); +async function deletarBanco(id, idusuarios) { + const bancoAtual = await buscarBancoPorId(id, idusuarios); if (!bancoAtual) { return false; @@ -302,8 +335,9 @@ async function deletarBanco(id) { ` DELETE FROM bancos WHERE idbancos = ? + AND idusuarios = ? `, - [id] + [id, idusuarios] ); return true; diff --git a/financeiro-api/src/modules/bancos/services/bancosRendimentos.service.js b/financeiro-api/src/modules/bancos/services/bancosRendimentos.service.js new file mode 100644 index 0000000..68e700d --- /dev/null +++ b/financeiro-api/src/modules/bancos/services/bancosRendimentos.service.js @@ -0,0 +1,726 @@ +const pool = require('../../../database/mysql'); +const movimentosService = require('../../movimentos/services/movimentos.service'); + +const ID_CENTRO_CUSTO_RENDIMENTO = 33; +const ORIGEM_RENDIMENTO = 'rendimento_investimento'; + +function hojeDataString() { + return new Date().toISOString().slice(0, 10); +} + +function extrairCompetencia(data) { + return String(data || hojeDataString()).slice(0, 7); +} + +function limitarNumero(valor, padrao, minimo, maximo) { + const numero = Number(valor); + + if (!Number.isFinite(numero)) { + return padrao; + } + + if (numero < minimo) return minimo; + if (numero > maximo) return maximo; + + return numero; +} + +function arredondar2(valor) { + return Number(Number(valor || 0).toFixed(2)); +} + +function calcularPercentual(delta, base) { + const valorBase = Number(base || 0); + + if (!valorBase) return 0; + + return Number(((Number(delta || 0) / valorBase) * 100).toFixed(4)); +} + +function resolverPeriodo(filtros = {}) { + const periodoMeses = limitarNumero(filtros.periodoMeses, 12, 1, 60); + + const hoje = new Date(); + const dataFim = filtros.dataFim || hoje.toISOString().slice(0, 10); + + const dataInicioObj = new Date(hoje.getFullYear(), hoje.getMonth() - periodoMeses + 1, 1); + const dataInicioPadrao = dataInicioObj.toISOString().slice(0, 10); + + return { + dataInicio: filtros.dataInicio || dataInicioPadrao, + dataFim, + periodoMeses, + }; +} + +async function listarBancosInvestimento(idusuarios) { + const [rows] = await pool.query( + ` + SELECT + idbancos, + descricao, + saldo, + debito, + investimento, + habilitado, + update_date + FROM bancos + WHERE idusuarios = ? + AND investimento = 1 + AND habilitado = 1 + ORDER BY descricao ASC + `, + [idusuarios] + ); + + return rows.map((item) => ({ + ...item, + saldo: Number(item.saldo || 0), + })); +} + +async function buscarBancoInvestimento(idusuarios, idbancos) { + const [rows] = await pool.query( + ` + SELECT + idbancos, + descricao, + saldo, + debito, + investimento, + habilitado, + update_date + FROM bancos + WHERE idbancos = ? + AND idusuarios = ? + AND investimento = 1 + AND habilitado = 1 + LIMIT 1 + `, + [idbancos, idusuarios] + ); + + return rows[0] || null; +} + +async function buscarUltimasApuracoes(idusuarios) { + const [rows] = await pool.query( + ` + SELECT + bsa.idbancos, + MAX(bsa.insert_date) AS ultima_apuracao + FROM bancos_saldo_auditoria bsa + INNER JOIN bancos b ON b.idbancos = bsa.idbancos + WHERE bsa.idusuarios = ? + AND b.idusuarios = ? + AND b.investimento = 1 + AND bsa.origem = ? + GROUP BY bsa.idbancos + `, + [idusuarios, idusuarios, ORIGEM_RENDIMENTO] + ); + + const mapa = new Map(); + + rows.forEach((row) => { + mapa.set(Number(row.idbancos), row.ultima_apuracao); + }); + + return mapa; +} + +async function buscarHistorico(idusuarios, filtros = {}) { + const { dataInicio, dataFim } = resolverPeriodo(filtros); + + const params = [ + idusuarios, + idusuarios, + ORIGEM_RENDIMENTO, + dataInicio, + dataFim, + ]; + + let filtroBanco = ''; + + if (filtros.idbancos) { + filtroBanco = 'AND bsa.idbancos = ?'; + params.push(Number(filtros.idbancos)); + } + + const [rows] = await pool.query( + ` + SELECT + bsa.idbancossaldoauditoria, + bsa.idbancos, + b.descricao AS banco_descricao, + bsa.idcontasapagar, + bsa.tipo_operacao, + bsa.origem, + bsa.valor_delta, + bsa.saldo_anterior, + bsa.saldo_posterior, + bsa.descricao, + bsa.insert_date + FROM bancos_saldo_auditoria bsa + INNER JOIN bancos b ON b.idbancos = bsa.idbancos + WHERE bsa.idusuarios = ? + AND b.idusuarios = ? + AND b.investimento = 1 + AND bsa.origem = ? + AND DATE(bsa.insert_date) >= ? + AND DATE(bsa.insert_date) <= ? + ${filtroBanco} + ORDER BY bsa.insert_date ASC, bsa.idbancossaldoauditoria ASC + `, + params + ); + + return rows.map((row) => ({ + ...row, + valor_delta: Number(row.valor_delta || 0), + saldo_anterior: Number(row.saldo_anterior || 0), + saldo_posterior: Number(row.saldo_posterior || 0), + percentual: calcularPercentual(row.valor_delta, row.saldo_anterior), + })); +} + +function montarSeriesMensais(historico) { + const porBancoMes = new Map(); + + historico.forEach((item) => { + const mes = String(item.insert_date).slice(0, 7); + const chave = `${item.idbancos}|${mes}`; + + if (!porBancoMes.has(chave)) { + porBancoMes.set(chave, { + idbancos: item.idbancos, + banco_descricao: item.banco_descricao, + competencia: mes, + rendimento: 0, + saldo_final: Number(item.saldo_posterior || 0), + eventos: 0, + }); + } + + const atual = porBancoMes.get(chave); + + atual.rendimento = arredondar2(atual.rendimento + Number(item.valor_delta || 0)); + atual.saldo_final = Number(item.saldo_posterior || atual.saldo_final || 0); + atual.eventos += 1; + }); + + return Array.from(porBancoMes.values()).sort((a, b) => { + if (a.competencia === b.competencia) { + return String(a.banco_descricao).localeCompare(String(b.banco_descricao)); + } + + return String(a.competencia).localeCompare(String(b.competencia)); + }); +} + +function montarResumo(bancos, historico, ranking = [], projecao = null) { + const totalInvestidoAtual = bancos.reduce( + (acc, banco) => acc + Number(banco.saldo || 0), + 0 + ); + + const rendimentoPeriodo = historico.reduce( + (acc, item) => acc + Number(item.valor_delta || 0), + 0 + ); + + const positivos = historico.filter((item) => Number(item.valor_delta || 0) > 0); + + const rendimentoMedioPercentual = positivos.length + ? positivos.reduce((acc, item) => acc + Number(item.percentual || 0), 0) / positivos.length + : 0; + + const melhorBanco = ranking[0] || null; + + return { + totalInvestidoAtual: arredondar2(totalInvestidoAtual), + rendimentoPeriodo: arredondar2(rendimentoPeriodo), + rendimentoMedioPercentual: Number(rendimentoMedioPercentual.toFixed(4)), + quantidadeBancos: bancos.length, + melhorBanco, + projecao12Meses: projecao + ? { + saldoProjetado: projecao.saldo_projetado_total, + rendimentoProjetado: projecao.rendimento_projetado_total, + taxaMediaMensal: projecao.taxa_media_mensal_total, + } + : null, + }; +} + +function agruparRendimentoPorBanco(historico) { + const mapa = new Map(); + + historico.forEach((item) => { + const idbancos = Number(item.idbancos); + const valorDelta = Number(item.valor_delta || 0); + const percentual = Number(item.percentual || 0); + + if (!mapa.has(idbancos)) { + mapa.set(idbancos, { + idbancos, + banco_descricao: item.banco_descricao, + rendimento_total: 0, + quantidade_eventos: 0, + soma_percentual: 0, + maior_rendimento: null, + menor_rendimento: null, + primeira_data: item.insert_date, + ultima_data: item.insert_date, + }); + } + + const atual = mapa.get(idbancos); + + atual.rendimento_total = arredondar2(atual.rendimento_total + valorDelta); + atual.quantidade_eventos += 1; + atual.soma_percentual += percentual; + + if (!atual.maior_rendimento || valorDelta > Number(atual.maior_rendimento.valor_delta || 0)) { + atual.maior_rendimento = { + valor_delta: valorDelta, + percentual, + insert_date: item.insert_date, + }; + } + + if (!atual.menor_rendimento || valorDelta < Number(atual.menor_rendimento.valor_delta || 0)) { + atual.menor_rendimento = { + valor_delta: valorDelta, + percentual, + insert_date: item.insert_date, + }; + } + + if (String(item.insert_date) < String(atual.primeira_data)) { + atual.primeira_data = item.insert_date; + } + + if (String(item.insert_date) > String(atual.ultima_data)) { + atual.ultima_data = item.insert_date; + } + }); + + return Array.from(mapa.values()).map((item) => ({ + ...item, + percentual_medio: item.quantidade_eventos > 0 + ? Number((item.soma_percentual / item.quantidade_eventos).toFixed(4)) + : 0, + })); +} + +function montarRanking(bancos, historico) { + const agrupado = agruparRendimentoPorBanco(historico); + + const saldoPorBanco = new Map(); + + bancos.forEach((banco) => { + saldoPorBanco.set(Number(banco.idbancos), Number(banco.saldo || 0)); + }); + + const ranking = agrupado.map((item) => { + const saldoAtual = saldoPorBanco.get(Number(item.idbancos)) || 0; + + return { + ...item, + saldo_atual: arredondar2(saldoAtual), + participacao_rendimento: 0, + }; + }); + + const totalRendimento = ranking.reduce( + (acc, item) => acc + Number(item.rendimento_total || 0), + 0 + ); + + return ranking + .map((item) => ({ + ...item, + participacao_rendimento: totalRendimento + ? Number(((Number(item.rendimento_total || 0) / totalRendimento) * 100).toFixed(4)) + : 0, + })) + .sort((a, b) => Number(b.rendimento_total || 0) - Number(a.rendimento_total || 0)); +} + +function montarGraficoBarrasMensal(seriesMensais) { + const mapa = new Map(); + + seriesMensais.forEach((item) => { + const competencia = item.competencia; + + if (!mapa.has(competencia)) { + mapa.set(competencia, { + competencia, + total: 0, + }); + } + + const row = mapa.get(competencia); + const nomeBanco = item.banco_descricao; + + row[nomeBanco] = arredondar2(Number(row[nomeBanco] || 0) + Number(item.rendimento || 0)); + row.total = arredondar2(Number(row.total || 0) + Number(item.rendimento || 0)); + }); + + return Array.from(mapa.values()).sort((a, b) => + String(a.competencia).localeCompare(String(b.competencia)) + ); +} + +function montarGraficoSaldoMensal(seriesMensais) { + const mapa = new Map(); + + seriesMensais.forEach((item) => { + const competencia = item.competencia; + + if (!mapa.has(competencia)) { + mapa.set(competencia, { + competencia, + }); + } + + const row = mapa.get(competencia); + row[item.banco_descricao] = Number(item.saldo_final || 0); + }); + + return Array.from(mapa.values()).sort((a, b) => + String(a.competencia).localeCompare(String(b.competencia)) + ); +} + +function montarGraficoRendimentoAcumulado(seriesMensais) { + const bancos = Array.from( + new Set(seriesMensais.map((item) => item.banco_descricao)) + ); + + const competencias = Array.from( + new Set(seriesMensais.map((item) => item.competencia)) + ).sort(); + + const acumuladoPorBanco = new Map(); + + bancos.forEach((nomeBanco) => { + acumuladoPorBanco.set(nomeBanco, 0); + }); + + return competencias.map((competencia) => { + const row = { + competencia, + total: 0, + }; + + bancos.forEach((nomeBanco) => { + const item = seriesMensais.find( + (serie) => + serie.competencia === competencia && + serie.banco_descricao === nomeBanco + ); + + const rendimentoMes = item ? Number(item.rendimento || 0) : 0; + const novoAcumulado = Number(acumuladoPorBanco.get(nomeBanco) || 0) + rendimentoMes; + + acumuladoPorBanco.set(nomeBanco, arredondar2(novoAcumulado)); + row[nomeBanco] = arredondar2(novoAcumulado); + row.total = arredondar2(Number(row.total || 0) + novoAcumulado); + }); + + return row; + }); +} + +function calcularMesesEntreDatas(dataInicio, dataFim) { + if (!dataInicio || !dataFim) return 0; + + const inicio = new Date(dataInicio); + const fim = new Date(dataFim); + + if (Number.isNaN(inicio.getTime()) || Number.isNaN(fim.getTime())) { + return 0; + } + + const anos = fim.getFullYear() - inicio.getFullYear(); + const meses = fim.getMonth() - inicio.getMonth(); + + return Math.max(1, anos * 12 + meses + 1); +} + +function montarProjecao(bancos, seriesMensais, mesesProjetar = 12) { + const resultadoPorBanco = []; + + bancos.forEach((banco) => { + const seriesBanco = seriesMensais + .filter((item) => Number(item.idbancos) === Number(banco.idbancos)) + .sort((a, b) => String(a.competencia).localeCompare(String(b.competencia))); + + const ultimasSeries = seriesBanco.slice(-6); + + const rendimentosValidos = ultimasSeries.filter( + (item) => Number(item.saldo_final || 0) > 0 + ); + + const taxasMensais = rendimentosValidos.map((item) => { + const saldoFinal = Number(item.saldo_final || 0); + const rendimento = Number(item.rendimento || 0); + const saldoBase = saldoFinal - rendimento; + + if (saldoBase <= 0) return 0; + + return rendimento / saldoBase; + }); + + const taxaMediaMensal = taxasMensais.length + ? taxasMensais.reduce((acc, taxa) => acc + taxa, 0) / taxasMensais.length + : 0; + + const saldoAtual = Number(banco.saldo || 0); + const saldoProjetado = saldoAtual * Math.pow(1 + taxaMediaMensal, mesesProjetar); + const rendimentoProjetado = saldoProjetado - saldoAtual; + + resultadoPorBanco.push({ + idbancos: banco.idbancos, + banco_descricao: banco.descricao, + saldo_atual: arredondar2(saldoAtual), + taxa_media_mensal: Number((taxaMediaMensal * 100).toFixed(4)), + meses_base: taxasMensais.length, + meses_projecao: mesesProjetar, + saldo_projetado: arredondar2(saldoProjetado), + rendimento_projetado: arredondar2(rendimentoProjetado), + }); + }); + + const saldoAtualTotal = resultadoPorBanco.reduce( + (acc, item) => acc + Number(item.saldo_atual || 0), + 0 + ); + + const saldoProjetadoTotal = resultadoPorBanco.reduce( + (acc, item) => acc + Number(item.saldo_projetado || 0), + 0 + ); + + const rendimentoProjetadoTotal = saldoProjetadoTotal - saldoAtualTotal; + + return { + meses_projecao: mesesProjetar, + saldo_atual_total: arredondar2(saldoAtualTotal), + saldo_projetado_total: arredondar2(saldoProjetadoTotal), + rendimento_projetado_total: arredondar2(rendimentoProjetadoTotal), + taxa_media_mensal_total: saldoAtualTotal + ? Number(((Math.pow(saldoProjetadoTotal / saldoAtualTotal, 1 / mesesProjetar) - 1) * 100).toFixed(4)) + : 0, + bancos: resultadoPorBanco.sort((a, b) => + Number(b.rendimento_projetado || 0) - Number(a.rendimento_projetado || 0) + ), + observacao: 'Projeção estimada com base no histórico recente de rendimentos. Não representa garantia de rendimento futuro.', + }; +} + +async function buscarDashboard(idusuarios, filtros = {}) { + const bancos = await listarBancosInvestimento(idusuarios); + const ultimasApuracoes = await buscarUltimasApuracoes(idusuarios); + const historico = await buscarHistorico(idusuarios, filtros); + const seriesMensais = montarSeriesMensais(historico); + + const mesesProjecao = limitarNumero(filtros.mesesProjecao, 12, 1, 60); + + const ranking = montarRanking(bancos, historico); + const projecao = montarProjecao(bancos, seriesMensais, mesesProjecao); + + const graficos = { + barrasMensal: montarGraficoBarrasMensal(seriesMensais), + saldoMensal: montarGraficoSaldoMensal(seriesMensais), + rendimentoAcumulado: montarGraficoRendimentoAcumulado(seriesMensais), + }; + + const resumo = montarResumo(bancos, historico, ranking, projecao); + + const bancosComMetricas = bancos.map((banco) => { + const rankingBanco = ranking.find( + (item) => Number(item.idbancos) === Number(banco.idbancos) + ); + + const projecaoBanco = projecao.bancos.find( + (item) => Number(item.idbancos) === Number(banco.idbancos) + ); + + return { + ...banco, + ultima_apuracao: ultimasApuracoes.get(Number(banco.idbancos)) || null, + rendimento_periodo: rankingBanco?.rendimento_total || 0, + percentual_medio_periodo: rankingBanco?.percentual_medio || 0, + projecao: projecaoBanco || null, + }; + }); + + return { + resumo, + bancos: bancosComMetricas, + historico, + seriesMensais, + ranking, + projecao, + graficos, + }; +} + +function validarItemComputar(item) { + const idbancos = Number(item.idbancos); + const novoSaldo = Number(item.novoSaldo); + + if (!Number.isFinite(idbancos) || idbancos <= 0) { + return 'Banco inválido.'; + } + + if (!Number.isFinite(novoSaldo)) { + return 'Novo saldo inválido.'; + } + + if (novoSaldo < 0) { + return 'Novo saldo não pode ser negativo.'; + } + + return null; +} + +async function computarRendimentoBanco(idusuarios, item, opcoes = {}) { + const erro = validarItemComputar(item); + + if (erro) { + return { + ok: false, + idbancos: item.idbancos, + message: erro, + }; + } + + const idbancos = Number(item.idbancos); + const novoSaldo = arredondar2(item.novoSaldo); + + const banco = await buscarBancoInvestimento(idusuarios, idbancos); + + if (!banco) { + return { + ok: false, + idbancos, + message: 'Banco de investimento não encontrado para este usuário.', + }; + } + + const saldoAnterior = arredondar2(banco.saldo); + const valorDelta = arredondar2(novoSaldo - saldoAnterior); + + if (valorDelta === 0) { + return { + ok: true, + ignorado: true, + idbancos, + banco_descricao: banco.descricao, + saldo_anterior: saldoAnterior, + saldo_posterior: novoSaldo, + valor_delta: 0, + percentual: 0, + message: 'Sem alteração de saldo.', + }; + } + + if (valorDelta < 0) { + return { + ok: false, + idbancos, + banco_descricao: banco.descricao, + saldo_anterior: saldoAnterior, + saldo_posterior: novoSaldo, + valor_delta: valorDelta, + percentual: calcularPercentual(valorDelta, saldoAnterior), + message: 'O novo saldo é menor que o saldo atual. Para rendimento, informe apenas crescimento positivo.', + }; + } + + const data = opcoes.data || hojeDataString(); + + const descricaoBase = item.descricao + || opcoes.descricao + || `Rendimento de investimento - ${banco.descricao}`; + + const movimento = await movimentosService.criarMovimento(idusuarios, { + movimento: 'Entrada', + descricao: descricaoBase, + dataentrada: data, + datavencimento: data, + databaixa: data, + valor: valorDelta, + parcela: 1, + parcelas: 1, + status: 'Recebido', + idcentrodecustos: ID_CENTRO_CUSTO_RENDIMENTO, + idbancos, + idclientes: null, + idveiculosdetalhes: null, + idbancos_p: null, + idmovimentosfixos: null, + competencia: extrairCompetencia(data), + origem: ORIGEM_RENDIMENTO, + saldo_processado: 0, + observacao: `Saldo anterior: ${saldoAnterior}. Novo saldo informado: ${novoSaldo}.`, + confirmarDuplicado: opcoes.confirmarDuplicado === true, + }); + + return { + ok: true, + idbancos, + banco_descricao: banco.descricao, + saldo_anterior: saldoAnterior, + saldo_posterior: novoSaldo, + valor_delta: valorDelta, + percentual: calcularPercentual(valorDelta, saldoAnterior), + movimento: movimento.data, + impactoSaldo: movimento.impactoSaldo || [], + }; +} + +async function computarRendimentos(idusuarios, dados = {}) { + const itens = Array.isArray(dados.itens) ? dados.itens : []; + + const resultados = []; + + for (const item of itens) { + const resultado = await computarRendimentoBanco(idusuarios, item, { + data: dados.data, + descricao: dados.descricao, + confirmarDuplicado: dados.confirmarDuplicado, + }); + + resultados.push(resultado); + } + + const processados = resultados.filter((item) => item.ok && !item.ignorado); + const ignorados = resultados.filter((item) => item.ok && item.ignorado); + const erros = resultados.filter((item) => !item.ok); + + return { + processados, + ignorados, + erros, + resumo: { + quantidadeProcessada: processados.length, + quantidadeIgnorada: ignorados.length, + quantidadeErro: erros.length, + totalRendimento: arredondar2( + processados.reduce((acc, item) => acc + Number(item.valor_delta || 0), 0) + ), + }, + }; +} + + +module.exports = { + buscarDashboard, + buscarHistorico, + computarRendimentos, +}; \ No newline at end of file diff --git a/financeiro-api/src/modules/dashboard/controllers/dashboard.controller.js b/financeiro-api/src/modules/dashboard/controllers/dashboard.controller.js index a573fdc..9998fce 100644 --- a/financeiro-api/src/modules/dashboard/controllers/dashboard.controller.js +++ b/financeiro-api/src/modules/dashboard/controllers/dashboard.controller.js @@ -1,8 +1,21 @@ const dashboardService = require('../services/dashboard.service'); +function resolverIdUsuario(req) { + return Number(req.user?.idusuarios || req.user?.id); +} + async function buscarResumo(req, res) { try { - const resultado = await dashboardService.buscarResumoDashboard({ + const idUsuario = resolverIdUsuario(req); + + if (!idUsuario) { + return res.status(401).json({ + ok: false, + message: 'Usuário inválido.', + }); + } + + const resultado = await dashboardService.buscarResumoDashboard(idUsuario, { dataInicio: req.query.dataInicio, dataFim: req.query.dataFim, }); diff --git a/financeiro-api/src/modules/dashboard/services/dashboard.service.js b/financeiro-api/src/modules/dashboard/services/dashboard.service.js index ee41763..875cc17 100644 --- a/financeiro-api/src/modules/dashboard/services/dashboard.service.js +++ b/financeiro-api/src/modules/dashboard/services/dashboard.service.js @@ -52,27 +52,95 @@ function calcularPercentual(valor, base) { return (valorNormalizado / baseNormalizada) * 100; } -async function buscarCardsBancos() { - const [rows] = await pool.query(` - SELECT - COALESCE(SUM(CASE WHEN debito = 1 THEN saldo ELSE 0 END), 0) AS saldoDebito, - COALESCE(SUM(CASE WHEN debito = 0 THEN saldo ELSE 0 END), 0) AS dividaCredito, - COUNT(*) AS quantidadeCarteiras, - SUM(CASE WHEN habilitado = 1 THEN 1 ELSE 0 END) AS carteirasHabilitadas - FROM bancos - `); +async function buscarCardsBancos(idUsuario) { + const [rows] = await pool.query( + ` + SELECT + COALESCE(SUM(CASE + WHEN debito = 1 + AND COALESCE(investimento, 0) = 0 + THEN saldo + ELSE 0 + END), 0) AS saldoDisponivel, + + COALESCE(SUM(CASE + WHEN debito = 1 + AND COALESCE(investimento, 0) = 1 + THEN saldo + ELSE 0 + END), 0) AS totalInvestido, + + COALESCE(SUM(CASE + WHEN debito = 1 + THEN saldo + ELSE 0 + END), 0) AS saldoDebitoTotal, + + COALESCE(SUM(CASE + WHEN debito = 0 + THEN saldo + ELSE 0 + END), 0) AS dividaCredito, + + COUNT(*) AS quantidadeCarteiras, + + SUM(CASE + WHEN habilitado = 1 THEN 1 + ELSE 0 + END) AS carteirasHabilitadas, + + SUM(CASE + WHEN debito = 1 + AND COALESCE(investimento, 0) = 0 + AND habilitado = 1 + THEN 1 + ELSE 0 + END) AS quantidadeContasDisponiveis, + + SUM(CASE + WHEN debito = 1 + AND COALESCE(investimento, 0) = 1 + AND habilitado = 1 + THEN 1 + ELSE 0 + END) AS quantidadeInvestimentos, + + SUM(CASE + WHEN debito = 0 + AND habilitado = 1 + THEN 1 + ELSE 0 + END) AS quantidadeCreditos + FROM bancos + WHERE idusuarios = ? + `, + [idUsuario] + ); const row = rows[0] || {}; + const saldoDisponivel = normalizarNumero(row.saldoDisponivel); + const totalInvestido = normalizarNumero(row.totalInvestido); + const saldoDebitoTotal = normalizarNumero(row.saldoDebitoTotal); + const dividaCredito = normalizarNumero(row.dividaCredito); + return { - saldoDebito: normalizarNumero(row.saldoDebito), - dividaCredito: normalizarNumero(row.dividaCredito), + saldoDisponivel, + totalInvestido, + saldoDebitoTotal, + saldoDebito: saldoDisponivel, // mantém compatibilidade com o front atual + dividaCredito, + patrimonioBruto: saldoDisponivel + totalInvestido, + patrimonioLiquido: saldoDisponivel + totalInvestido - dividaCredito, quantidadeCarteiras: normalizarNumero(row.quantidadeCarteiras), carteirasHabilitadas: normalizarNumero(row.carteirasHabilitadas), + quantidadeContasDisponiveis: normalizarNumero(row.quantidadeContasDisponiveis), + quantidadeInvestimentos: normalizarNumero(row.quantidadeInvestimentos), + quantidadeCreditos: normalizarNumero(row.quantidadeCreditos), }; } -async function buscarCardsMovimentos(dataInicio, dataFim) { +async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) { const [rows] = await pool.query( ` SELECT @@ -144,12 +212,15 @@ async function buscarCardsMovimentos(dataInicio, dataFim) { COUNT(*) AS quantidadeMovimentos FROM contasapagar cp + LEFT JOIN bancos b + ON b.idbancos = cp.idbancos LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos WHERE cp.deleted_at IS NULL + AND b.idusuarios = ? AND DATE(cp.datavencimento) BETWEEN ? AND ? `, - [dataInicio, dataFim] + [idUsuario, dataInicio, dataFim] ); const row = rows[0] || {}; @@ -188,7 +259,7 @@ async function buscarCardsMovimentos(dataInicio, dataFim) { }; } -async function buscarAlertasVencimento() { +async function buscarAlertasVencimento(idUsuario) { const hoje = hojeISO(); const [rows] = await pool.query( @@ -286,9 +357,12 @@ async function buscarAlertasVencimento() { FROM contasapagar cp LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos + LEFT JOIN bancos b + ON b.idbancos = cp.idbancos WHERE cp.deleted_at IS NULL AND cp.status IN ('A pagar', 'A receber') AND cp.datavencimento IS NOT NULL + AND b.idusuarios = ? `, [ hoje, hoje, @@ -304,6 +378,8 @@ async function buscarAlertasVencimento() { hoje, hoje, hoje, hoje, + + idUsuario ] ); @@ -334,7 +410,7 @@ async function buscarAlertasVencimento() { }; } -async function buscarProximosVencimentos() { +async function buscarProximosVencimentos(idUsuario) { const [rows] = await pool.query(` SELECT cp.idcontasapagar, @@ -357,16 +433,18 @@ async function buscarProximosVencimentos() { LEFT JOIN bancos bc ON bc.idbancos = cp.idbancos LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos WHERE cp.deleted_at IS NULL + AND bc.idusuarios = ? AND cp.status IN ('A pagar', 'A receber') AND cp.datavencimento IS NOT NULL ORDER BY DATE(cp.datavencimento) ASC, cp.idcontasapagar ASC LIMIT 10 - `); + `, + [idUsuario]); return rows; } -async function buscarCreditosEmAberto(dataInicio, dataFim) { +async function buscarCreditosEmAberto(idUsuario, dataInicio, dataFim) { const [rows] = await pool.query( ` SELECT @@ -423,7 +501,8 @@ async function buscarCreditosEmAberto(dataInicio, dataFim) { AND cp.deleted_at IS NULL AND cp.status = 'A pagar' AND cp.movimento IN ('Saida', 'Sangria') - WHERE bc.debito = 0 + WHERE bc.idusuarios = ? + AND bc.debito = 0 AND bc.habilitado = 1 GROUP BY bc.idbancos, @@ -443,6 +522,7 @@ async function buscarCreditosEmAberto(dataInicio, dataFim) { dataFim, dataFim, dataFim, + idUsuario, ] ); @@ -460,7 +540,7 @@ async function buscarCreditosEmAberto(dataInicio, dataFim) { })); } -async function buscarCarteiras(dataInicio, dataFim) { +async function buscarCarteiras(idUsuario, dataInicio, dataFim) { const [rows] = await pool.query( ` SELECT @@ -472,21 +552,25 @@ async function buscarCarteiras(dataInicio, dataFim) { bc.dia_vencimento, bc.insert_date, bc.update_date, + bc.investimento, COALESCE(aud.delta_periodo, 0) AS delta_periodo FROM bancos bc LEFT JOIN ( SELECT - idbancos, - COALESCE(SUM(valor_delta), 0) AS delta_periodo - FROM bancos_saldo_auditoria - WHERE DATE(insert_date) BETWEEN ? AND ? - GROUP BY idbancos + bsa.idbancos, + COALESCE(SUM(bsa.valor_delta), 0) AS delta_periodo + FROM bancos_saldo_auditoria bsa + INNER JOIN bancos b2 ON b2.idbancos = bsa.idbancos + WHERE b2.idusuarios = ? + AND DATE(bsa.insert_date) BETWEEN ? AND ? + GROUP BY bsa.idbancos ) aud ON aud.idbancos = bc.idbancos WHERE bc.habilitado = 1 + AND bc.idusuarios = ? ORDER BY bc.debito DESC, bc.descricao ASC `, - [dataInicio, dataFim] + [idUsuario, dataInicio, dataFim, idUsuario] ); return rows.map((item) => { @@ -494,6 +578,7 @@ async function buscarCarteiras(dataInicio, dataFim) { const deltaPeriodo = normalizarNumero(item.delta_periodo); const saldoMesAnterior = saldoAtual - deltaPeriodo; const variacaoValor = saldoAtual - saldoMesAnterior; + const isInvestimento = Number(item.investimento || 0) === 1; const variacaoPercentual = saldoMesAnterior !== 0 ? (variacaoValor / Math.abs(saldoMesAnterior)) * 100 @@ -512,11 +597,16 @@ async function buscarCarteiras(dataInicio, dataFim) { variacao_valor: variacaoValor, variacao_percentual: variacaoPercentual, tendencia_boa: tendenciaBoa, + tipo_carteira: isCredito + ? 'credito' + : isInvestimento + ? 'investimento' + : 'disponivel', }; }); } -async function buscarUltimosMovimentos() { +async function buscarUltimosMovimentos(idUsuario) { const [rows] = await pool.query(` SELECT cp.idcontasapagar, @@ -543,14 +633,16 @@ async function buscarUltimosMovimentos() { LEFT JOIN bancos bc ON bc.idbancos = cp.idbancos LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos WHERE cp.deleted_at IS NULL + AND bc.idusuarios = ? ORDER BY cp.insert_date DESC, cp.idcontasapagar DESC LIMIT 10 - `); + `, + [idUsuario]); return rows; } -async function buscarGraficos(dataInicio, dataFim) { +async function buscarGraficos(idUsuario, dataInicio, dataFim) { const [centrosRows] = await pool.query( ` SELECT @@ -560,9 +652,12 @@ async function buscarGraficos(dataInicio, dataFim) { FROM contasapagar cp LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos + LEFT JOIN bancos b + ON b.idbancos = cp.idbancos WHERE cp.deleted_at IS NULL AND cp.movimento IN ('Saida') AND DATE(cp.datavencimento) BETWEEN ? AND ? + AND b.idusuarios = ? GROUP BY cp.idcentrodecustos, COALESCE(cc.descricao, 'Sem centro de custo') @@ -570,7 +665,7 @@ async function buscarGraficos(dataInicio, dataFim) { ORDER BY valor DESC LIMIT 8 `, - [dataInicio, dataFim] + [dataInicio, dataFim, idUsuario] ); const [entradasSaidasRows] = await pool.query( @@ -595,10 +690,13 @@ async function buscarGraficos(dataInicio, dataFim) { FROM contasapagar cp LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos + LEFT JOIN bancos b + ON b.idbancos = cp.idbancos WHERE cp.deleted_at IS NULL AND DATE(cp.datavencimento) BETWEEN ? AND ? + AND b.idusuarios = ? `, - [dataInicio, dataFim] + [dataInicio, dataFim, idUsuario] ); const entradas = normalizarNumero(entradasSaidasRows[0]?.entradas); @@ -627,13 +725,38 @@ async function buscarGraficos(dataInicio, dataFim) { }; } -async function buscarResumoDashboard(filtros = {}) { +async function buscarCardsInvestimentos(idUsuario, dataInicio, dataFim) { + const [rows] = await pool.query( + ` + SELECT + COALESCE(SUM(bsa.valor_delta), 0) AS rendimentoInvestimentosPeriodo, + COUNT(*) AS quantidadeRendimentosPeriodo + FROM bancos_saldo_auditoria bsa + INNER JOIN bancos b ON b.idbancos = bsa.idbancos + WHERE b.idusuarios = ? + AND b.investimento = 1 + AND bsa.origem = 'rendimento_investimento' + AND DATE(bsa.insert_date) BETWEEN ? AND ? + `, + [idUsuario, dataInicio, dataFim] + ); + + const row = rows[0] || {}; + + return { + rendimentoInvestimentosPeriodo: normalizarNumero(row.rendimentoInvestimentosPeriodo), + quantidadeRendimentosPeriodo: normalizarNumero(row.quantidadeRendimentosPeriodo), + }; +} + +async function buscarResumoDashboard(idUsuario, filtros = {}) { const dataInicio = normalizarDataISO(filtros.dataInicio, inicioMesAtualISO()); const dataFim = normalizarDataISO(filtros.dataFim, fimMesAtualISO()); const [ cardsBancos, cardsMovimentos, + cardsInvestimentos, alertasVencimento, proximosVencimentos, creditos, @@ -641,14 +764,15 @@ async function buscarResumoDashboard(filtros = {}) { ultimosMovimentos, graficos, ] = await Promise.all([ - buscarCardsBancos(), - buscarCardsMovimentos(dataInicio, dataFim), - buscarAlertasVencimento(), - buscarProximosVencimentos(), - buscarCreditosEmAberto(dataInicio, dataFim), - buscarCarteiras(dataInicio, dataFim), - buscarUltimosMovimentos(), - buscarGraficos(dataInicio, dataFim), + buscarCardsBancos(idUsuario), + buscarCardsMovimentos(idUsuario, dataInicio, dataFim), + buscarCardsInvestimentos(idUsuario, dataInicio, dataFim), + buscarAlertasVencimento(idUsuario), + buscarProximosVencimentos(idUsuario), + buscarCreditosEmAberto(idUsuario, dataInicio, dataFim), + buscarCarteiras(idUsuario, dataInicio, dataFim), + buscarUltimosMovimentos(idUsuario), + buscarGraficos(idUsuario, dataInicio, dataFim), ]); return { @@ -660,6 +784,7 @@ async function buscarResumoDashboard(filtros = {}) { cards: { ...cardsBancos, ...cardsMovimentos, + ...cardsInvestimentos, ...alertasVencimento, }, diff --git a/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js b/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js index e38b17d..201d305 100644 --- a/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js +++ b/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js @@ -124,6 +124,7 @@ async function listar(req, res) { limite: req.query.limite, offset: req.query.offset, page: req.query.page, + busca: req.query.busca, movimento: req.query.movimento, status: req.query.status, idbancos: req.query.idbancos, @@ -139,10 +140,11 @@ async function listar(req, res) { dataCampo: req.query.dataCampo, dataInicio: req.query.dataInicio, dataFim: req.query.dataFim, - busca: req.query.busca, incluirExcluidos: req.query.incluirExcluidos, orderBy: req.query.orderBy, orderDirection: req.query.orderDirection, + + idusuarios: req.user.idusuarios || req.user.id, }); return res.json({ @@ -172,7 +174,9 @@ async function buscarPorId(req, res) { }); } - const movimento = await movimentosService.buscarMovimentoPorId(id); + const movimento = await movimentosService.buscarMovimentoPorId(id, { + idUsuario: req.user.idusuarios || req.user.id, + }); if (!movimento) { return res.status(404).json({ diff --git a/financeiro-api/src/modules/movimentos/services/movimentos.service.js b/financeiro-api/src/modules/movimentos/services/movimentos.service.js index 836abbe..dd9aca8 100644 --- a/financeiro-api/src/modules/movimentos/services/movimentos.service.js +++ b/financeiro-api/src/modules/movimentos/services/movimentos.service.js @@ -206,6 +206,11 @@ function montarWhereMovimentos(filtros = {}) { where.push('cp.deleted_at IS NULL'); } + if (filtros.idusuarios) { + where.push('b.idusuarios = ?'); + params.push(Number(filtros.idusuarios)); + } + if (filtros.movimento) { where.push('cp.movimento = ?'); params.push(filtros.movimento); @@ -411,6 +416,15 @@ async function listarMovimentos(filtros = {}) { async function buscarMovimentoPorId(id, opcoes = {}) { const executor = opcoes.connection || pool; const incluirExcluidos = opcoes.incluirExcluidos === true; + const idUsuario = opcoes.idUsuario || null; + + const params = [id]; + let filtroUsuario = ''; + + if (idUsuario) { + filtroUsuario = 'AND b.idusuarios = ?'; + params.push(Number(idUsuario)); + } const [rows] = await executor.query( ` @@ -419,26 +433,37 @@ async function buscarMovimentoPorId(id, opcoes = {}) { ${FROM_MOVIMENTO_JOIN} WHERE cp.idcontasapagar = ? ${incluirExcluidos ? '' : 'AND cp.deleted_at IS NULL'} + ${filtroUsuario} LIMIT 1 `, - [id] + params ); return rows[0] || null; } -async function buscarMovimentoPorIdParaUpdate(connection, id) { +async function buscarMovimentoPorIdParaUpdate(connection, id, idUsuario = null) { + const params = [id]; + let filtroUsuario = ''; + + if (idUsuario) { + filtroUsuario = 'AND b.idusuarios = ?'; + params.push(Number(idUsuario)); + } + const [rows] = await connection.query( ` SELECT cp.* FROM contasapagar cp + INNER JOIN bancos b ON b.idbancos = cp.idbancos WHERE cp.idcontasapagar = ? - AND cp.deleted_at IS NULL + AND cp.deleted_at IS NULL + ${filtroUsuario} LIMIT 1 FOR UPDATE `, - [id] + params ); return rows[0] || null; @@ -713,7 +738,7 @@ function montarParcelas(idUsuario, dados) { } async function aplicarSaldoMovimento(connection, movimento, contexto) { - const deltas = await movimentosSaldoService.calcularImpactoMovimento(connection, movimento); + const deltas = await movimentosSaldoService.calcularImpactoMovimento(connection, movimento, contexto); if (deltas.length === 0) { return { @@ -826,7 +851,7 @@ async function atualizarMovimento(id, idUsuario, dados) { try { await connection.beginTransaction(); - const movimentoAntigo = await buscarMovimentoPorIdParaUpdate(connection, id); + const movimentoAntigo = await buscarMovimentoPorIdParaUpdate(connection, id, idUsuario); if (!movimentoAntigo) { await connection.rollback(); @@ -845,7 +870,8 @@ async function atualizarMovimento(id, idUsuario, dados) { if (Number(movimentoAntigo.saldo_processado) === 1) { const deltasAntigos = await movimentosSaldoService.calcularImpactoMovimento( connection, - movimentoAntigo + movimentoAntigo, + { idusuarios: idUsuario } ); const impactosReversao = await movimentosSaldoService.aplicarDeltasSaldo( @@ -924,7 +950,7 @@ async function deletarMovimento(id, idUsuario) { try { await connection.beginTransaction(); - const movimentoAntigo = await buscarMovimentoPorIdParaUpdate(connection, id); + const movimentoAntigo = await buscarMovimentoPorIdParaUpdate(connection, id, idUsuario); if (!movimentoAntigo) { await connection.rollback(); @@ -939,7 +965,8 @@ async function deletarMovimento(id, idUsuario) { if (Number(movimentoAntigo.saldo_processado) === 1) { const deltasAntigos = await movimentosSaldoService.calcularImpactoMovimento( connection, - movimentoAntigo + movimentoAntigo, + { idusuarios: idUsuario } ); const impactosReversao = await movimentosSaldoService.aplicarDeltasSaldo( diff --git a/financeiro-api/src/modules/movimentos/services/movimentosSaldo.service.js b/financeiro-api/src/modules/movimentos/services/movimentosSaldo.service.js index 414b25b..d18cac3 100644 --- a/financeiro-api/src/modules/movimentos/services/movimentosSaldo.service.js +++ b/financeiro-api/src/modules/movimentos/services/movimentosSaldo.service.js @@ -12,42 +12,64 @@ function bancoEhCredito(banco) { return Number(banco?.debito) === 0; } -async function buscarBanco(connection, idbancos) { +async function buscarBanco(connection, idbancos, idusuarios = null) { if (!idbancos) return null; + const params = [idbancos]; + let filtroUsuario = ''; + + if (idusuarios) { + filtroUsuario = 'AND idusuarios = ?'; + params.push(idusuarios); + } + const [rows] = await connection.query( ` SELECT idbancos, descricao, saldo, - debito + debito, + investimento, + idusuarios FROM bancos WHERE idbancos = ? + ${filtroUsuario} LIMIT 1 `, - [idbancos] + params ); return rows[0] || null; } -async function buscarBancoParaUpdate(connection, idbancos) { +async function buscarBancoParaUpdate(connection, idbancos, idusuarios = null) { if (!idbancos) return null; + const params = [idbancos]; + let filtroUsuario = ''; + + if (idusuarios) { + filtroUsuario = 'AND idusuarios = ?'; + params.push(idusuarios); + } + const [rows] = await connection.query( ` SELECT idbancos, descricao, saldo, - debito + debito, + investimento, + idusuarios FROM bancos WHERE idbancos = ? + ${filtroUsuario} LIMIT 1 FOR UPDATE `, - [idbancos] + params ); return rows[0] || null; @@ -145,9 +167,9 @@ function calcularDeltasParaMovimento(movimento, bancoOrigem, bancoDestino = null return deltas; } -async function calcularImpactoMovimento(connection, movimento) { - const bancoOrigem = await buscarBanco(connection, movimento.idbancos); - const bancoDestino = await buscarBanco(connection, movimento.idbancos_p); +async function calcularImpactoMovimento(connection, movimento, contexto = {}) { + const bancoOrigem = await buscarBanco(connection, movimento.idbancos, contexto.idusuarios); + const bancoDestino = await buscarBanco(connection, movimento.idbancos_p, contexto.idusuarios); return calcularDeltasParaMovimento(movimento, bancoOrigem, bancoDestino); } @@ -164,7 +186,7 @@ async function aplicarDeltasSaldo(connection, deltas, contexto = {}) { const impactos = []; for (const delta of deltas) { - const banco = await buscarBancoParaUpdate(connection, delta.idbancos); + const banco = await buscarBancoParaUpdate(connection, delta.idbancos, contexto.idusuarios); if (!banco) { continue; diff --git a/financeiro-api/src/modules/quitacoesCredito/controllers/quitacoesCredito.controller.js b/financeiro-api/src/modules/quitacoesCredito/controllers/quitacoesCredito.controller.js index ee0867a..d5f42c0 100644 --- a/financeiro-api/src/modules/quitacoesCredito/controllers/quitacoesCredito.controller.js +++ b/financeiro-api/src/modules/quitacoesCredito/controllers/quitacoesCredito.controller.js @@ -1,5 +1,9 @@ const quitacoesCreditoService = require('../services/quitacoesCredito.service'); +function resolverIdUsuario(req) { + return Number(req.user?.idusuarios || req.user?.id); +} + function tratarErro(error, res, mensagemPadrao) { if (error?.statusCode) { return res.status(error.statusCode).json({ @@ -43,7 +47,16 @@ function validarCriacao(dados) { async function listarBancosCredito(req, res) { try { - const bancos = await quitacoesCreditoService.listarBancosCredito(); + const idUsuario = resolverIdUsuario(req); + + if (!idUsuario) { + return res.status(401).json({ + ok: false, + message: 'Usuário inválido.', + }); + } + + const bancos = await quitacoesCreditoService.listarBancosCredito(idUsuario); return res.json({ ok: true, @@ -57,7 +70,16 @@ async function listarBancosCredito(req, res) { async function listarBancosPagamento(req, res) { try { - const bancos = await quitacoesCreditoService.listarBancosPagamento(); + const idUsuario = resolverIdUsuario(req); + + if (!idUsuario) { + return res.status(401).json({ + ok: false, + message: 'Usuário inválido.', + }); + } + + const bancos = await quitacoesCreditoService.listarBancosPagamento(idUsuario); return res.json({ ok: true, @@ -71,7 +93,16 @@ async function listarBancosPagamento(req, res) { async function preview(req, res) { try { - const resultado = await quitacoesCreditoService.previewQuitacao({ + const idUsuario = resolverIdUsuario(req); + + if (!idUsuario) { + return res.status(401).json({ + ok: false, + message: 'Usuário inválido.', + }); + } + + const resultado = await quitacoesCreditoService.previewQuitacao(idUsuario, { idBancoCredito: req.query.idBancoCredito || req.query.idbancos, dataInicio: req.query.dataInicio, dataFim: req.query.dataFim, @@ -102,7 +133,16 @@ async function criar(req, res) { }); } - const resultado = await quitacoesCreditoService.criarQuitacaoCredito(req.user.id, req.body); + const idUsuario = resolverIdUsuario(req); + + if (!idUsuario) { + return res.status(401).json({ + ok: false, + message: 'Usuário inválido.', + }); + } + + const resultado = await quitacoesCreditoService.criarQuitacaoCredito(idUsuario, req.body); return res.status(201).json({ ok: true, @@ -120,7 +160,16 @@ async function criar(req, res) { async function listarHistorico(req, res) { try { - const resultado = await quitacoesCreditoService.listarHistorico({ + const idUsuario = resolverIdUsuario(req); + + if (!idUsuario) { + return res.status(401).json({ + ok: false, + message: 'Usuário inválido.', + }); + } + + const resultado = await quitacoesCreditoService.listarHistorico(idUsuario, { limite: req.query.limite, offset: req.query.offset, page: req.query.page, @@ -154,7 +203,16 @@ async function buscarPorId(req, res) { }); } - const quitacao = await quitacoesCreditoService.buscarQuitacaoPorId(id); + const idUsuario = resolverIdUsuario(req); + + if (!idUsuario) { + return res.status(401).json({ + ok: false, + message: 'Usuário inválido.', + }); + } + + const quitacao = await quitacoesCreditoService.buscarQuitacaoPorId(id, idUsuario); if (!quitacao) { return res.status(404).json({ diff --git a/financeiro-api/src/modules/quitacoesCredito/services/quitacoesCredito.service.js b/financeiro-api/src/modules/quitacoesCredito/services/quitacoesCredito.service.js index 46cdc3c..be3359c 100644 --- a/financeiro-api/src/modules/quitacoesCredito/services/quitacoesCredito.service.js +++ b/financeiro-api/src/modules/quitacoesCredito/services/quitacoesCredito.service.js @@ -112,7 +112,7 @@ function idsUnicos(ids = []) { )]; } -async function buscarBancoParaUpdate(connection, idbancos) { +async function buscarBancoParaUpdate(connection, idbancos, idUsuario) { const [rows] = await connection.query( ` SELECT @@ -120,13 +120,15 @@ async function buscarBancoParaUpdate(connection, idbancos) { descricao, saldo, debito, - habilitado + habilitado, + idusuarios FROM bancos WHERE idbancos = ? + AND idusuarios = ? LIMIT 1 FOR UPDATE `, - [idbancos] + [idbancos, idUsuario] ); return rows[0] || null; @@ -154,7 +156,7 @@ async function inserirAuditoriaConta(connection, dados) { ); } -async function listarBancosCredito() { +async function listarBancosCredito(idUsuario) { const [rows] = await pool.query( ` SELECT @@ -166,16 +168,18 @@ async function listarBancosCredito() { insert_date, update_date FROM bancos - WHERE debito = 0 - AND COALESCE(habilitado, 1) = 1 + WHERE idusuarios = ? + AND debito = 0 + AND COALESCE(habilitado, 1) = 1 ORDER BY descricao ASC, idbancos ASC - ` + `, + [idUsuario] ); return rows; } -async function listarBancosPagamento() { +async function listarBancosPagamento(idUsuario) { const [rows] = await pool.query( ` SELECT @@ -187,10 +191,12 @@ async function listarBancosPagamento() { insert_date, update_date FROM bancos - WHERE debito = 1 - AND COALESCE(habilitado, 1) = 1 + WHERE idusuarios = ? + AND debito = 1 + AND COALESCE(habilitado, 1) = 1 ORDER BY descricao ASC, idbancos ASC - ` + `, + [idUsuario] ); return rows; @@ -202,9 +208,13 @@ function montarWhereContasPreview(filtros = {}) { "cp.movimento = 'Saida'", "cp.status = 'A pagar'", 'cp.idbancos = ?', + 'b.idusuarios = ?', ]; - const params = [Number(filtros.idBancoCredito)]; + const params = [ + Number(filtros.idBancoCredito), + Number(filtros.idUsuario), + ]; if (filtros.dataInicio) { where.push('DATE(cp.datavencimento) >= ?'); @@ -237,7 +247,7 @@ function montarWhereContasPreview(filtros = {}) { }; } -async function previewQuitacao(filtros = {}) { +async function previewQuitacao(idUsuario, filtros = {}) { const idBancoCredito = normalizarNumeroOuNull(filtros.idBancoCredito || filtros.idbancos); if (!idBancoCredito) { @@ -257,12 +267,14 @@ async function previewQuitacao(filtros = {}) { descricao, saldo, debito, - habilitado + habilitado, + idusuarios FROM bancos WHERE idbancos = ? + AND idusuarios = ? LIMIT 1 `, - [idBancoCredito] + [idBancoCredito, idUsuario] ); const bancoCredito = bancoRows[0] || null; @@ -278,6 +290,7 @@ async function previewQuitacao(filtros = {}) { const { whereSql, params } = montarWhereContasPreview({ ...filtros, idBancoCredito, + idUsuario, }); const [rows] = await pool.query( @@ -339,7 +352,7 @@ async function previewQuitacao(filtros = {}) { }; } -async function buscarContasSelecionadasParaUpdate(connection, ids) { +async function buscarContasSelecionadasParaUpdate(connection, ids, idBancoCredito, idUsuario) { if (!ids.length) return []; const placeholders = ids.map(() => '?').join(', '); @@ -349,16 +362,23 @@ async function buscarContasSelecionadasParaUpdate(connection, ids) { SELECT cp.* FROM contasapagar cp + INNER JOIN bancos b ON b.idbancos = cp.idbancos WHERE cp.idcontasapagar IN (${placeholders}) + AND cp.idbancos = ? + AND b.idusuarios = ? FOR UPDATE `, - ids + [ + ...ids, + idBancoCredito, + idUsuario, + ] ); return rows; } -async function buscarContasDetalhadasPorIds(connection, ids) { +async function buscarContasDetalhadasPorIds(connection, ids, idUsuario) { if (!ids.length) return []; const placeholders = ids.map(() => '?').join(', '); @@ -369,9 +389,10 @@ async function buscarContasDetalhadasPorIds(connection, ids) { ${CAMPOS_CONTA_SELECT} ${FROM_CONTA_JOIN} WHERE cp.idcontasapagar IN (${placeholders}) + AND b.idusuarios = ? ORDER BY cp.datavencimento ASC, cp.idcontasapagar ASC `, - ids + [...ids, idUsuario] ); return rows; @@ -405,8 +426,8 @@ async function criarQuitacaoCredito(idUsuario, dados = {}) { try { await connection.beginTransaction(); - const bancoCredito = await buscarBancoParaUpdate(connection, idBancoCredito); - const bancoPagamento = await buscarBancoParaUpdate(connection, idBancoPagamento); + const bancoCredito = await buscarBancoParaUpdate(connection, idBancoCredito, idUsuario); + const bancoPagamento = await buscarBancoParaUpdate(connection, idBancoPagamento, idUsuario); if (!bancoCredito) { throw criarErro('Banco de crédito não encontrado.', 404); @@ -432,7 +453,12 @@ async function criarQuitacaoCredito(idUsuario, dados = {}) { throw criarErro('O banco de pagamento está desabilitado.'); } - const contasAntes = await buscarContasSelecionadasParaUpdate(connection, idsContas); + const contasAntes = await buscarContasSelecionadasParaUpdate( + connection, + idsContas, + idBancoCredito, + idUsuario + ); if (contasAntes.length !== idsContas.length) { const encontrados = new Set(contasAntes.map((conta) => Number(conta.idcontasapagar))); @@ -538,7 +564,7 @@ async function criarQuitacaoCredito(idUsuario, dados = {}) { ] ); - const contasDepois = await buscarContasDetalhadasPorIds(connection, idsContas); + const contasDepois = await buscarContasDetalhadasPorIds(connection, idsContas, idUsuario); const contasDepoisPorId = new Map(contasDepois.map((conta) => [Number(conta.idcontasapagar), conta])); for (const contaAntes of contasAntes) { @@ -615,15 +641,19 @@ async function criarQuitacaoCredito(idUsuario, dados = {}) { } } -async function listarHistorico(filtros = {}) { +async function listarHistorico(idUsuario, filtros = {}) { const limite = limitarNumero(filtros.limite, 20, 1, 100); const page = limitarNumero(filtros.page, 1, 1, 999999); const offset = filtros.offset !== undefined ? limitarNumero(filtros.offset, 0, 0, 999999999) : (page - 1) * limite; - const where = ['cq.deleted_at IS NULL']; - const params = []; + const where = [ + 'cq.deleted_at IS NULL', + 'bp.idusuarios = ?', + 'bc.idusuarios = ?', + ]; + const params = [idUsuario, idUsuario]; if (filtros.idBancoCredito) { where.push('cq.idbancos_p = ?'); @@ -746,7 +776,7 @@ async function listarHistorico(filtros = {}) { }; } -async function buscarQuitacaoPorId(id) { +async function buscarQuitacaoPorId(id, idUsuario) { const [quitacaoRows] = await pool.query( ` SELECT @@ -768,9 +798,11 @@ async function buscarQuitacaoPorId(id) { LEFT JOIN bancos bc ON bc.idbancos = cq.idbancos_p LEFT JOIN usuarios u ON u.idusuarios = cq.idusuarios WHERE cq.idcontasquitadas = ? + AND bp.idusuarios = ? + AND bc.idusuarios = ? LIMIT 1 `, - [id] + [id, idUsuario, idUsuario] ); const quitacao = quitacaoRows[0] || null; @@ -786,9 +818,10 @@ async function buscarQuitacaoPorId(id) { ${FROM_CONTA_JOIN} INNER JOIN contasquitadasref cqr ON cqr.idcontasapagar = cp.idcontasapagar WHERE cqr.idcontasquitadas = ? + AND b.idusuarios = ? ORDER BY cp.datavencimento ASC, cp.idcontasapagar ASC `, - [id] + [id, idUsuario] ); return { diff --git a/financeiro-api/src/modules/referencias/services/referencias.service.js b/financeiro-api/src/modules/referencias/services/referencias.service.js index f1385a4..bbff766 100644 --- a/financeiro-api/src/modules/referencias/services/referencias.service.js +++ b/financeiro-api/src/modules/referencias/services/referencias.service.js @@ -4,14 +4,27 @@ async function listarBancos() { const [rows] = await pool.query( ` SELECT - idbancos AS id, - descricao, - saldo, - debito, - dia_vencimento - FROM bancos - WHERE habilitado = 1 - ORDER BY descricao ASC + b.idbancos AS id, + b.idbancos, + b.descricao AS banco_descricao, + u.idusuarios, + u.nome AS usuario_nome, + CONCAT( + COALESCE(u.nome, 'Usuário sem nome'), + ' - ', + b.descricao + ) AS descricao, + b.saldo, + b.debito, + b.investimento, + b.dia_vencimento + FROM bancos b + LEFT JOIN usuarios u + ON u.idusuarios = b.idusuarios + WHERE b.habilitado = 1 + ORDER BY + b.descricao ASC, + u.nome ASC ` ); diff --git a/financeiro-web/src/app/router.tsx b/financeiro-web/src/app/router.tsx index 8aaa9e7..d7d7571 100644 --- a/financeiro-web/src/app/router.tsx +++ b/financeiro-web/src/app/router.tsx @@ -12,6 +12,7 @@ import { NotFoundPage } from '../pages/NotFoundPage'; import { BancosPage } from '../features/bancos/pages/BancosPage'; import { NovoBancoPage } from '../features/bancos/pages/NovoBancoPage'; import { EditarBancoPage } from '../features/bancos/pages/EditarBancoPage'; +import { RendimentosPage } from '../features/bancos/pages/RendimentosPage'; import { ClientesPage } from '../features/clientes/pages/ClientesPage'; import { NovoClientePage } from '../features/clientes/pages/NovoClientePage'; import { EditarClientePage } from '../features/clientes/pages/EditarClientePage'; @@ -73,6 +74,10 @@ export const routes: RouteObject[] = [ path: 'bancos/:id/editar', element: , }, + { + path: 'rendimentos', + element: , + }, { path: 'clientes', element: , diff --git a/financeiro-web/src/components/layout/Sidebar.tsx b/financeiro-web/src/components/layout/Sidebar.tsx index 8baba0b..4d95b80 100644 --- a/financeiro-web/src/components/layout/Sidebar.tsx +++ b/financeiro-web/src/components/layout/Sidebar.tsx @@ -11,6 +11,7 @@ import DashboardIcon from '@mui/icons-material/Dashboard'; import SwapHorizIcon from '@mui/icons-material/SwapHoriz'; import AssessmentIcon from '@mui/icons-material/Assessment'; import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; +import SavingsIcon from '@mui/icons-material/Savings'; import SettingsIcon from '@mui/icons-material/Settings'; import PeopleAltIcon from '@mui/icons-material/PeopleAlt'; import AccountTreeIcon from '@mui/icons-material/AccountTree'; @@ -49,6 +50,11 @@ const menuItems = [ path: '/bancos', icon: , }, + { + label: 'Rendimentos', + path: '/rendimentos', + icon: , + }, { label: 'Centros de custo', path: '/centros-custo', diff --git a/financeiro-web/src/features/bancos/components/BancoForm.tsx b/financeiro-web/src/features/bancos/components/BancoForm.tsx index c9afff8..af17dd6 100644 --- a/financeiro-web/src/features/bancos/components/BancoForm.tsx +++ b/financeiro-web/src/features/bancos/components/BancoForm.tsx @@ -110,7 +110,7 @@ export function BancoForm({ mode, initialData }: BancoFormProps) { const [descricao, setDescricao] = useState(''); const [saldo, setSaldo] = useState('0'); - const [debito, setDebito] = useState('1'); + const [tipoCarteira, setTipoCarteira] = useState<'debito' | 'credito' | 'investimento'>('debito'); const [habilitado, setHabilitado] = useState('1'); const [diaVencimento, setDiaVencimento] = useState(''); @@ -124,17 +124,24 @@ export function BancoForm({ mode, initialData }: BancoFormProps) { setDescricao(initialData.descricao || ''); setSaldo(String(initialData.saldo ?? 0)); - setDebito(String(initialData.debito ?? 1)); setHabilitado(String(initialData.habilitado ?? 1)); setDiaVencimento( initialData.dia_vencimento ? String(initialData.dia_vencimento) : '' ); + + if (Number(initialData.investimento) === 1) { + setTipoCarteira('investimento'); + } else if (Number(initialData.debito) === 0) { + setTipoCarteira('credito'); + } else { + setTipoCarteira('debito'); + } }, [initialData]); function limparFormulario() { setDescricao(''); setSaldo('0'); - setDebito('1'); + setTipoCarteira('debito'); setHabilitado('1'); setDiaVencimento(''); } @@ -152,7 +159,7 @@ export function BancoForm({ mode, initialData }: BancoFormProps) { return; } - if (Number(debito) === 0) { + if (tipoCarteira === 'credito') { const dia = Number(diaVencimento); if (!diaVencimento || !Number.isInteger(dia) || dia < 1 || dia > 31) { @@ -169,9 +176,10 @@ export function BancoForm({ mode, initialData }: BancoFormProps) { const payload: CriarBancoRequest = { descricao: descricao.trim(), saldo: Number(saldo || 0), - debito: Number(debito), + debito: tipoCarteira === 'credito' ? 0 : 1, + investimento: tipoCarteira === 'investimento' ? 1 : 0, habilitado: Number(habilitado || 1), - dia_vencimento: Number(debito) === 0 ? Number(diaVencimento) : null, + dia_vencimento: tipoCarteira === 'credito' ? Number(diaVencimento) : null, }; if (isEdit) { @@ -281,16 +289,19 @@ export function BancoForm({ mode, initialData }: BancoFormProps) { setDebito(event.target.value)} + value={tipoCarteira} + onChange={(event) => + setTipoCarteira(event.target.value as 'debito' | 'credito' | 'investimento') + } fullWidth sx={fieldSx} > - Débito - Crédito + Débito + Crédito + Investimento - {Number(debito) === 0 && ( + {tipoCarteira === 'credito' && ( - {Number(debito) === 1 ? 'Débito' : 'Crédito'} + {tipoCarteira === 'investimento' + ? 'Investimento' + : tipoCarteira === 'credito' + ? 'Crédito' + : 'Débito'} - {Number(debito) === 0 && ( + {tipoCarteira === 'credito' && ( Dia de vencimento diff --git a/financeiro-web/src/features/bancos/pages/BancosPage.tsx b/financeiro-web/src/features/bancos/pages/BancosPage.tsx index 6cdc14e..ce1a853 100644 --- a/financeiro-web/src/features/bancos/pages/BancosPage.tsx +++ b/financeiro-web/src/features/bancos/pages/BancosPage.tsx @@ -61,12 +61,14 @@ function formatarData(data: string | null) { return new Date(`${String(data).slice(0, 10)}T00:00:00`).toLocaleDateString('pt-BR'); } -function tipoBancoLabel(debito: number) { - return Number(debito) === 1 ? 'Débito' : 'Crédito'; +function tipoBancoLabel(item: Banco) { + if (Number(item.investimento) === 1) return 'Investimento'; + return Number(item.debito) === 1 ? 'Débito' : 'Crédito'; } -function tipoBancoColor(debito: number) { - return Number(debito) === 1 ? 'success' : 'info'; +function tipoBancoColor(item: Banco) { + if (Number(item.investimento) === 1) return 'secondary'; + return Number(item.debito) === 1 ? 'success' : 'info'; } function statusBancoLabel(habilitado: number) { @@ -96,8 +98,9 @@ export function BancosPage() { const [summary, setSummary] = useState({ quantidade: 0, saldoTotal: 0, - saldoCarteiras: 0, + saldoCredito: 0, saldoDebito: 0, + saldoInvestimento: 0, }); const [alterandoStatusId, setAlterandoStatusId] = useState(null); @@ -108,7 +111,7 @@ export function BancosPage() { const [page, setPage] = useState(1); const [busca, setBusca] = useState(''); - const [debito, setDebito] = useState(''); + const [tipoFiltro, setTipoFiltro] = useState<'' | 'credito' | 'debito' | 'investimento'>(''); const [habilitado, setHabilitado] = useState(''); const [diaVencimento, setDiaVencimento] = useState(''); const [orderBy, setOrderBy] = useState('descricao'); @@ -121,12 +124,12 @@ export function BancosPage() { let count = 0; if (busca.trim()) count += 1; - if (debito !== '') count += 1; + if (tipoFiltro !== '') count += 1; if (habilitado !== '') count += 1; if (diaVencimento !== '') count += 1; return count; - }, [busca, debito, habilitado, diaVencimento]); + }, [busca, tipoFiltro, habilitado, diaVencimento]); async function carregarBancos(pageToLoad = page) { try { @@ -137,7 +140,18 @@ export function BancosPage() { limite: LIMITE_PADRAO, page: pageToLoad, busca: busca.trim() || undefined, - debito, + debito: + tipoFiltro === 'credito' + ? 0 + : tipoFiltro === 'debito' + ? 1 + : undefined, + investimento: + tipoFiltro === 'investimento' + ? 1 + : tipoFiltro === 'credito' || tipoFiltro === 'debito' + ? 0 + : undefined, habilitado, dia_vencimento: diaVencimento, orderBy, @@ -168,7 +182,7 @@ export function BancosPage() { function limparFiltros() { setBusca(''); - setDebito(''); + setTipoFiltro(''); setHabilitado(''); setDiaVencimento(''); setOrderBy('descricao'); @@ -303,7 +317,7 @@ export function BancosPage() { display="grid" gridTemplateColumns={{ xs: '1fr', - md: 'repeat(4, 1fr)', + md: 'repeat(5, 1fr)', }} gap={{ xs: 2, md: 2.5 }} marginBottom={{ xs: 2.5, md: 3 }} @@ -337,10 +351,10 @@ export function BancosPage() { - Dívida em crédito + Saldo em crédito - - {formatarValor(summary.saldoCarteiras)} + + {formatarValor(summary.saldoCredito)} @@ -350,11 +364,22 @@ export function BancosPage() { Saldo em débito - + {formatarValor(summary.saldoDebito)} + + + + + Investimentos + + + {formatarValor(summary.saldoInvestimento)} + + + @@ -382,7 +407,7 @@ export function BancosPage() { display="grid" gridTemplateColumns={{ xs: '1fr', - md: 'repeat(4, 1fr)', + md: 'repeat(5, 1fr)', }} gap={{ xs: 2, md: 2.25 }} > @@ -397,15 +422,16 @@ export function BancosPage() { - setDebito(event.target.value === '' ? '' : Number(event.target.value)) + setTipoFiltro(event.target.value as '' | 'credito' | 'debito' | 'investimento') } fullWidth > Todos - Crédito - Débito + Crédito + Débito + Investimento @@ -550,9 +580,9 @@ export function BancosPage() { @@ -664,9 +694,9 @@ export function BancosPage() { diff --git a/financeiro-web/src/features/bancos/pages/RendimentosPage.tsx b/financeiro-web/src/features/bancos/pages/RendimentosPage.tsx new file mode 100644 index 0000000..2dba1aa --- /dev/null +++ b/financeiro-web/src/features/bancos/pages/RendimentosPage.tsx @@ -0,0 +1,1216 @@ +import { useEffect, useMemo, useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Divider, + MenuItem, + Paper, + Stack, + TextField, + Tooltip, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from '@mui/material'; +import SavingsIcon from '@mui/icons-material/Savings'; +import TrendingUpIcon from '@mui/icons-material/TrendingUp'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import SaveIcon from '@mui/icons-material/Save'; +import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; +import StarIcon from '@mui/icons-material/Star'; +import BarChartIcon from '@mui/icons-material/BarChart'; +import TimelineIcon from '@mui/icons-material/Timeline'; +import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; +import InsightsIcon from '@mui/icons-material/Insights'; +import { + Bar, + BarChart, + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip as ChartTooltip, + XAxis, + YAxis, +} from 'recharts'; +import { + buscarDashboardRendimentos, + computarRendimentos, +} from '../services/rendimentosService'; +import type { + BancoInvestimento, + RendimentosDashboard, +} from '../types/rendimentosTypes'; + +type LinhaComputacao = { + idbancos: number; + novoSaldo: string; + descricao: string; + computar: boolean; +}; + +const PERIODOS = [ + { value: 3, label: 'Últimos 3 meses' }, + { value: 6, label: 'Últimos 6 meses' }, + { value: 12, label: 'Últimos 12 meses' }, + { value: 24, label: 'Últimos 24 meses' }, +]; + +const CORES_GRAFICO = [ + '#2563EB', // azul + '#16A34A', // verde + '#F97316', // laranja + '#9333EA', // roxo + '#DC2626', // vermelho + '#0891B2', // ciano + '#CA8A04', // amarelo escuro + '#DB2777', // rosa +]; + +function corSerie(index: number) { + return CORES_GRAFICO[index % CORES_GRAFICO.length]; +} + +function hojeDataString() { + return new Date().toISOString().slice(0, 10); +} + +function formatarValor(valor: number) { + return new Intl.NumberFormat('pt-BR', { + style: 'currency', + currency: 'BRL', + }).format(Number(valor || 0)); +} + +function formatarPercentual(valor: number) { + return `${Number(valor || 0).toFixed(2).replace('.', ',')}%`; +} + +function formatarData(data: string | null | undefined) { + if (!data) return '-'; + + return new Date(String(data)).toLocaleDateString('pt-BR'); +} + +function formatarCompetencia(competencia: string) { + if (!competencia) return '-'; + + const [ano, mes] = competencia.split('-'); + + if (!ano || !mes) return competencia; + + return `${mes}/${ano}`; +} + +function calcularDelta(banco: BancoInvestimento, linha?: LinhaComputacao) { + if (!linha?.novoSaldo) return 0; + + const novoSaldo = Number(linha.novoSaldo); + + if (!Number.isFinite(novoSaldo)) return 0; + + return Number((novoSaldo - Number(banco.saldo || 0)).toFixed(2)); +} + +function calcularPercentual(delta: number, saldoAtual: number) { + if (!saldoAtual) return 0; + + return Number(((delta / saldoAtual) * 100).toFixed(4)); +} + +export function RendimentosPage() { + const [dashboard, setDashboard] = useState(null); + const [linhas, setLinhas] = useState>({}); + const [periodoMeses, setPeriodoMeses] = useState(12); + const [mesesProjecao, setMesesProjecao] = useState(12); + const [dataComputacao, setDataComputacao] = useState(hojeDataString()); + const [descricaoGeral, setDescricaoGeral] = useState('Fechamento de rendimentos'); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [erro, setErro] = useState(''); + const [sucesso, setSucesso] = useState(''); + + const dadosGraficoSaldo = useMemo( + () => dashboard?.graficos?.saldoMensal || [], + [dashboard] + ); + + const dadosGraficoBarras = useMemo( + () => dashboard?.graficos?.barrasMensal || [], + [dashboard] + ); + + const dadosGraficoAcumulado = useMemo( + () => dashboard?.graficos?.rendimentoAcumulado || [], + [dashboard] + ); + + const bancosGrafico = useMemo(() => { + if (!dashboard?.bancos) return []; + + return dashboard.bancos.map((banco) => banco.descricao); + }, [dashboard]); + + const totaisPreComputacao = useMemo(() => { + if (!dashboard) { + return { + quantidadeSelecionada: 0, + totalDelta: 0, + rendimentoMedio: 0, + }; + } + + let quantidadeSelecionada = 0; + let totalDelta = 0; + let somaPercentual = 0; + + dashboard.bancos.forEach((banco) => { + const linha = linhas[banco.idbancos]; + + if (!linha?.computar) return; + + const delta = calcularDelta(banco, linha); + + if (delta <= 0) return; + + quantidadeSelecionada += 1; + totalDelta += delta; + somaPercentual += calcularPercentual(delta, Number(banco.saldo || 0)); + }); + + return { + quantidadeSelecionada, + totalDelta: Number(totalDelta.toFixed(2)), + rendimentoMedio: quantidadeSelecionada + ? Number((somaPercentual / quantidadeSelecionada).toFixed(4)) + : 0, + }; + }, [dashboard, linhas]); + + async function carregarDashboard() { + try { + setLoading(true); + setErro(''); + setSucesso(''); + + const data = await buscarDashboardRendimentos({ + periodoMeses, + mesesProjecao, + }); + + setDashboard(data); + + const novasLinhas: Record = {}; + + data.bancos.forEach((banco) => { + novasLinhas[banco.idbancos] = { + idbancos: banco.idbancos, + novoSaldo: '', + descricao: '', + computar: true, + }; + }); + + setLinhas(novasLinhas); + } catch (error: any) { + const message = + error?.response?.data?.message || + 'Não foi possível carregar o painel de rendimentos.'; + + setErro(message); + } finally { + setLoading(false); + } + } + + function atualizarLinha(idbancos: number, patch: Partial) { + setLinhas((atual) => ({ + ...atual, + [idbancos]: { + ...atual[idbancos], + idbancos, + ...patch, + }, + })); + } + + async function handleComputar() { + if (!dashboard) return; + + const itens = dashboard.bancos + .map((banco) => { + const linha = linhas[banco.idbancos]; + const novoSaldo = Number(linha?.novoSaldo); + const delta = calcularDelta(banco, linha); + + if (!linha?.computar) return null; + if (!Number.isFinite(novoSaldo)) return null; + if (delta <= 0) return null; + + return { + idbancos: banco.idbancos, + novoSaldo, + descricao: linha.descricao || undefined, + }; + }) + .filter(Boolean) as Array<{ + idbancos: number; + novoSaldo: number; + descricao?: string; + }>; + + if (itens.length === 0) { + setErro('Informe ao menos um novo saldo maior que o saldo atual.'); + return; + } + + try { + setSaving(true); + setErro(''); + setSucesso(''); + + const resultado = await computarRendimentos({ + data: dataComputacao, + descricao: descricaoGeral || undefined, + itens, + }); + + const msg = [ + `${resultado.resumo.quantidadeProcessada} rendimento(s) computado(s).`, + resultado.resumo.quantidadeIgnorada > 0 + ? `${resultado.resumo.quantidadeIgnorada} ignorado(s).` + : null, + resultado.resumo.quantidadeErro > 0 + ? `${resultado.resumo.quantidadeErro} com erro.` + : null, + `Total: ${formatarValor(resultado.resumo.totalRendimento)}.`, + ] + .filter(Boolean) + .join(' '); + + setSucesso(msg); + + await carregarDashboard(); + } catch (error: any) { + const message = + error?.response?.data?.message || + 'Não foi possível computar os rendimentos.'; + + setErro(message); + } finally { + setSaving(false); + } + } + + useEffect(() => { + carregarDashboard(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [periodoMeses, mesesProjecao]); + + return ( + + + + } + label="Investimentos" + color="secondary" + variant="outlined" + sx={{ marginBottom: 1.25, fontWeight: 700 }} + /> + + + Painel de rendimentos + + + + Acompanhe a evolução dos investimentos e compute os rendimentos por banco. + + + + + setPeriodoMeses(Number(event.target.value))} + size="small" + sx={{ minWidth: 190 }} + > + {PERIODOS.map((item) => ( + + {item.label} + + ))} + + + setMesesProjecao(Number(event.target.value))} + size="small" + sx={{ minWidth: 160 }} + > + 6 meses + 12 meses + 24 meses + 36 meses + + + + + + + {erro && ( + + {erro} + + )} + + {sucesso && ( + + {sucesso} + + )} + + {loading ? ( + + + Carregando painel de rendimentos... + + ) : !dashboard ? ( + + Não foi possível montar o painel de rendimentos. + + ) : ( + <> + + + + + Total investido + + + {formatarValor(dashboard.resumo.totalInvestidoAtual)} + + + + + + + + Rendimento no período + + = 0 ? 'success.main' : 'error.main'} + > + {formatarValor(dashboard.resumo.rendimentoPeriodo)} + + + + + + + + Rentabilidade média + + + {formatarPercentual(dashboard.resumo.rendimentoMedioPercentual)} + + + + + + + + + + Projeção {dashboard.projecao?.meses_projecao || mesesProjecao} meses + + + + + {formatarValor(dashboard.projecao?.saldo_projetado_total || 0)} + + + + +{formatarValor(dashboard.projecao?.rendimento_projetado_total || 0)} + + + + + + + + Bancos de investimento + + + {dashboard.resumo.quantidadeBancos} + + + + + + + + + + Melhor no período + + + + + {dashboard.resumo.melhorBanco?.banco_descricao || '-'} + + + + {dashboard.resumo.melhorBanco + ? formatarValor(dashboard.resumo.melhorBanco.rendimento_total || 0) + : formatarValor(0)} + + + + + + + + + + + Evolução do saldo por banco + + + Curva baseada nas auditorias geradas pelos rendimentos computados. + + + + } + label={`${dashboard.seriesMensais.length} ponto(s) no histórico`} + color="secondary" + variant="outlined" + /> + + + + {dadosGraficoSaldo.length === 0 ? ( + + + Ainda não há histórico suficiente para desenhar a curva. + Compute os primeiros rendimentos e o gráfico começa a ganhar vida. + + + ) : ( + + + + + + new Intl.NumberFormat('pt-BR', { + notation: 'compact', + compactDisplay: 'short', + }).format(Number(value || 0)) + } + /> + formatarValor(Number(value || 0))} + /> + + + {bancosGrafico.map((nomeBanco, index) => ( + + ))} + + + )} + + + + + + + + + + Rendimento mensal por banco + + + Compare quanto cada investimento rendeu em cada competência. + + + + } + label="Barras mensais" + color="primary" + variant="outlined" + /> + + + + {dadosGraficoBarras.length === 0 ? ( + + + Ainda não há rendimentos mensais suficientes para montar o gráfico. + + + ) : ( + + + + + + new Intl.NumberFormat('pt-BR', { + notation: 'compact', + compactDisplay: 'short', + }).format(Number(value || 0)) + } + /> + formatarValor(Number(value || 0))} + /> + + + {bancosGrafico.map((nomeBanco, index) => ( + + ))} + + + )} + + + + + + + + + + Rendimento acumulado por banco + + + Veja quanto cada investimento acumulou em rendimentos ao longo do tempo. + + + + } + label="Acumulado" + color="secondary" + variant="outlined" + /> + + + + {dadosGraficoAcumulado.length === 0 ? ( + + + Ainda não há histórico suficiente para montar o acumulado. + + + ) : ( + + + + + + new Intl.NumberFormat('pt-BR', { + notation: 'compact', + compactDisplay: 'short', + }).format(Number(value || 0)) + } + /> + formatarValor(Number(value || 0))} + /> + + + {bancosGrafico.map((nomeBanco, index) => ( + + ))} + + + )} + + + + + + + + + + Ranking de investimentos + + + Classificação por rendimento total no período selecionado. + + + + } + label={`${dashboard.ranking?.length || 0} investimento(s)`} + color="warning" + variant="outlined" + /> + + + {(!dashboard.ranking || dashboard.ranking.length === 0) ? ( + + Ainda não há dados suficientes para montar o ranking. + + ) : ( + + + + + Posição + Banco + Rendimento + Média % + Participação + Eventos + Último rendimento + + + + + {dashboard.ranking.map((item, index) => ( + + + + + + + + {item.banco_descricao} + + + + + + {formatarValor(item.rendimento_total)} + + + + + {formatarPercentual(item.percentual_medio)} + + + + {formatarPercentual(item.participacao_rendimento)} + + + + {item.quantidade_eventos} + + + + {formatarData(item.ultima_data)} + + + ))} + +
+
+ )} +
+
+ + + + + + + Computar rendimentos + + + Informe o saldo real atual de cada investimento. O sistema cria a entrada e atualiza o saldo automaticamente. + + + + + setDataComputacao(event.target.value)} + size="small" + InputLabelProps={{ shrink: true }} + /> + + setDescricaoGeral(event.target.value)} + size="small" + sx={{ minWidth: { xs: '100%', sm: 260 } }} + /> + + + + + + + Selecionados para computar + + + {totaisPreComputacao.quantidadeSelecionada} + + + + + + Rendimento calculado + + + {formatarValor(totaisPreComputacao.totalDelta)} + + + + + + Rentabilidade média lançada + + + {formatarPercentual(totaisPreComputacao.rendimentoMedio)} + + + + + {dashboard.bancos.length === 0 ? ( + + Nenhum banco de investimento habilitado encontrado. + + ) : ( + + + + + Banco + Saldo atual + Última apuração + Rendimento período + Projeção + Novo saldo real + Diferença + Rendimento % + Descrição específica + + + + + {dashboard.bancos.map((banco) => { + const linha = linhas[banco.idbancos]; + const delta = calcularDelta(banco, linha); + const percentual = calcularPercentual(delta, Number(banco.saldo || 0)); + const invalido = linha?.novoSaldo + ? Number(linha.novoSaldo) < Number(banco.saldo || 0) + : false; + + return ( + + + + + + + {banco.descricao} + + + Atualizado em {formatarData(banco.update_date)} + + + + + + + + {formatarValor(banco.saldo)} + + + + + {formatarData(banco.ultima_apuracao)} + + + + + {formatarValor(banco.rendimento_periodo || 0)} + + + + {formatarPercentual(banco.percentual_medio_periodo || 0)} + + + + + + {formatarValor(banco.projecao?.saldo_projetado || 0)} + + + + +{formatarValor(banco.projecao?.rendimento_projetado || 0)} + + + + + + atualizarLinha(banco.idbancos, { + novoSaldo: event.target.value, + }) + } + inputProps={{ + step: '0.01', + min: 0, + }} + error={invalido} + helperText={invalido ? 'Menor que o saldo atual' : ' '} + sx={{ minWidth: 160 }} + /> + + + + 0 + ? 'success.main' + : delta < 0 + ? 'error.main' + : 'text.secondary' + } + > + {formatarValor(delta)} + + + + + 0 ? 'success' : delta < 0 ? 'error' : 'default'} + variant="outlined" + size="small" + /> + + + + + + atualizarLinha(banco.idbancos, { + descricao: event.target.value, + }) + } + placeholder="Ex: rendimento CDB junho" + sx={{ minWidth: 240 }} + /> + + + + ); + })} + +
+
+ )} + + + + + + + + +
+
+ + + + + Histórico recente + + + + Últimas auditorias de saldo originadas por computação de rendimento. + + + {dashboard.historico.length === 0 ? ( + + Ainda não há histórico de rendimentos no período selecionado. + + ) : ( + } spacing={0}> + {dashboard.historico.slice().reverse().slice(0, 8).map((item) => ( + + + + {item.banco_descricao} + + + {item.descricao || 'Rendimento computado'} + + + + + + Data + + + {formatarData(item.insert_date)} + + + + + + Rendimento + + + {formatarValor(item.valor_delta)} + + + + + + Saldo após + + + {formatarValor(item.saldo_posterior)} + + + + ))} + + )} + + + + )} +
+ ); +} \ No newline at end of file diff --git a/financeiro-web/src/features/bancos/services/bancosService.ts b/financeiro-web/src/features/bancos/services/bancosService.ts index 1de903f..992dd39 100644 --- a/financeiro-web/src/features/bancos/services/bancosService.ts +++ b/financeiro-web/src/features/bancos/services/bancosService.ts @@ -15,6 +15,7 @@ export type ListarBancosParams = { page?: number; busca?: string; debito?: number | ''; + investimento?: number | ''; habilitado?: number | ''; dia_vencimento?: number | ''; orderBy?: string; diff --git a/financeiro-web/src/features/bancos/services/rendimentosService.ts b/financeiro-web/src/features/bancos/services/rendimentosService.ts new file mode 100644 index 0000000..a0170dc --- /dev/null +++ b/financeiro-web/src/features/bancos/services/rendimentosService.ts @@ -0,0 +1,55 @@ +import { api } from '../../../services/api'; +import type { + ApiResponse, + ComputarRendimentosRequest, + ComputarRendimentosResponse, + RendimentosDashboard, + RendimentoHistoricoItem, +} from '../types/rendimentosTypes'; + +export type BuscarRendimentosDashboardParams = { + periodoMeses?: number; + mesesProjecao?: number; + dataInicio?: string; + dataFim?: string; +}; + +export type BuscarHistoricoRendimentosParams = { + idbancos?: number | ''; + periodoMeses?: number; + dataInicio?: string; + dataFim?: string; +}; + +export async function buscarDashboardRendimentos( + params?: BuscarRendimentosDashboardParams +): Promise { + const response = await api.get>( + '/bancos/rendimentos/dashboard', + { params } + ); + + return response.data.data; +} + +export async function buscarHistoricoRendimentos( + params?: BuscarHistoricoRendimentosParams +): Promise { + const response = await api.get>( + '/bancos/rendimentos/historico', + { params } + ); + + return response.data.data; +} + +export async function computarRendimentos( + data: ComputarRendimentosRequest +): Promise { + const response = await api.post>( + '/bancos/rendimentos/computar', + data + ); + + return response.data.data; +} \ No newline at end of file diff --git a/financeiro-web/src/features/bancos/types/bancoTypes.ts b/financeiro-web/src/features/bancos/types/bancoTypes.ts index 7dd2977..ce8f9b6 100644 --- a/financeiro-web/src/features/bancos/types/bancoTypes.ts +++ b/financeiro-web/src/features/bancos/types/bancoTypes.ts @@ -1,8 +1,10 @@ export type Banco = { idbancos: number; + idusuarios: number; descricao: string; saldo: number; debito: number; + investimento: number; habilitado: number; dia_vencimento: number | null; insert_date: string | null; @@ -13,6 +15,7 @@ export type CriarBancoRequest = { descricao: string; saldo: number; debito: number; + investimento: number; habilitado: number; dia_vencimento: number | null; }; @@ -30,8 +33,9 @@ export type BancosPagination = { export type BancosSummary = { quantidade: number; saldoTotal: number; - saldoCarteiras: number; + saldoCredito: number; saldoDebito: number; + saldoInvestimento: number; }; export type ApiResponse = { diff --git a/financeiro-web/src/features/bancos/types/rendimentosTypes.ts b/financeiro-web/src/features/bancos/types/rendimentosTypes.ts new file mode 100644 index 0000000..7fca55c --- /dev/null +++ b/financeiro-web/src/features/bancos/types/rendimentosTypes.ts @@ -0,0 +1,155 @@ +export type BancoInvestimentoProjecao = { + idbancos: number; + banco_descricao: string; + saldo_atual: number; + taxa_media_mensal: number; + meses_base: number; + meses_projecao: number; + saldo_projetado: number; + rendimento_projetado: number; +}; + +export type BancoInvestimento = { + idbancos: number; + descricao: string; + saldo: number; + debito: number; + investimento: number; + habilitado: number; + update_date: string | null; + ultima_apuracao: string | null; + rendimento_periodo?: number; + percentual_medio_periodo?: number; + projecao?: BancoInvestimentoProjecao | null; +}; + +export type RendimentoHistoricoItem = { + idbancossaldoauditoria: number; + idbancos: number; + banco_descricao: string; + idcontasapagar: number | null; + tipo_operacao: string | null; + origem: string | null; + valor_delta: number; + saldo_anterior: number; + saldo_posterior: number; + descricao: string | null; + insert_date: string; + percentual: number; +}; + +export type RendimentoSerieMensal = { + idbancos: number; + banco_descricao: string; + competencia: string; + rendimento: number; + saldo_final: number; + eventos: number; +}; + +export type RendimentoRankingItem = { + idbancos: number; + banco_descricao: string; + rendimento_total: number; + quantidade_eventos: number; + soma_percentual: number; + percentual_medio: number; + maior_rendimento: { + valor_delta: number; + percentual: number; + insert_date: string; + } | null; + menor_rendimento: { + valor_delta: number; + percentual: number; + insert_date: string; + } | null; + primeira_data: string; + ultima_data: string; + saldo_atual: number; + participacao_rendimento: number; +}; + +export type RendimentosProjecao = { + meses_projecao: number; + saldo_atual_total: number; + saldo_projetado_total: number; + rendimento_projetado_total: number; + taxa_media_mensal_total: number; + bancos: BancoInvestimentoProjecao[]; + observacao: string; +}; + +export type RendimentosGraficos = { + barrasMensal: Array>; + saldoMensal: Array>; + rendimentoAcumulado: Array>; +}; + +export type RendimentosResumo = { + totalInvestidoAtual: number; + rendimentoPeriodo: number; + rendimentoMedioPercentual: number; + quantidadeBancos: number; + melhorBanco: RendimentoRankingItem | null; + projecao12Meses?: { + saldoProjetado: number; + rendimentoProjetado: number; + taxaMediaMensal: number; + } | null; +}; + +export type RendimentosDashboard = { + resumo: RendimentosResumo; + bancos: BancoInvestimento[]; + historico: RendimentoHistoricoItem[]; + seriesMensais: RendimentoSerieMensal[]; + ranking: RendimentoRankingItem[]; + projecao: RendimentosProjecao; + graficos: RendimentosGraficos; +}; + +export type ComputarRendimentoItemRequest = { + idbancos: number; + novoSaldo: number; + descricao?: string; +}; + +export type ComputarRendimentosRequest = { + data?: string; + descricao?: string; + confirmarDuplicado?: boolean; + itens: ComputarRendimentoItemRequest[]; +}; + +export type ComputarRendimentoResultado = { + ok: boolean; + ignorado?: boolean; + idbancos: number; + banco_descricao?: string; + saldo_anterior?: number; + saldo_posterior?: number; + valor_delta?: number; + percentual?: number; + message?: string; + movimento?: any; + impactoSaldo?: any[]; +}; + +export type ComputarRendimentosResponse = { + processados: ComputarRendimentoResultado[]; + ignorados: ComputarRendimentoResultado[]; + erros: ComputarRendimentoResultado[]; + resumo: { + quantidadeProcessada: number; + quantidadeIgnorada: number; + quantidadeErro: number; + totalRendimento: number; + }; +}; + +export type ApiResponse = { + ok: boolean; + message?: string; + data: T; +}; \ No newline at end of file diff --git a/financeiro-web/src/features/quitacoesCredito/pages/QuitacoesCreditoPage.tsx b/financeiro-web/src/features/quitacoesCredito/pages/QuitacoesCreditoPage.tsx index c0c725d..90f3b62 100644 --- a/financeiro-web/src/features/quitacoesCredito/pages/QuitacoesCreditoPage.tsx +++ b/financeiro-web/src/features/quitacoesCredito/pages/QuitacoesCreditoPage.tsx @@ -291,7 +291,7 @@ export function QuitacoesCreditoPage() { }); setSucesso( - `Quitação realizada com sucesso. Valor quitado: ${formatarValor(response.valor)}.` + `Quitação realizada com sucesso. Valor quitado: ${formatarValor(response.valorQuitado)}.` ); setImpactoSaldo(response.impactoSaldo || []); diff --git a/financeiro-web/src/features/quitacoesCredito/types/quitacaoCreditoTypes.ts b/financeiro-web/src/features/quitacoesCredito/types/quitacaoCreditoTypes.ts index 39fe419..579d564 100644 --- a/financeiro-web/src/features/quitacoesCredito/types/quitacaoCreditoTypes.ts +++ b/financeiro-web/src/features/quitacoesCredito/types/quitacaoCreditoTypes.ts @@ -79,7 +79,7 @@ export type QuitacaoCreditoResponse = { idcontasquitadas: number; idbancos: number; idbancos_p: number; - valor: number; + valorQuitado: number; descricao: string | null; data_quitacao: string | null; contasQuitadas?: ContaCreditoQuitacao[];