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}
>
-
-
+
+
+
- {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
>
-
-
+
+
+
@@ -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) => (
+
+ ))}
+
+
+ setMesesProjecao(Number(event.target.value))}
+ size="small"
+ sx={{ minWidth: 160 }}
+ >
+
+
+
+
+
+
+ }
+ onClick={carregarDashboard}
+ disabled={loading || saving}
+ sx={{ minHeight: 40 }}
+ >
+ Atualizar
+
+
+
+
+ {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 }}
+ />
+
+
+
+ );
+ })}
+
+
+
+ )}
+
+
+
+
+ }
+ onClick={carregarDashboard}
+ disabled={loading || saving}
+ >
+ Recarregar
+
+
+ :
+ }
+ onClick={handleComputar}
+ disabled={saving || dashboard.bancos.length === 0}
+ sx={{ boxShadow: 3 }}
+ >
+ {saving ? 'Computando...' : 'Computar rendimentos'}
+
+
+
+
+
+
+
+
+ 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[];