adicionado tela de rendimentos e filtrado bancos por usuario

This commit is contained in:
Diego Freitas 2026-06-01 22:30:10 -03:00
parent 345d6033a8
commit 0731328492
25 changed files with 2873 additions and 170 deletions

View File

@ -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);

View File

@ -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({

View File

@ -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,
};

View File

@ -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;

View File

@ -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;

View File

@ -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,
};

View File

@ -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,
});

View File

@ -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,
},

View File

@ -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({

View File

@ -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(

View File

@ -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;

View File

@ -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({

View File

@ -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 {

View File

@ -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
`
);

View File

@ -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: <EditarBancoPage />,
},
{
path: 'rendimentos',
element: <RendimentosPage />,
},
{
path: 'clientes',
element: <ClientesPage />,

View File

@ -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: <AccountBalanceWalletIcon />,
},
{
label: 'Rendimentos',
path: '/rendimentos',
icon: <SavingsIcon />,
},
{
label: 'Centros de custo',
path: '/centros-custo',

View File

@ -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) {
<TextField
select
label="Tipo"
value={debito}
onChange={(event) => setDebito(event.target.value)}
value={tipoCarteira}
onChange={(event) =>
setTipoCarteira(event.target.value as 'debito' | 'credito' | 'investimento')
}
fullWidth
sx={fieldSx}
>
<MenuItem value="1">Débito</MenuItem>
<MenuItem value="0">Crédito</MenuItem>
<MenuItem value="debito">Débito</MenuItem>
<MenuItem value="credito">Crédito</MenuItem>
<MenuItem value="investimento">Investimento</MenuItem>
</TextField>
{Number(debito) === 0 && (
{tipoCarteira === 'credito' && (
<TextField
label="Dia de vencimento"
type="number"
@ -414,11 +425,15 @@ export function BancoForm({ mode, initialData }: BancoFormProps) {
Tipo
</Typography>
<Typography fontWeight={800}>
{Number(debito) === 1 ? 'Débito' : 'Crédito'}
{tipoCarteira === 'investimento'
? 'Investimento'
: tipoCarteira === 'credito'
? 'Crédito'
: 'Débito'}
</Typography>
</Box>
{Number(debito) === 0 && (
{tipoCarteira === 'credito' && (
<Box>
<Typography variant="caption" color="text.secondary">
Dia de vencimento

View File

@ -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<BancosSummary>({
quantidade: 0,
saldoTotal: 0,
saldoCarteiras: 0,
saldoCredito: 0,
saldoDebito: 0,
saldoInvestimento: 0,
});
const [alterandoStatusId, setAlterandoStatusId] = useState<number | null>(null);
@ -108,7 +111,7 @@ export function BancosPage() {
const [page, setPage] = useState(1);
const [busca, setBusca] = useState('');
const [debito, setDebito] = useState<number | ''>('');
const [tipoFiltro, setTipoFiltro] = useState<'' | 'credito' | 'debito' | 'investimento'>('');
const [habilitado, setHabilitado] = useState<number | ''>('');
const [diaVencimento, setDiaVencimento] = useState<number | ''>('');
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() {
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Dívida em crédito
Saldo em crédito
</Typography>
<Typography variant="h5" fontWeight={950} color="success.main">
{formatarValor(summary.saldoCarteiras)}
<Typography variant="h5" fontWeight={950} color="info.main">
{formatarValor(summary.saldoCredito)}
</Typography>
</CardContent>
</Card>
@ -350,11 +364,22 @@ export function BancosPage() {
<Typography variant="body2" color="text.secondary">
Saldo em débito
</Typography>
<Typography variant="h5" fontWeight={950} color="warning.main">
<Typography variant="h5" fontWeight={950} color="success.main">
{formatarValor(summary.saldoDebito)}
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Investimentos
</Typography>
<Typography variant="h5" fontWeight={950} color="secondary.main">
{formatarValor(summary.saldoInvestimento)}
</Typography>
</CardContent>
</Card>
</Box>
<Card sx={{ marginBottom: { xs: 2.5, md: 3 } }}>
@ -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() {
<TextField
select
label="Tipo"
value={debito}
value={tipoFiltro}
onChange={(event) =>
setDebito(event.target.value === '' ? '' : Number(event.target.value))
setTipoFiltro(event.target.value as '' | 'credito' | 'debito' | 'investimento')
}
fullWidth
>
<MenuItem value="">Todos</MenuItem>
<MenuItem value={0}>Crédito</MenuItem>
<MenuItem value={1}>Débito</MenuItem>
<MenuItem value="credito">Crédito</MenuItem>
<MenuItem value="debito">Débito</MenuItem>
<MenuItem value="investimento">Investimento</MenuItem>
</TextField>
<TextField
@ -420,8 +446,12 @@ export function BancosPage() {
max: 31,
step: 1,
}}
disabled={debito === 1}
helperText={debito === 1 ? 'Disponível apenas para crédito' : 'Filtra bancos de crédito'}
disabled={tipoFiltro !== 'credito'}
helperText={
tipoFiltro !== 'credito'
? 'Disponível apenas para crédito'
: 'Filtra bancos de crédito'
}
fullWidth
/>
@ -550,9 +580,9 @@ export function BancosPage() {
<Stack direction="row" flexWrap="wrap" spacing={1} useFlexGap>
<Chip
label={tipoBancoLabel(item.debito)}
label={tipoBancoLabel(item)}
size="small"
color={tipoBancoColor(item.debito) as any}
color={tipoBancoColor(item) as any}
variant="outlined"
/>
@ -664,9 +694,9 @@ export function BancosPage() {
<TableCell>
<Chip
label={tipoBancoLabel(item.debito)}
label={tipoBancoLabel(item)}
size="small"
color={tipoBancoColor(item.debito) as any}
color={tipoBancoColor(item) as any}
variant="outlined"
/>
</TableCell>

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@ export type ListarBancosParams = {
page?: number;
busca?: string;
debito?: number | '';
investimento?: number | '';
habilitado?: number | '';
dia_vencimento?: number | '';
orderBy?: string;

View File

@ -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<RendimentosDashboard> {
const response = await api.get<ApiResponse<RendimentosDashboard>>(
'/bancos/rendimentos/dashboard',
{ params }
);
return response.data.data;
}
export async function buscarHistoricoRendimentos(
params?: BuscarHistoricoRendimentosParams
): Promise<RendimentoHistoricoItem[]> {
const response = await api.get<ApiResponse<RendimentoHistoricoItem[]>>(
'/bancos/rendimentos/historico',
{ params }
);
return response.data.data;
}
export async function computarRendimentos(
data: ComputarRendimentosRequest
): Promise<ComputarRendimentosResponse> {
const response = await api.post<ApiResponse<ComputarRendimentosResponse>>(
'/bancos/rendimentos/computar',
data
);
return response.data.data;
}

View File

@ -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<T> = {

View File

@ -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<Record<string, string | number>>;
saldoMensal: Array<Record<string, string | number>>;
rendimentoAcumulado: Array<Record<string, string | number>>;
};
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<T> = {
ok: boolean;
message?: string;
data: T;
};

View File

@ -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 || []);

View File

@ -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[];