diff --git a/financeiro-api/src/app.js b/financeiro-api/src/app.js
index 9d9fbd6..269b298 100644
--- a/financeiro-api/src/app.js
+++ b/financeiro-api/src/app.js
@@ -10,6 +10,8 @@ const bancosRoutes = require('./modules/bancos/routes/bancos.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');
+const quitacoesCreditoRoutes = require('./modules/quitacoesCredito/routes/quitacoesCredito.routes');
+const dashboardRoutes = require('./modules/dashboard/routes/dashboard.routes');
const app = express();
@@ -42,6 +44,8 @@ app.use('/api/bancos', bancosRoutes);
app.use('/api/centros-custo', centrosCustoRoutes);
app.use('/api/movimentos-fixos', movimentosFixosRoutes);
app.use('/api/usuarios', usuariosRoutes);
+app.use('/api/quitacoes-credito', quitacoesCreditoRoutes);
+app.use('/api/dashboard', dashboardRoutes);
app.use((req, res) => {
return res.status(404).json({
diff --git a/financeiro-api/src/modules/bancos/controllers/bancos.controller.js b/financeiro-api/src/modules/bancos/controllers/bancos.controller.js
index 0eff64b..0663d5a 100644
--- a/financeiro-api/src/modules/bancos/controllers/bancos.controller.js
+++ b/financeiro-api/src/modules/bancos/controllers/bancos.controller.js
@@ -29,6 +29,14 @@ 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) {
+ return 'Dia de Vencimeneto inválido.';
+ }
+ }
+
return null;
}
@@ -40,6 +48,7 @@ async function listar(req, res) {
page: req.query.page,
busca: req.query.busca,
debito: req.query.debito,
+ dia_vencimento: req.query.dia_vencimento,
habilitado: req.query.habilitado,
orderBy: req.query.orderBy,
orderDirection: req.query.orderDirection,
diff --git a/financeiro-api/src/modules/bancos/services/bancos.service.js b/financeiro-api/src/modules/bancos/services/bancos.service.js
index b2ce8e9..dfcaf0f 100644
--- a/financeiro-api/src/modules/bancos/services/bancos.service.js
+++ b/financeiro-api/src/modules/bancos/services/bancos.service.js
@@ -6,6 +6,7 @@ const CAMPOS_BANCO_SELECT = `
saldo,
debito,
habilitado,
+ dia_vencimento,
insert_date,
update_date
`;
@@ -57,6 +58,7 @@ function resolverOrdenacao(orderBy) {
saldo: 'saldo',
debito: 'debito',
habilitado: 'habilitado',
+ dia_vencimento: 'dia_vencimento',
insert_date: 'insert_date',
update_date: 'update_date',
};
@@ -82,6 +84,11 @@ function montarWhereBancos(filtros = {}) {
params.push(Number(filtros.habilitado));
}
+ if (filtros.dia_vencimento !== undefined && filtros.dia_vencimento !== null && filtros.dia_vencimento !== '') {
+ where.push('dia_vencimento = ?');
+ params.push(Number(filtros.dia_vencimento));
+ }
+
if (filtros.busca) {
where.push(`
(
@@ -189,6 +196,7 @@ async function criarBanco(dados) {
saldo,
debito,
habilitado,
+ dia_vencimento,
} = dados;
const [result] = await pool.query(
@@ -198,9 +206,10 @@ async function criarBanco(dados) {
saldo,
debito,
habilitado,
+ dia_vencimento,
insert_date,
update_date
- ) VALUES (?, ?, ?, ?, NOW(), NOW())
+ ) VALUES (?, ?, ?, ?, ?, NOW(), NOW())
`,
[
normalizarTextoOuNull(descricao),
@@ -209,6 +218,7 @@ async function criarBanco(dados) {
habilitado === undefined || habilitado === null || habilitado === ''
? 1
: Number(habilitado),
+ normalizarNumero(dia_vencimento, null)
]
);
@@ -227,6 +237,7 @@ async function atualizarBanco(id, dados) {
saldo,
debito,
habilitado,
+ dia_vencimento
} = dados;
await pool.query(
@@ -237,6 +248,7 @@ async function atualizarBanco(id, dados) {
saldo = ?,
debito = ?,
habilitado = ?,
+ dia_vencimento = ?,
update_date = NOW()
WHERE idbancos = ?
`,
@@ -247,7 +259,8 @@ async function atualizarBanco(id, dados) {
habilitado === undefined || habilitado === null || habilitado === ''
? 1
: Number(habilitado),
- id,
+ normalizarNumero(dia_vencimento, null),
+ id
]
);
diff --git a/financeiro-api/src/modules/dashboard/controllers/dashboard.controller.js b/financeiro-api/src/modules/dashboard/controllers/dashboard.controller.js
new file mode 100644
index 0000000..a573fdc
--- /dev/null
+++ b/financeiro-api/src/modules/dashboard/controllers/dashboard.controller.js
@@ -0,0 +1,26 @@
+const dashboardService = require('../services/dashboard.service');
+
+async function buscarResumo(req, res) {
+ try {
+ const resultado = await dashboardService.buscarResumoDashboard({
+ dataInicio: req.query.dataInicio,
+ dataFim: req.query.dataFim,
+ });
+
+ return res.json({
+ ok: true,
+ data: resultado,
+ });
+ } catch (error) {
+ console.error('[dashboard.controller] buscarResumo:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Não foi possível carregar o resumo da dashboard.',
+ });
+ }
+}
+
+module.exports = {
+ buscarResumo,
+};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/dashboard/routes/dashboard.routes.js b/financeiro-api/src/modules/dashboard/routes/dashboard.routes.js
new file mode 100644
index 0000000..8bdf13f
--- /dev/null
+++ b/financeiro-api/src/modules/dashboard/routes/dashboard.routes.js
@@ -0,0 +1,10 @@
+const express = require('express');
+
+const authMiddleware = require('../../../middlewares/auth.middleware');
+const dashboardController = require('../controllers/dashboard.controller');
+
+const router = express.Router();
+
+router.get('/resumo', authMiddleware, dashboardController.buscarResumo);
+
+module.exports = router;
\ No newline at end of file
diff --git a/financeiro-api/src/modules/dashboard/services/dashboard.service.js b/financeiro-api/src/modules/dashboard/services/dashboard.service.js
new file mode 100644
index 0000000..ee41763
--- /dev/null
+++ b/financeiro-api/src/modules/dashboard/services/dashboard.service.js
@@ -0,0 +1,676 @@
+const pool = require('../../../database/mysql');
+
+function hojeISO() {
+ return new Date().toISOString().slice(0, 10);
+}
+
+function inicioMesAtualISO() {
+ const hoje = new Date();
+ return new Date(hoje.getFullYear(), hoje.getMonth(), 1)
+ .toISOString()
+ .slice(0, 10);
+}
+
+function fimMesAtualISO() {
+ const hoje = new Date();
+ return new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0)
+ .toISOString()
+ .slice(0, 10);
+}
+
+function normalizarDataISO(valor, fallback) {
+ if (!valor) return fallback;
+
+ const texto = String(valor).slice(0, 10);
+ const data = new Date(`${texto}T00:00:00`);
+
+ if (Number.isNaN(data.getTime())) {
+ return fallback;
+ }
+
+ return texto;
+}
+
+function normalizarNumero(valor, padrao = 0) {
+ const numero = Number(valor);
+
+ if (!Number.isFinite(numero)) {
+ return padrao;
+ }
+
+ return numero;
+}
+
+function calcularPercentual(valor, base) {
+ const valorNormalizado = normalizarNumero(valor);
+ const baseNormalizada = normalizarNumero(base);
+
+ if (baseNormalizada === 0) {
+ return 0;
+ }
+
+ 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
+ `);
+
+ const row = rows[0] || {};
+
+ return {
+ saldoDebito: normalizarNumero(row.saldoDebito),
+ dividaCredito: normalizarNumero(row.dividaCredito),
+ quantidadeCarteiras: normalizarNumero(row.quantidadeCarteiras),
+ carteirasHabilitadas: normalizarNumero(row.carteirasHabilitadas),
+ };
+}
+
+async function buscarCardsMovimentos(dataInicio, dataFim) {
+ const [rows] = await pool.query(
+ `
+ SELECT
+ COALESCE(SUM(CASE
+ WHEN cp.movimento IN ('Entrada', 'Estorno') THEN cp.valor
+ ELSE 0
+ END), 0) AS entradas,
+
+ COALESCE(SUM(CASE
+ WHEN cp.movimento = 'Saida' THEN cp.valor
+ ELSE 0
+ END), 0) AS saidas,
+
+ COALESCE(SUM(CASE
+ WHEN cp.movimento = 'Sangria'
+ AND COALESCE(cc.investimento, 0) = 1
+ THEN cp.valor
+ ELSE 0
+ END), 0) AS investimentos,
+
+ COALESCE(SUM(CASE
+ WHEN cp.status = 'A receber'
+ AND cp.movimento IN ('Entrada', 'Estorno')
+ THEN cp.valor
+ ELSE 0
+ END), 0) AS abertoEntradas,
+
+ COUNT(CASE
+ WHEN cp.status = 'A receber'
+ AND cp.movimento IN ('Entrada', 'Estorno')
+ THEN 1
+ ELSE NULL
+ END) AS quantidadeAbertoEntradas,
+
+ COALESCE(SUM(CASE
+ WHEN cp.status = 'A pagar'
+ AND cp.movimento = 'Saida'
+ THEN cp.valor
+ ELSE 0
+ END), 0) AS abertoSaidas,
+
+ COUNT(CASE
+ WHEN cp.status = 'A pagar'
+ AND cp.movimento = 'Saida'
+ THEN 1
+ ELSE NULL
+ END) AS quantidadeAbertoSaidas,
+
+ COALESCE(SUM(CASE
+ WHEN cp.status = 'A pagar'
+ AND cp.movimento = 'Sangria'
+ AND COALESCE(cc.investimento, 0) = 1
+ THEN cp.valor
+ ELSE 0
+ END), 0) AS abertoInvestimentos,
+
+ COUNT(CASE
+ WHEN cp.status = 'A pagar'
+ AND cp.movimento = 'Sangria'
+ AND COALESCE(cc.investimento, 0) = 1
+ THEN 1
+ ELSE NULL
+ END) AS quantidadeAbertoInvestimentos,
+
+ COALESCE(SUM(CASE
+ WHEN cp.status IN ('Pago', 'Recebido') THEN cp.valor
+ ELSE 0
+ END), 0) AS baixado,
+
+ COUNT(*) AS quantidadeMovimentos
+ FROM contasapagar cp
+ LEFT JOIN centrodecustos cc
+ ON cc.idcentrodecustos = cp.idcentrodecustos
+ WHERE cp.deleted_at IS NULL
+ AND DATE(cp.datavencimento) BETWEEN ? AND ?
+ `,
+ [dataInicio, dataFim]
+ );
+
+ const row = rows[0] || {};
+
+ const entradas = normalizarNumero(row.entradas);
+ const saidas = normalizarNumero(row.saidas);
+ const investimentos = normalizarNumero(row.investimentos);
+ const resultado = entradas - saidas - investimentos;
+
+ const abertoEntradas = normalizarNumero(row.abertoEntradas);
+ const abertoSaidas = normalizarNumero(row.abertoSaidas);
+ const abertoInvestimentos = normalizarNumero(row.abertoInvestimentos);
+
+ return {
+ entradas,
+ saidas,
+ investimentos,
+ resultado,
+
+ percentualEntradas: entradas > 0 ? 100 : 0,
+ percentualSaidas: calcularPercentual(saidas, entradas),
+ percentualInvestimentos: calcularPercentual(investimentos, entradas),
+ percentualResultado: calcularPercentual(resultado, entradas),
+
+ abertoEntradas,
+ abertoSaidas,
+ abertoInvestimentos,
+ aberto: abertoEntradas - abertoSaidas - abertoInvestimentos,
+
+ quantidadeAbertoEntradas: normalizarNumero(row.quantidadeAbertoEntradas),
+ quantidadeAbertoSaidas: normalizarNumero(row.quantidadeAbertoSaidas),
+ quantidadeAbertoInvestimentos: normalizarNumero(row.quantidadeAbertoInvestimentos),
+
+ baixado: normalizarNumero(row.baixado),
+ quantidadeMovimentos: normalizarNumero(row.quantidadeMovimentos),
+ };
+}
+
+async function buscarAlertasVencimento() {
+ const hoje = hojeISO();
+
+ const [rows] = await pool.query(
+ `
+ SELECT
+ COALESCE(SUM(CASE
+ WHEN DATE(cp.datavencimento) < ? THEN cp.valor
+ ELSE 0
+ END), 0) AS valorVencido,
+
+ COUNT(CASE
+ WHEN DATE(cp.datavencimento) < ? THEN 1
+ ELSE NULL
+ END) AS vencidos,
+
+ COALESCE(SUM(CASE
+ WHEN DATE(cp.datavencimento) = ? THEN cp.valor
+ ELSE 0
+ END), 0) AS valorVenceHoje,
+
+ COUNT(CASE
+ WHEN DATE(cp.datavencimento) = ? THEN 1
+ ELSE NULL
+ END) AS venceHoje,
+
+ COALESCE(SUM(CASE
+ WHEN DATE(cp.datavencimento) > ?
+ AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
+ THEN cp.valor
+ ELSE 0
+ END), 0) AS valorVenceEmBreve,
+
+ COUNT(CASE
+ WHEN DATE(cp.datavencimento) > ?
+ AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
+ THEN 1
+ ELSE NULL
+ END) AS venceEmBreve,
+
+ COALESCE(SUM(CASE
+ WHEN DATE(cp.datavencimento) >= ?
+ AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
+ AND cp.movimento IN ('Entrada', 'Estorno')
+ AND cp.status = 'A receber'
+ THEN cp.valor
+ ELSE 0
+ END), 0) AS venceEmBreveEntradas,
+
+ COUNT(CASE
+ WHEN DATE(cp.datavencimento) >= ?
+ AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
+ AND cp.movimento IN ('Entrada', 'Estorno')
+ AND cp.status = 'A receber'
+ THEN 1
+ ELSE NULL
+ END) AS quantidadeVenceEmBreveEntradas,
+
+ COALESCE(SUM(CASE
+ WHEN DATE(cp.datavencimento) >= ?
+ AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
+ AND cp.movimento = 'Saida'
+ AND cp.status = 'A pagar'
+ THEN cp.valor
+ ELSE 0
+ END), 0) AS venceEmBreveSaidas,
+
+ COUNT(CASE
+ WHEN DATE(cp.datavencimento) >= ?
+ AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
+ AND cp.movimento = 'Saida'
+ AND cp.status = 'A pagar'
+ THEN 1
+ ELSE NULL
+ END) AS quantidadeVenceEmBreveSaidas,
+
+ COALESCE(SUM(CASE
+ WHEN DATE(cp.datavencimento) >= ?
+ AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
+ AND cp.movimento = 'Sangria'
+ AND cp.status = 'A pagar'
+ AND COALESCE(cc.investimento, 0) = 1
+ THEN cp.valor
+ ELSE 0
+ END), 0) AS venceEmBreveInvestimentos,
+
+ COUNT(CASE
+ WHEN DATE(cp.datavencimento) >= ?
+ AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
+ AND cp.movimento = 'Sangria'
+ AND cp.status = 'A pagar'
+ AND COALESCE(cc.investimento, 0) = 1
+ THEN 1
+ ELSE NULL
+ END) AS quantidadeVenceEmBreveInvestimentos
+ FROM contasapagar cp
+ LEFT JOIN centrodecustos cc
+ ON cc.idcentrodecustos = cp.idcentrodecustos
+ WHERE cp.deleted_at IS NULL
+ AND cp.status IN ('A pagar', 'A receber')
+ AND cp.datavencimento IS NOT NULL
+ `,
+ [
+ hoje, hoje,
+ hoje, hoje,
+ hoje, hoje,
+ hoje, hoje,
+
+ hoje, hoje,
+ hoje, hoje,
+
+ hoje, hoje,
+ hoje, hoje,
+
+ hoje, hoje,
+ hoje, hoje,
+ ]
+ );
+
+ const row = rows[0] || {};
+
+ const venceEmBreveEntradas = normalizarNumero(row.venceEmBreveEntradas);
+ const venceEmBreveSaidas = normalizarNumero(row.venceEmBreveSaidas);
+ const venceEmBreveInvestimentos = normalizarNumero(row.venceEmBreveInvestimentos);
+
+ return {
+ vencidos: normalizarNumero(row.vencidos),
+ valorVencido: normalizarNumero(row.valorVencido),
+
+ venceHoje: normalizarNumero(row.venceHoje),
+ valorVenceHoje: normalizarNumero(row.valorVenceHoje),
+
+ venceEmBreve: normalizarNumero(row.venceEmBreve),
+ valorVenceEmBreve: normalizarNumero(row.valorVenceEmBreve),
+
+ venceEmBreveEntradas,
+ venceEmBreveSaidas,
+ venceEmBreveInvestimentos,
+ venceEmBreveResultado: venceEmBreveEntradas - venceEmBreveSaidas - venceEmBreveInvestimentos,
+
+ quantidadeVenceEmBreveEntradas: normalizarNumero(row.quantidadeVenceEmBreveEntradas),
+ quantidadeVenceEmBreveSaidas: normalizarNumero(row.quantidadeVenceEmBreveSaidas),
+ quantidadeVenceEmBreveInvestimentos: normalizarNumero(row.quantidadeVenceEmBreveInvestimentos),
+ };
+}
+
+async function buscarProximosVencimentos() {
+ const [rows] = await pool.query(`
+ SELECT
+ cp.idcontasapagar,
+ cp.movimento,
+ cp.descricao,
+ cp.valor,
+ cp.status,
+ cp.dataentrada,
+ cp.datavencimento,
+ cp.databaixa,
+ cp.parcela,
+ cp.parcelas,
+ cp.idbancos,
+ bc.descricao AS banco_descricao,
+ bc.debito AS banco_debito,
+ cp.idcentrodecustos,
+ cc.descricao AS centro_custo_descricao,
+ DATEDIFF(DATE(cp.datavencimento), CURDATE()) AS dias_para_vencer
+ FROM contasapagar cp
+ 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 cp.status IN ('A pagar', 'A receber')
+ AND cp.datavencimento IS NOT NULL
+ ORDER BY DATE(cp.datavencimento) ASC, cp.idcontasapagar ASC
+ LIMIT 10
+ `);
+
+ return rows;
+}
+
+async function buscarCreditosEmAberto(dataInicio, dataFim) {
+ const [rows] = await pool.query(
+ `
+ SELECT
+ bc.idbancos,
+ bc.descricao,
+ bc.saldo,
+ bc.debito,
+ bc.habilitado,
+ bc.dia_vencimento,
+
+ COUNT(cp.idcontasapagar) AS quantidade_aberta,
+
+ COALESCE(SUM(CASE
+ WHEN cp.status = 'A pagar'
+ AND cp.movimento IN ('Saida', 'Sangria')
+ THEN cp.valor
+ ELSE 0
+ END), 0) AS total_aberto,
+
+ COALESCE(SUM(CASE
+ WHEN cp.status = 'A pagar'
+ AND cp.movimento IN ('Saida', 'Sangria')
+ AND DATE(cp.datavencimento) BETWEEN ? AND ?
+ THEN cp.valor
+ ELSE 0
+ END), 0) AS fatura_atual,
+
+ COUNT(CASE
+ WHEN cp.status = 'A pagar'
+ AND cp.movimento IN ('Saida', 'Sangria')
+ AND DATE(cp.datavencimento) BETWEEN ? AND ?
+ THEN 1
+ ELSE NULL
+ END) AS quantidade_fatura_atual,
+
+ COALESCE(SUM(CASE
+ WHEN cp.status = 'A pagar'
+ AND cp.movimento IN ('Saida', 'Sangria')
+ AND DATE(cp.datavencimento) > ?
+ THEN cp.valor
+ ELSE 0
+ END), 0) AS proximas_faturas,
+
+ COUNT(CASE
+ WHEN cp.status = 'A pagar'
+ AND cp.movimento IN ('Saida', 'Sangria')
+ AND DATE(cp.datavencimento) > ?
+ THEN 1
+ ELSE NULL
+ END) AS quantidade_proximas_faturas
+ FROM bancos bc
+ LEFT JOIN contasapagar cp
+ ON cp.idbancos = bc.idbancos
+ AND cp.deleted_at IS NULL
+ AND cp.status = 'A pagar'
+ AND cp.movimento IN ('Saida', 'Sangria')
+ WHERE bc.debito = 0
+ AND bc.habilitado = 1
+ GROUP BY
+ bc.idbancos,
+ bc.descricao,
+ bc.saldo,
+ bc.debito,
+ bc.habilitado,
+ bc.dia_vencimento
+ ORDER BY
+ CASE WHEN bc.dia_vencimento IS NULL THEN 99 ELSE bc.dia_vencimento END ASC,
+ bc.descricao ASC
+ `,
+ [
+ dataInicio,
+ dataFim,
+ dataInicio,
+ dataFim,
+ dataFim,
+ dataFim,
+ ]
+ );
+
+ return rows.map((item) => ({
+ ...item,
+ saldo: normalizarNumero(item.saldo),
+ quantidade_aberta: normalizarNumero(item.quantidade_aberta),
+ total_aberto: normalizarNumero(item.total_aberto),
+
+ fatura_atual: normalizarNumero(item.fatura_atual),
+ quantidade_fatura_atual: normalizarNumero(item.quantidade_fatura_atual),
+
+ proximas_faturas: normalizarNumero(item.proximas_faturas),
+ quantidade_proximas_faturas: normalizarNumero(item.quantidade_proximas_faturas),
+ }));
+}
+
+async function buscarCarteiras(dataInicio, dataFim) {
+ const [rows] = await pool.query(
+ `
+ SELECT
+ bc.idbancos,
+ bc.descricao,
+ bc.saldo,
+ bc.debito,
+ bc.habilitado,
+ bc.dia_vencimento,
+ bc.insert_date,
+ bc.update_date,
+
+ 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
+ ) aud ON aud.idbancos = bc.idbancos
+ WHERE bc.habilitado = 1
+ ORDER BY bc.debito DESC, bc.descricao ASC
+ `,
+ [dataInicio, dataFim]
+ );
+
+ return rows.map((item) => {
+ const saldoAtual = normalizarNumero(item.saldo);
+ const deltaPeriodo = normalizarNumero(item.delta_periodo);
+ const saldoMesAnterior = saldoAtual - deltaPeriodo;
+ const variacaoValor = saldoAtual - saldoMesAnterior;
+
+ const variacaoPercentual = saldoMesAnterior !== 0
+ ? (variacaoValor / Math.abs(saldoMesAnterior)) * 100
+ : null;
+
+ const isCredito = Number(item.debito) === 0;
+
+ const tendenciaBoa = isCredito
+ ? variacaoValor < 0
+ : variacaoValor > 0;
+
+ return {
+ ...item,
+ saldo: saldoAtual,
+ saldo_mes_anterior: saldoMesAnterior,
+ variacao_valor: variacaoValor,
+ variacao_percentual: variacaoPercentual,
+ tendencia_boa: tendenciaBoa,
+ };
+ });
+}
+
+async function buscarUltimosMovimentos() {
+ const [rows] = await pool.query(`
+ SELECT
+ cp.idcontasapagar,
+ cp.movimento,
+ cp.descricao,
+ cp.valor,
+ cp.status,
+ cp.dataentrada,
+ cp.datavencimento,
+ cp.databaixa,
+ cp.parcela,
+ cp.parcelas,
+ cp.competencia,
+ cp.origem,
+ cp.saldo_processado,
+ cp.insert_date,
+ cp.update_date,
+ cp.idbancos,
+ bc.descricao AS banco_descricao,
+ bc.debito AS banco_debito,
+ cp.idcentrodecustos,
+ cc.descricao AS centro_custo_descricao
+ FROM contasapagar cp
+ LEFT JOIN bancos bc ON bc.idbancos = cp.idbancos
+ LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos
+ WHERE cp.deleted_at IS NULL
+ ORDER BY cp.insert_date DESC, cp.idcontasapagar DESC
+ LIMIT 10
+ `);
+
+ return rows;
+}
+
+async function buscarGraficos(dataInicio, dataFim) {
+ const [centrosRows] = await pool.query(
+ `
+ SELECT
+ cp.idcentrodecustos,
+ COALESCE(cc.descricao, 'Sem centro de custo') AS descricao,
+ COALESCE(SUM(cp.valor), 0) AS valor
+ FROM contasapagar cp
+ LEFT JOIN centrodecustos cc
+ ON cc.idcentrodecustos = cp.idcentrodecustos
+ WHERE cp.deleted_at IS NULL
+ AND cp.movimento IN ('Saida')
+ AND DATE(cp.datavencimento) BETWEEN ? AND ?
+ GROUP BY
+ cp.idcentrodecustos,
+ COALESCE(cc.descricao, 'Sem centro de custo')
+ HAVING valor > 0
+ ORDER BY valor DESC
+ LIMIT 8
+ `,
+ [dataInicio, dataFim]
+ );
+
+ const [entradasSaidasRows] = await pool.query(
+ `
+ SELECT
+ COALESCE(SUM(CASE
+ WHEN cp.movimento IN ('Entrada', 'Estorno') THEN cp.valor
+ ELSE 0
+ END), 0) AS entradas,
+
+ COALESCE(SUM(CASE
+ WHEN cp.movimento = 'Saida' THEN cp.valor
+ ELSE 0
+ END), 0) AS saidas,
+
+ COALESCE(SUM(CASE
+ WHEN cp.movimento = 'Sangria'
+ AND COALESCE(cc.investimento, 0) = 1
+ THEN cp.valor
+ ELSE 0
+ END), 0) AS investimentos
+ FROM contasapagar cp
+ LEFT JOIN centrodecustos cc
+ ON cc.idcentrodecustos = cp.idcentrodecustos
+ WHERE cp.deleted_at IS NULL
+ AND DATE(cp.datavencimento) BETWEEN ? AND ?
+ `,
+ [dataInicio, dataFim]
+ );
+
+ const entradas = normalizarNumero(entradasSaidasRows[0]?.entradas);
+ const saidas = normalizarNumero(entradasSaidasRows[0]?.saidas);
+ const investimentos = normalizarNumero(entradasSaidasRows[0]?.investimentos);
+ const resultado = entradas - saidas - investimentos;
+
+ return {
+ gastosPorCentroCusto: centrosRows.map((item) => ({
+ idcentrodecustos: item.idcentrodecustos,
+ descricao: item.descricao,
+ valor: normalizarNumero(item.valor),
+ })),
+
+ entradasSaidas: {
+ entradas,
+ saidas,
+ investimentos,
+ resultado,
+
+ percentualEntradas: entradas > 0 ? 100 : 0,
+ percentualSaidas: calcularPercentual(saidas, entradas),
+ percentualInvestimentos: calcularPercentual(investimentos, entradas),
+ percentualResultado: calcularPercentual(resultado, entradas),
+ },
+ };
+}
+
+async function buscarResumoDashboard(filtros = {}) {
+ const dataInicio = normalizarDataISO(filtros.dataInicio, inicioMesAtualISO());
+ const dataFim = normalizarDataISO(filtros.dataFim, fimMesAtualISO());
+
+ const [
+ cardsBancos,
+ cardsMovimentos,
+ alertasVencimento,
+ proximosVencimentos,
+ creditos,
+ carteiras,
+ ultimosMovimentos,
+ graficos,
+ ] = await Promise.all([
+ buscarCardsBancos(),
+ buscarCardsMovimentos(dataInicio, dataFim),
+ buscarAlertasVencimento(),
+ buscarProximosVencimentos(),
+ buscarCreditosEmAberto(dataInicio, dataFim),
+ buscarCarteiras(dataInicio, dataFim),
+ buscarUltimosMovimentos(),
+ buscarGraficos(dataInicio, dataFim),
+ ]);
+
+ return {
+ periodo: {
+ dataInicio,
+ dataFim,
+ },
+
+ cards: {
+ ...cardsBancos,
+ ...cardsMovimentos,
+ ...alertasVencimento,
+ },
+
+ proximosVencimentos,
+ creditos,
+ carteiras,
+ ultimosMovimentos,
+ graficos,
+ };
+}
+
+module.exports = {
+ buscarResumoDashboard,
+};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/quitacoesCredito/controllers/quitacoesCredito.controller.js b/financeiro-api/src/modules/quitacoesCredito/controllers/quitacoesCredito.controller.js
new file mode 100644
index 0000000..ee0867a
--- /dev/null
+++ b/financeiro-api/src/modules/quitacoesCredito/controllers/quitacoesCredito.controller.js
@@ -0,0 +1,183 @@
+const quitacoesCreditoService = require('../services/quitacoesCredito.service');
+
+function tratarErro(error, res, mensagemPadrao) {
+ if (error?.statusCode) {
+ return res.status(error.statusCode).json({
+ ok: false,
+ message: error.message,
+ code: error.code || undefined,
+ });
+ }
+
+ return res.status(500).json({
+ ok: false,
+ message: mensagemPadrao,
+ });
+}
+
+function validarCriacao(dados) {
+ if (!dados.idBancoCredito && !dados.idbancos_p) {
+ return 'Banco de crédito é obrigatório.';
+ }
+
+ if (!dados.idBancoPagamento && !dados.idbancos) {
+ return 'Banco de pagamento é obrigatório.';
+ }
+
+ const idsContas = dados.idsContas || dados.contas;
+
+ if (!Array.isArray(idsContas) || idsContas.length === 0) {
+ return 'Selecione pelo menos uma conta para quitar.';
+ }
+
+ if (dados.valorQuitado !== undefined && dados.valorQuitado !== null && dados.valorQuitado !== '') {
+ const valor = Number(dados.valorQuitado);
+
+ if (!Number.isFinite(valor) || valor <= 0) {
+ return 'Valor quitado inválido.';
+ }
+ }
+
+ return null;
+}
+
+async function listarBancosCredito(req, res) {
+ try {
+ const bancos = await quitacoesCreditoService.listarBancosCredito();
+
+ return res.json({
+ ok: true,
+ data: bancos,
+ });
+ } catch (error) {
+ console.error('Erro ao listar bancos de crédito:', error);
+ return tratarErro(error, res, 'Erro ao listar bancos de crédito.');
+ }
+}
+
+async function listarBancosPagamento(req, res) {
+ try {
+ const bancos = await quitacoesCreditoService.listarBancosPagamento();
+
+ return res.json({
+ ok: true,
+ data: bancos,
+ });
+ } catch (error) {
+ console.error('Erro ao listar bancos de pagamento:', error);
+ return tratarErro(error, res, 'Erro ao listar bancos de pagamento.');
+ }
+}
+
+async function preview(req, res) {
+ try {
+ const resultado = await quitacoesCreditoService.previewQuitacao({
+ idBancoCredito: req.query.idBancoCredito || req.query.idbancos,
+ dataInicio: req.query.dataInicio,
+ dataFim: req.query.dataFim,
+ busca: req.query.busca,
+ limite: req.query.limite,
+ offset: req.query.offset,
+ page: req.query.page,
+ });
+
+ return res.json({
+ ok: true,
+ data: resultado,
+ });
+ } catch (error) {
+ console.error('Erro ao gerar preview de quitação de crédito:', error);
+ return tratarErro(error, res, 'Erro ao gerar preview de quitação de crédito.');
+ }
+}
+
+async function criar(req, res) {
+ try {
+ const erroValidacao = validarCriacao(req.body);
+
+ if (erroValidacao) {
+ return res.status(400).json({
+ ok: false,
+ message: erroValidacao,
+ });
+ }
+
+ const resultado = await quitacoesCreditoService.criarQuitacaoCredito(req.user.id, req.body);
+
+ return res.status(201).json({
+ ok: true,
+ message: 'Crédito quitado com sucesso.',
+ data: resultado.quitacao,
+ contasQuitadas: resultado.contasQuitadas,
+ valorQuitado: resultado.valorQuitado,
+ impactoSaldo: resultado.impactoSaldo || [],
+ });
+ } catch (error) {
+ console.error('Erro ao quitar crédito:', error);
+ return tratarErro(error, res, 'Erro ao quitar crédito.');
+ }
+}
+
+async function listarHistorico(req, res) {
+ try {
+ const resultado = await quitacoesCreditoService.listarHistorico({
+ limite: req.query.limite,
+ offset: req.query.offset,
+ page: req.query.page,
+ idBancoCredito: req.query.idBancoCredito,
+ idBancoPagamento: req.query.idBancoPagamento,
+ dataInicio: req.query.dataInicio,
+ dataFim: req.query.dataFim,
+ busca: req.query.busca,
+ });
+
+ return res.json({
+ ok: true,
+ data: resultado.data,
+ pagination: resultado.pagination,
+ summary: resultado.summary,
+ });
+ } catch (error) {
+ console.error('Erro ao listar histórico de quitações:', error);
+ return tratarErro(error, res, 'Erro ao listar histórico de quitações.');
+ }
+}
+
+async function buscarPorId(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const quitacao = await quitacoesCreditoService.buscarQuitacaoPorId(id);
+
+ if (!quitacao) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Quitação não encontrada.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ data: quitacao,
+ });
+ } catch (error) {
+ console.error('Erro ao buscar quitação:', error);
+ return tratarErro(error, res, 'Erro ao buscar quitação.');
+ }
+}
+
+module.exports = {
+ listarBancosCredito,
+ listarBancosPagamento,
+ preview,
+ criar,
+ listarHistorico,
+ buscarPorId,
+};
diff --git a/financeiro-api/src/modules/quitacoesCredito/routes/quitacoesCredito.routes.js b/financeiro-api/src/modules/quitacoesCredito/routes/quitacoesCredito.routes.js
new file mode 100644
index 0000000..72b1c9f
--- /dev/null
+++ b/financeiro-api/src/modules/quitacoesCredito/routes/quitacoesCredito.routes.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const authMiddleware = require('../../../middlewares/auth.middleware');
+const quitacoesCreditoController = require('../controllers/quitacoesCredito.controller');
+
+const router = express.Router();
+
+router.use(authMiddleware);
+
+router.get('/bancos-credito', quitacoesCreditoController.listarBancosCredito);
+router.get('/bancos-pagamento', quitacoesCreditoController.listarBancosPagamento);
+router.get('/preview', quitacoesCreditoController.preview);
+router.get('/historico', quitacoesCreditoController.listarHistorico);
+router.get('/:id', quitacoesCreditoController.buscarPorId);
+
+router.post('/', quitacoesCreditoController.criar);
+
+module.exports = router;
diff --git a/financeiro-api/src/modules/quitacoesCredito/services/quitacoesCredito.service.js b/financeiro-api/src/modules/quitacoesCredito/services/quitacoesCredito.service.js
new file mode 100644
index 0000000..46cdc3c
--- /dev/null
+++ b/financeiro-api/src/modules/quitacoesCredito/services/quitacoesCredito.service.js
@@ -0,0 +1,807 @@
+const pool = require('../../../database/mysql');
+const movimentosSaldoService = require('../../movimentos/services/movimentosSaldo.service');
+
+const CAMPOS_CONTA_SELECT = `
+ cp.idcontasapagar,
+ cp.movimento,
+ cp.descricao,
+ cp.dataentrada,
+ cp.datavencimento,
+ cp.databaixa,
+ cp.valor,
+ cp.parcela,
+ cp.parcelas,
+ cp.status,
+ cp.idcentrodecustos,
+ cp.idbancos,
+ cp.idusuarios_cad,
+ cp.idusuarios_baixa,
+ cp.idclientes,
+ cp.idveiculosdetalhes,
+ cp.idbancos_p,
+ cp.idmovimentosfixos,
+ cp.competencia,
+ cp.grupo_parcelamento,
+ cp.origem,
+ cp.saldo_processado,
+ cp.observacao,
+ cp.insert_date,
+ cp.update_date,
+ cp.deleted_at,
+ b.descricao AS banco_descricao,
+ b.debito AS banco_debito,
+ cc.descricao AS centro_custo_descricao,
+ c.nome AS cliente_nome,
+ bp.descricao AS banco_referencia_descricao,
+ bp.debito AS banco_referencia_debito
+`;
+
+const FROM_CONTA_JOIN = `
+ FROM contasapagar cp
+ LEFT JOIN bancos b ON b.idbancos = cp.idbancos
+ LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos
+ LEFT JOIN clientes c ON c.idclientes = cp.idclientes
+ LEFT JOIN bancos bp ON bp.idbancos = cp.idbancos_p
+`;
+
+function criarErro(message, statusCode = 400, code = 'VALIDACAO_QUITACAO_CREDITO') {
+ const error = new Error(message);
+ error.statusCode = statusCode;
+ error.code = code;
+ return error;
+}
+
+function normalizarNumeroOuNull(valor) {
+ if (valor === undefined || valor === null || valor === '') {
+ return null;
+ }
+
+ const numero = Number(valor);
+ return Number.isFinite(numero) ? numero : null;
+}
+
+function normalizarTextoOuNull(valor) {
+ if (valor === undefined || valor === null || valor === '') {
+ return null;
+ }
+
+ const texto = String(valor).trim();
+ return texto || null;
+}
+
+function normalizarDataOuHoje(data) {
+ const texto = normalizarTextoOuNull(data);
+
+ if (!texto) {
+ return new Date().toISOString().slice(0, 10);
+ }
+
+ return texto.slice(0, 10);
+}
+
+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 arredondarMoeda(valor) {
+ return Number(Number(valor || 0).toFixed(2));
+}
+
+function bancoEhCredito(banco) {
+ return Number(banco?.debito) === 0;
+}
+
+function bancoEhDebito(banco) {
+ return Number(banco?.debito) === 1;
+}
+
+function idsUnicos(ids = []) {
+ return [...new Set(
+ ids
+ .map((id) => Number(id))
+ .filter((id) => Number.isInteger(id) && id > 0)
+ )];
+}
+
+async function buscarBancoParaUpdate(connection, idbancos) {
+ const [rows] = await connection.query(
+ `
+ SELECT
+ idbancos,
+ descricao,
+ saldo,
+ debito,
+ habilitado
+ FROM bancos
+ WHERE idbancos = ?
+ LIMIT 1
+ FOR UPDATE
+ `,
+ [idbancos]
+ );
+
+ return rows[0] || null;
+}
+
+async function inserirAuditoriaConta(connection, dados) {
+ await connection.query(
+ `
+ INSERT INTO contasapagar_auditoria (
+ idcontasapagar,
+ acao,
+ dados_antes,
+ dados_depois,
+ idusuarios,
+ insert_date
+ ) VALUES (?, ?, ?, ?, ?, NOW())
+ `,
+ [
+ dados.idcontasapagar || null,
+ dados.acao,
+ dados.dados_antes ? JSON.stringify(dados.dados_antes) : null,
+ dados.dados_depois ? JSON.stringify(dados.dados_depois) : null,
+ dados.idusuarios || null,
+ ]
+ );
+}
+
+async function listarBancosCredito() {
+ const [rows] = await pool.query(
+ `
+ SELECT
+ idbancos,
+ descricao,
+ saldo,
+ debito,
+ habilitado,
+ insert_date,
+ update_date
+ FROM bancos
+ WHERE debito = 0
+ AND COALESCE(habilitado, 1) = 1
+ ORDER BY descricao ASC, idbancos ASC
+ `
+ );
+
+ return rows;
+}
+
+async function listarBancosPagamento() {
+ const [rows] = await pool.query(
+ `
+ SELECT
+ idbancos,
+ descricao,
+ saldo,
+ debito,
+ habilitado,
+ insert_date,
+ update_date
+ FROM bancos
+ WHERE debito = 1
+ AND COALESCE(habilitado, 1) = 1
+ ORDER BY descricao ASC, idbancos ASC
+ `
+ );
+
+ return rows;
+}
+
+function montarWhereContasPreview(filtros = {}) {
+ const where = [
+ 'cp.deleted_at IS NULL',
+ "cp.movimento = 'Saida'",
+ "cp.status = 'A pagar'",
+ 'cp.idbancos = ?',
+ ];
+
+ const params = [Number(filtros.idBancoCredito)];
+
+ if (filtros.dataInicio) {
+ where.push('DATE(cp.datavencimento) >= ?');
+ params.push(String(filtros.dataInicio).slice(0, 10));
+ }
+
+ if (filtros.dataFim) {
+ where.push('DATE(cp.datavencimento) <= ?');
+ params.push(String(filtros.dataFim).slice(0, 10));
+ }
+
+ if (filtros.busca) {
+ where.push(`
+ (
+ cp.descricao LIKE ?
+ OR cp.observacao LIKE ?
+ OR cp.competencia LIKE ?
+ OR cc.descricao LIKE ?
+ OR c.nome LIKE ?
+ )
+ `);
+
+ const termo = `%${String(filtros.busca).trim()}%`;
+ params.push(termo, termo, termo, termo, termo);
+ }
+
+ return {
+ whereSql: `WHERE ${where.join(' AND ')}`,
+ params,
+ };
+}
+
+async function previewQuitacao(filtros = {}) {
+ const idBancoCredito = normalizarNumeroOuNull(filtros.idBancoCredito || filtros.idbancos);
+
+ if (!idBancoCredito) {
+ throw criarErro('Banco de crédito é obrigatório.');
+ }
+
+ const limite = limitarNumero(filtros.limite, 100, 1, 300);
+ const page = limitarNumero(filtros.page, 1, 1, 999999);
+ const offset = filtros.offset !== undefined
+ ? limitarNumero(filtros.offset, 0, 0, 999999999)
+ : (page - 1) * limite;
+
+ const [bancoRows] = await pool.query(
+ `
+ SELECT
+ idbancos,
+ descricao,
+ saldo,
+ debito,
+ habilitado
+ FROM bancos
+ WHERE idbancos = ?
+ LIMIT 1
+ `,
+ [idBancoCredito]
+ );
+
+ const bancoCredito = bancoRows[0] || null;
+
+ if (!bancoCredito) {
+ throw criarErro('Banco de crédito não encontrado.', 404);
+ }
+
+ if (!bancoEhCredito(bancoCredito)) {
+ throw criarErro('O banco informado não é um banco de crédito.');
+ }
+
+ const { whereSql, params } = montarWhereContasPreview({
+ ...filtros,
+ idBancoCredito,
+ });
+
+ const [rows] = await pool.query(
+ `
+ SELECT
+ ${CAMPOS_CONTA_SELECT}
+ ${FROM_CONTA_JOIN}
+ ${whereSql}
+ ORDER BY cp.datavencimento ASC, cp.idcontasapagar ASC
+ LIMIT ? OFFSET ?
+ `,
+ [...params, limite, offset]
+ );
+
+ const [countRows] = await pool.query(
+ `
+ SELECT COUNT(*) AS total
+ ${FROM_CONTA_JOIN}
+ ${whereSql}
+ `,
+ params
+ );
+
+ const [summaryRows] = await pool.query(
+ `
+ SELECT
+ COUNT(*) AS quantidade,
+ COALESCE(SUM(cp.valor), 0) AS valorTotal,
+ MIN(cp.datavencimento) AS primeiroVencimento,
+ MAX(cp.datavencimento) AS ultimoVencimento
+ ${FROM_CONTA_JOIN}
+ ${whereSql}
+ `,
+ params
+ );
+
+ const total = Number(countRows[0]?.total || 0);
+ const totalPages = Math.max(1, Math.ceil(total / limite));
+ const resumo = summaryRows[0] || {};
+
+ return {
+ bancoCredito,
+ contas: rows,
+ pagination: {
+ total,
+ limite,
+ offset,
+ page: Math.floor(offset / limite) + 1,
+ totalPages,
+ },
+ summary: {
+ quantidade: Number(resumo.quantidade || 0),
+ valorTotal: arredondarMoeda(resumo.valorTotal || 0),
+ saldoCreditoAtual: arredondarMoeda(bancoCredito.saldo || 0),
+ saldoCreditoAposQuitacaoTotal: arredondarMoeda(Number(bancoCredito.saldo || 0) - Number(resumo.valorTotal || 0)),
+ primeiroVencimento: resumo.primeiroVencimento || null,
+ ultimoVencimento: resumo.ultimoVencimento || null,
+ },
+ };
+}
+
+async function buscarContasSelecionadasParaUpdate(connection, ids) {
+ if (!ids.length) return [];
+
+ const placeholders = ids.map(() => '?').join(', ');
+
+ const [rows] = await connection.query(
+ `
+ SELECT
+ cp.*
+ FROM contasapagar cp
+ WHERE cp.idcontasapagar IN (${placeholders})
+ FOR UPDATE
+ `,
+ ids
+ );
+
+ return rows;
+}
+
+async function buscarContasDetalhadasPorIds(connection, ids) {
+ if (!ids.length) return [];
+
+ const placeholders = ids.map(() => '?').join(', ');
+
+ const [rows] = await connection.query(
+ `
+ SELECT
+ ${CAMPOS_CONTA_SELECT}
+ ${FROM_CONTA_JOIN}
+ WHERE cp.idcontasapagar IN (${placeholders})
+ ORDER BY cp.datavencimento ASC, cp.idcontasapagar ASC
+ `,
+ ids
+ );
+
+ return rows;
+}
+
+async function criarQuitacaoCredito(idUsuario, dados = {}) {
+ const idBancoCredito = normalizarNumeroOuNull(dados.idBancoCredito || dados.idbancos_p);
+ const idBancoPagamento = normalizarNumeroOuNull(dados.idBancoPagamento || dados.idbancos);
+ const idsContas = idsUnicos(dados.idsContas || dados.contas || []);
+ const dataQuitacao = normalizarDataOuHoje(dados.dataQuitacao || dados.data_quitacao);
+ const descricaoInformada = normalizarTextoOuNull(dados.descricao);
+
+ if (!idBancoCredito) {
+ throw criarErro('Banco de crédito é obrigatório.');
+ }
+
+ if (!idBancoPagamento) {
+ throw criarErro('Banco de pagamento é obrigatório.');
+ }
+
+ if (idBancoCredito === idBancoPagamento) {
+ throw criarErro('Banco de crédito e banco de pagamento não podem ser iguais.');
+ }
+
+ if (!idsContas.length) {
+ throw criarErro('Selecione pelo menos uma conta para quitar.');
+ }
+
+ const connection = await pool.getConnection();
+
+ try {
+ await connection.beginTransaction();
+
+ const bancoCredito = await buscarBancoParaUpdate(connection, idBancoCredito);
+ const bancoPagamento = await buscarBancoParaUpdate(connection, idBancoPagamento);
+
+ if (!bancoCredito) {
+ throw criarErro('Banco de crédito não encontrado.', 404);
+ }
+
+ if (!bancoPagamento) {
+ throw criarErro('Banco de pagamento não encontrado.', 404);
+ }
+
+ if (!bancoEhCredito(bancoCredito)) {
+ throw criarErro('O banco informado para quitação não é um banco de crédito.');
+ }
+
+ if (!bancoEhDebito(bancoPagamento)) {
+ throw criarErro('O banco usado para pagamento precisa ser um banco/carteira de débito.');
+ }
+
+ if (Number(bancoCredito.habilitado) === 0) {
+ throw criarErro('O banco de crédito está desabilitado.');
+ }
+
+ if (Number(bancoPagamento.habilitado) === 0) {
+ throw criarErro('O banco de pagamento está desabilitado.');
+ }
+
+ const contasAntes = await buscarContasSelecionadasParaUpdate(connection, idsContas);
+
+ if (contasAntes.length !== idsContas.length) {
+ const encontrados = new Set(contasAntes.map((conta) => Number(conta.idcontasapagar)));
+ const faltantes = idsContas.filter((id) => !encontrados.has(id));
+ throw criarErro(`Algumas contas selecionadas não foram encontradas: ${faltantes.join(', ')}.`);
+ }
+
+ for (const conta of contasAntes) {
+ if (Number(conta.deleted_at ? 1 : 0) === 1 || conta.deleted_at) {
+ throw criarErro(`A conta ${conta.idcontasapagar} está excluída e não pode ser quitada.`);
+ }
+
+ if (Number(conta.idbancos) !== idBancoCredito) {
+ throw criarErro(`A conta ${conta.idcontasapagar} não pertence ao banco de crédito selecionado.`);
+ }
+
+ if (conta.movimento !== 'Saida') {
+ throw criarErro(`A conta ${conta.idcontasapagar} não é uma saída de crédito.`);
+ }
+
+ if (conta.status !== 'A pagar') {
+ throw criarErro(`A conta ${conta.idcontasapagar} não está em aberto para pagamento.`);
+ }
+ }
+
+ const valorTotal = arredondarMoeda(contasAntes.reduce((total, conta) => total + Number(conta.valor || 0), 0));
+
+ if (valorTotal <= 0) {
+ throw criarErro('O valor total da quitação deve ser maior que zero.');
+ }
+
+ if (dados.valorQuitado !== undefined && dados.valorQuitado !== null && dados.valorQuitado !== '') {
+ const valorInformado = arredondarMoeda(dados.valorQuitado);
+
+ if (valorInformado !== valorTotal) {
+ throw criarErro('O valor informado não confere com a soma das contas selecionadas.');
+ }
+ }
+
+ const descricao = descricaoInformada || `Quitação de crédito - ${bancoCredito.descricao}`;
+
+ const [quitacaoResult] = await connection.query(
+ `
+ INSERT INTO contasquitadas (
+ idbancos,
+ idbancos_p,
+ valor,
+ descricao,
+ data_quitacao,
+ idusuarios,
+ insert_date,
+ update_date,
+ deleted_at
+ ) VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW(), NULL)
+ `,
+ [
+ idBancoPagamento,
+ idBancoCredito,
+ valorTotal,
+ descricao,
+ dataQuitacao,
+ idUsuario || null,
+ ]
+ );
+
+ const idcontasquitadas = quitacaoResult.insertId;
+
+ const refsValues = idsContas.map((idcontasapagar) => [
+ idcontasquitadas,
+ idcontasapagar,
+ ]);
+
+ await connection.query(
+ `
+ INSERT INTO contasquitadasref (
+ idcontasquitadas,
+ idcontasapagar,
+ insert_date,
+ update_date
+ ) VALUES ${refsValues.map(() => '(?, ?, NOW(), NOW())').join(', ')}
+ `,
+ refsValues.flat()
+ );
+
+ const placeholders = idsContas.map(() => '?').join(', ');
+
+ await connection.query(
+ `
+ UPDATE contasapagar
+ SET
+ status = 'Pago',
+ databaixa = ?,
+ idusuarios_baixa = ?,
+ idbancos_p = ?,
+ update_date = NOW()
+ WHERE idcontasapagar IN (${placeholders})
+ `,
+ [
+ dataQuitacao,
+ idUsuario || null,
+ idBancoPagamento,
+ ...idsContas,
+ ]
+ );
+
+ const contasDepois = await buscarContasDetalhadasPorIds(connection, idsContas);
+ const contasDepoisPorId = new Map(contasDepois.map((conta) => [Number(conta.idcontasapagar), conta]));
+
+ for (const contaAntes of contasAntes) {
+ await inserirAuditoriaConta(connection, {
+ idcontasapagar: contaAntes.idcontasapagar,
+ acao: 'quitacao_credito',
+ dados_antes: contaAntes,
+ dados_depois: contasDepoisPorId.get(Number(contaAntes.idcontasapagar)) || null,
+ idusuarios: idUsuario,
+ });
+ }
+
+ const impactosSaldo = await movimentosSaldoService.aplicarDeltasSaldo(
+ connection,
+ [
+ {
+ idbancos: idBancoPagamento,
+ valor_delta: -valorTotal,
+ descricao: `Pagamento de crédito: ${bancoCredito.descricao}`,
+ },
+ {
+ idbancos: idBancoCredito,
+ valor_delta: -valorTotal,
+ descricao: `Baixa de dívida de crédito paga por ${bancoPagamento.descricao}`,
+ },
+ ],
+ {
+ idcontasquitadas,
+ tipo_operacao: 'quitacao_credito',
+ origem: 'quitacao_credito',
+ idusuarios: idUsuario,
+ }
+ );
+
+ const [quitacaoRows] = await connection.query(
+ `
+ SELECT
+ cq.idcontasquitadas,
+ cq.idbancos,
+ bp.descricao AS banco_pagamento_descricao,
+ bp.saldo AS banco_pagamento_saldo,
+ cq.idbancos_p,
+ bc.descricao AS banco_credito_descricao,
+ bc.saldo AS banco_credito_saldo,
+ cq.valor,
+ cq.descricao,
+ cq.data_quitacao,
+ cq.idusuarios,
+ cq.insert_date,
+ cq.update_date,
+ cq.deleted_at
+ FROM contasquitadas cq
+ LEFT JOIN bancos bp ON bp.idbancos = cq.idbancos
+ LEFT JOIN bancos bc ON bc.idbancos = cq.idbancos_p
+ WHERE cq.idcontasquitadas = ?
+ LIMIT 1
+ `,
+ [idcontasquitadas]
+ );
+
+ await connection.commit();
+
+ return {
+ quitacao: quitacaoRows[0] || null,
+ contasQuitadas: contasDepois,
+ valorQuitado: valorTotal,
+ impactoSaldo: impactosSaldo,
+ };
+ } catch (error) {
+ await connection.rollback();
+ throw error;
+ } finally {
+ connection.release();
+ }
+}
+
+async function listarHistorico(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 = [];
+
+ if (filtros.idBancoCredito) {
+ where.push('cq.idbancos_p = ?');
+ params.push(Number(filtros.idBancoCredito));
+ }
+
+ if (filtros.idBancoPagamento) {
+ where.push('cq.idbancos = ?');
+ params.push(Number(filtros.idBancoPagamento));
+ }
+
+ if (filtros.dataInicio) {
+ where.push('DATE(cq.data_quitacao) >= ?');
+ params.push(String(filtros.dataInicio).slice(0, 10));
+ }
+
+ if (filtros.dataFim) {
+ where.push('DATE(cq.data_quitacao) <= ?');
+ params.push(String(filtros.dataFim).slice(0, 10));
+ }
+
+ if (filtros.busca) {
+ where.push(`
+ (
+ cq.descricao LIKE ?
+ OR bp.descricao LIKE ?
+ OR bc.descricao LIKE ?
+ )
+ `);
+
+ const termo = `%${String(filtros.busca).trim()}%`;
+ params.push(termo, termo, termo);
+ }
+
+ const whereSql = `WHERE ${where.join(' AND ')}`;
+
+ const [rows] = await pool.query(
+ `
+ SELECT
+ cq.idcontasquitadas,
+ cq.idbancos,
+ bp.descricao AS banco_pagamento_descricao,
+ cq.idbancos_p,
+ bc.descricao AS banco_credito_descricao,
+ cq.valor,
+ cq.descricao,
+ cq.data_quitacao,
+ cq.idusuarios,
+ u.nome AS usuario_nome,
+ cq.insert_date,
+ cq.update_date,
+ cq.deleted_at,
+ COUNT(cqr.idcontasquitadasref) AS quantidade_contas
+ FROM contasquitadas cq
+ LEFT JOIN bancos bp ON bp.idbancos = cq.idbancos
+ LEFT JOIN bancos bc ON bc.idbancos = cq.idbancos_p
+ LEFT JOIN usuarios u ON u.idusuarios = cq.idusuarios
+ LEFT JOIN contasquitadasref cqr ON cqr.idcontasquitadas = cq.idcontasquitadas
+ ${whereSql}
+ GROUP BY
+ cq.idcontasquitadas,
+ cq.idbancos,
+ bp.descricao,
+ cq.idbancos_p,
+ bc.descricao,
+ cq.valor,
+ cq.descricao,
+ cq.data_quitacao,
+ cq.idusuarios,
+ u.nome,
+ cq.insert_date,
+ cq.update_date,
+ cq.deleted_at
+ ORDER BY cq.data_quitacao DESC, cq.idcontasquitadas DESC
+ LIMIT ? OFFSET ?
+ `,
+ [...params, limite, offset]
+ );
+
+ const [countRows] = await pool.query(
+ `
+ SELECT COUNT(*) AS total
+ FROM contasquitadas cq
+ LEFT JOIN bancos bp ON bp.idbancos = cq.idbancos
+ LEFT JOIN bancos bc ON bc.idbancos = cq.idbancos_p
+ ${whereSql}
+ `,
+ params
+ );
+
+ const [summaryRows] = await pool.query(
+ `
+ SELECT
+ COUNT(*) AS quantidade,
+ COALESCE(SUM(cq.valor), 0) AS valorTotal
+ FROM contasquitadas cq
+ LEFT JOIN bancos bp ON bp.idbancos = cq.idbancos
+ LEFT JOIN bancos bc ON bc.idbancos = cq.idbancos_p
+ ${whereSql}
+ `,
+ params
+ );
+
+ const total = Number(countRows[0]?.total || 0);
+ const totalPages = Math.max(1, Math.ceil(total / limite));
+
+ return {
+ data: rows,
+ pagination: {
+ total,
+ limite,
+ offset,
+ page: Math.floor(offset / limite) + 1,
+ totalPages,
+ },
+ summary: {
+ quantidade: Number(summaryRows[0]?.quantidade || 0),
+ valorTotal: arredondarMoeda(summaryRows[0]?.valorTotal || 0),
+ },
+ };
+}
+
+async function buscarQuitacaoPorId(id) {
+ const [quitacaoRows] = await pool.query(
+ `
+ SELECT
+ cq.idcontasquitadas,
+ cq.idbancos,
+ bp.descricao AS banco_pagamento_descricao,
+ cq.idbancos_p,
+ bc.descricao AS banco_credito_descricao,
+ cq.valor,
+ cq.descricao,
+ cq.data_quitacao,
+ cq.idusuarios,
+ u.nome AS usuario_nome,
+ cq.insert_date,
+ cq.update_date,
+ cq.deleted_at
+ FROM contasquitadas cq
+ LEFT JOIN bancos bp ON bp.idbancos = cq.idbancos
+ LEFT JOIN bancos bc ON bc.idbancos = cq.idbancos_p
+ LEFT JOIN usuarios u ON u.idusuarios = cq.idusuarios
+ WHERE cq.idcontasquitadas = ?
+ LIMIT 1
+ `,
+ [id]
+ );
+
+ const quitacao = quitacaoRows[0] || null;
+
+ if (!quitacao) {
+ return null;
+ }
+
+ const [contas] = await pool.query(
+ `
+ SELECT
+ ${CAMPOS_CONTA_SELECT}
+ ${FROM_CONTA_JOIN}
+ INNER JOIN contasquitadasref cqr ON cqr.idcontasapagar = cp.idcontasapagar
+ WHERE cqr.idcontasquitadas = ?
+ ORDER BY cp.datavencimento ASC, cp.idcontasapagar ASC
+ `,
+ [id]
+ );
+
+ return {
+ ...quitacao,
+ contas,
+ };
+}
+
+module.exports = {
+ listarBancosCredito,
+ listarBancosPagamento,
+ previewQuitacao,
+ criarQuitacaoCredito,
+ listarHistorico,
+ buscarQuitacaoPorId,
+};
diff --git a/financeiro-api/src/modules/referencias/services/referencias.service.js b/financeiro-api/src/modules/referencias/services/referencias.service.js
index 2d227d7..f1385a4 100644
--- a/financeiro-api/src/modules/referencias/services/referencias.service.js
+++ b/financeiro-api/src/modules/referencias/services/referencias.service.js
@@ -7,7 +7,8 @@ async function listarBancos() {
idbancos AS id,
descricao,
saldo,
- debito
+ debito,
+ dia_vencimento
FROM bancos
WHERE habilitado = 1
ORDER BY descricao ASC
diff --git a/financeiro-web/src/app/router.tsx b/financeiro-web/src/app/router.tsx
index f0c51df..8aaa9e7 100644
--- a/financeiro-web/src/app/router.tsx
+++ b/financeiro-web/src/app/router.tsx
@@ -7,7 +7,7 @@ import { EditarMovimentoPage } from '../features/movimentos/pages/EditarMoviment
import { MovimentosPage } from '../features/movimentos/pages/MovimentosPage';
import { NovoMovimentoPage } from '../features/movimentos/pages/NovoMovimentoPage';
import { RelatoriosPage } from '../features/relatorios/pages/RelatoriosPage';
-import { DashboardPage } from '../pages/DashboardPage';
+import { DashboardPage } from '../features/dashboard/pages/DashboardPage';
import { NotFoundPage } from '../pages/NotFoundPage';
import { BancosPage } from '../features/bancos/pages/BancosPage';
import { NovoBancoPage } from '../features/bancos/pages/NovoBancoPage';
@@ -24,6 +24,7 @@ import { EditarMovimentoFixoPage } from '../features/movimentosFixos/pages/Edita
import { UsuariosPage } from '../features/usuarios/pages/UsuariosPage';
import { NovoUsuarioPage } from '../features/usuarios/pages/NovoUsuarioPage';
import { EditarUsuarioPage } from '../features/usuarios/pages/EditarUsuarioPage';
+import { QuitacoesCreditoPage } from '../features/quitacoesCredito/pages/QuitacoesCreditoPage';
export const routes: RouteObject[] = [
{
@@ -120,6 +121,10 @@ export const routes: RouteObject[] = [
path: 'usuarios/:id/editar',
element: ,
},
+ {
+ path: '/quitacoes-credito',
+ element: ,
+ }
],
},
],
diff --git a/financeiro-web/src/components/layout/Sidebar.tsx b/financeiro-web/src/components/layout/Sidebar.tsx
index 8b26f9f..8baba0b 100644
--- a/financeiro-web/src/components/layout/Sidebar.tsx
+++ b/financeiro-web/src/components/layout/Sidebar.tsx
@@ -16,6 +16,7 @@ import PeopleAltIcon from '@mui/icons-material/PeopleAlt';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
+import CreditCardIcon from '@mui/icons-material/CreditCard';
import { NavLink } from 'react-router-dom';
type SidebarProps = {
@@ -33,6 +34,11 @@ const menuItems = [
path: '/movimentos',
icon: ,
},
+ {
+ label: 'Quitar créditos',
+ path: '/quitacoes-credito',
+ icon: ,
+ },
{
label: 'Clientes',
path: '/clientes',
diff --git a/financeiro-web/src/features/bancos/components/BancoForm.tsx b/financeiro-web/src/features/bancos/components/BancoForm.tsx
index e65fe3d..c9afff8 100644
--- a/financeiro-web/src/features/bancos/components/BancoForm.tsx
+++ b/financeiro-web/src/features/bancos/components/BancoForm.tsx
@@ -110,8 +110,9 @@ export function BancoForm({ mode, initialData }: BancoFormProps) {
const [descricao, setDescricao] = useState('');
const [saldo, setSaldo] = useState('0');
- const [debito, setDebito] = useState('0');
+ const [debito, setDebito] = useState('1');
const [habilitado, setHabilitado] = useState('1');
+ const [diaVencimento, setDiaVencimento] = useState('');
const titulo = isEdit ? 'Editar banco/carteira' : 'Nova carteira';
const subtitulo = isEdit
@@ -123,15 +124,19 @@ export function BancoForm({ mode, initialData }: BancoFormProps) {
setDescricao(initialData.descricao || '');
setSaldo(String(initialData.saldo ?? 0));
- setDebito(String(initialData.debito ?? 0));
+ setDebito(String(initialData.debito ?? 1));
setHabilitado(String(initialData.habilitado ?? 1));
+ setDiaVencimento(
+ initialData.dia_vencimento ? String(initialData.dia_vencimento) : ''
+ );
}, [initialData]);
function limparFormulario() {
setDescricao('');
setSaldo('0');
- setDebito('0');
+ setDebito('1');
setHabilitado('1');
+ setDiaVencimento('');
}
async function handleSubmit(event: FormEvent) {
@@ -147,6 +152,15 @@ export function BancoForm({ mode, initialData }: BancoFormProps) {
return;
}
+ if (Number(debito) === 0) {
+ const dia = Number(diaVencimento);
+
+ if (!diaVencimento || !Number.isInteger(dia) || dia < 1 || dia > 31) {
+ setErro('Informe um dia de vencimento válido entre 1 e 31 para banco de crédito.');
+ return;
+ }
+ }
+
try {
setSaving(true);
setErro('');
@@ -155,8 +169,9 @@ export function BancoForm({ mode, initialData }: BancoFormProps) {
const payload: CriarBancoRequest = {
descricao: descricao.trim(),
saldo: Number(saldo || 0),
- debito: Number(debito || 0),
+ debito: Number(debito),
habilitado: Number(habilitado || 1),
+ dia_vencimento: Number(debito) === 0 ? Number(diaVencimento) : null,
};
if (isEdit) {
@@ -275,6 +290,23 @@ export function BancoForm({ mode, initialData }: BancoFormProps) {
+ {Number(debito) === 0 && (
+ setDiaVencimento(event.target.value)}
+ inputProps={{
+ min: 1,
+ max: 31,
+ step: 1,
+ }}
+ helperText="Usado para preencher automaticamente o vencimento das saídas no crédito."
+ fullWidth
+ sx={fieldSx}
+ />
+ )}
+
+ {Number(debito) === 0 && (
+
+
+ Dia de vencimento
+
+
+ {diaVencimento ? `Dia ${diaVencimento}` : 'Não informado'}
+
+
+ )}
+
Saldo
diff --git a/financeiro-web/src/features/bancos/pages/BancosPage.tsx b/financeiro-web/src/features/bancos/pages/BancosPage.tsx
index 44145c9..6cdc14e 100644
--- a/financeiro-web/src/features/bancos/pages/BancosPage.tsx
+++ b/financeiro-web/src/features/bancos/pages/BancosPage.tsx
@@ -77,6 +77,13 @@ function statusBancoColor(habilitado: number) {
return Number(habilitado) === 1 ? 'success' : 'default';
}
+function vencimentoBancoLabel(item: Banco) {
+ if (Number(item.debito) !== 0) return '-';
+ if (!item.dia_vencimento) return 'Não informado';
+
+ return `Dia ${item.dia_vencimento}`;
+}
+
export function BancosPage() {
const [bancos, setBancos] = useState([]);
const [pagination, setPagination] = useState({
@@ -103,6 +110,7 @@ export function BancosPage() {
const [busca, setBusca] = useState('');
const [debito, setDebito] = useState('');
const [habilitado, setHabilitado] = useState('');
+ const [diaVencimento, setDiaVencimento] = useState('');
const [orderBy, setOrderBy] = useState('descricao');
const [orderDirection, setOrderDirection] = useState<'ASC' | 'DESC'>('ASC');
@@ -115,9 +123,10 @@ export function BancosPage() {
if (busca.trim()) count += 1;
if (debito !== '') count += 1;
if (habilitado !== '') count += 1;
+ if (diaVencimento !== '') count += 1;
return count;
- }, [busca, debito, habilitado]);
+ }, [busca, debito, habilitado, diaVencimento]);
async function carregarBancos(pageToLoad = page) {
try {
@@ -130,6 +139,7 @@ export function BancosPage() {
busca: busca.trim() || undefined,
debito,
habilitado,
+ dia_vencimento: diaVencimento,
orderBy,
orderDirection,
};
@@ -160,6 +170,7 @@ export function BancosPage() {
setBusca('');
setDebito('');
setHabilitado('');
+ setDiaVencimento('');
setOrderBy('descricao');
setOrderDirection('ASC');
@@ -326,7 +337,7 @@ export function BancosPage() {
- Carteiras de crédito
+ Dívida em crédito
{formatarValor(summary.saldoCarteiras)}
@@ -337,7 +348,7 @@ export function BancosPage() {
- Carteiras de débito
+ Saldo em débito
{formatarValor(summary.saldoDebito)}
@@ -397,6 +408,23 @@ export function BancosPage() {
+
+ setDiaVencimento(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ inputProps={{
+ min: 1,
+ max: 31,
+ step: 1,
+ }}
+ disabled={debito === 1}
+ helperText={debito === 1 ? 'Disponível apenas para crédito' : 'Filtra bancos de crédito'}
+ fullWidth
+ />
+
Descrição
+
@@ -606,6 +635,7 @@ export function BancosPage() {
Descrição
Tipo
+ Vencimento
Saldo
Cadastro
Atualização
@@ -641,6 +671,21 @@ export function BancosPage() {
/>
+
+ {Number(item.debito) === 0 ? (
+
+ ) : (
+
+ -
+
+ )}
+
+
= 0 ? '+' : ''}${valor.toFixed(1)}%`;
+}
+
+function variacaoIcone(valor: number) {
+ if (valor >= 0) return ;
+ return ;
+}
+
+const pieColors = [
+ '#1565C0',
+ '#2E7D32',
+ '#ED6C02',
+ '#7B1FA2',
+ '#D32F2F',
+ '#00897B',
+ '#5D4037',
+ '#455A64',
+];
+
+type StatCardProps = {
+ title: string;
+ value: string;
+ subtitle?: string;
+ icon: React.ReactNode;
+ color?: 'success' | 'error' | 'warning' | 'info' | 'primary';
+ percent?: number | null;
+ percentLabel?: string;
+};
+
+function StatCard({
+ title,
+ value,
+ subtitle,
+ icon,
+ color = 'primary',
+ percent,
+ percentLabel,
+}: StatCardProps) {
+ const palette: Record = {
+ success: 'rgba(46,125,50,0.10)',
+ error: 'rgba(211,47,47,0.10)',
+ warning: 'rgba(237,108,2,0.10)',
+ info: 'rgba(2,136,209,0.10)',
+ primary: 'rgba(21,101,192,0.10)',
+ };
+
+ const textColor: Record = {
+ success: 'success.main',
+ error: 'error.main',
+ warning: 'warning.main',
+ info: 'info.main',
+ primary: 'primary.main',
+ };
+
+ return (
+
+
+
+
+ {icon}
+
+
+
+
+ {title}
+
+
+
+ {value}
+
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ {percent !== undefined && percent !== null && (
+ = 0 ? color as any : 'error'}
+ variant="outlined"
+ sx={{ marginTop: 0.75, fontWeight: 800 }}
+ />
+ )}
+
+
+
+
+ );
+}
+
+function MovimentoLinha({ item }: { item: DashboardMovimentoResumo }) {
+ const isEntrada = isMovimentoEntrada(item.movimento);
+
+ return (
+
+
+
+
+
+ {item.descricao}
+ {Number(item.parcelas || 1) > 1
+ ? ` - ${item.parcela}/${item.parcelas}`
+ : ''}
+
+
+
+
+
+
+ {item.banco_descricao || 'Sem banco'} · {formatarData(item.datavencimento)}
+
+
+ {item.centro_custo_descricao && (
+
+ {item.centro_custo_descricao}
+
+ )}
+
+ {item.dias_para_vencer !== undefined && (
+
+ {" - " + vencimentoLabel(item.dias_para_vencer)}
+
+ )}
+
+
+
+
+ {formatarValor(item.valor)}
+
+
+
+
+
+
+
+
+ );
+}
+
+function CreditoLinha({ item }: { item: DashboardCredito }) {
+ return (
+
+
+
+
+
+ {item.descricao}
+
+
+
+ {item.dia_vencimento ? `Vencimento dia ${item.dia_vencimento}` : 'Sem vencimento'}
+
+
+
+ {item.quantidade_aberta} conta(s) em aberto
+
+
+
+
+
+ Dívida acumulada
+
+
+
+ {formatarValor(item.saldo)}
+
+
+
+
+
+
+
+ Fatura do período
+
+
+ {formatarValor(item.fatura_atual)}
+
+
+ {item.quantidade_fatura_atual} lançamento(s)
+
+
+
+
+
+ Próximas faturas
+
+
+ {formatarValor(item.proximas_faturas)}
+
+
+ {item.quantidade_proximas_faturas} lançamento(s)
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function CarteiraLinha({ item }: { item: DashboardCarteira }) {
+ const isCredito = Number(item.debito) === 0;
+ const variacaoBoa = Boolean(item.tendencia_boa);
+ const variacaoValor = Number(item.variacao_valor || 0);
+
+ return (
+
+
+
+
+ {item.descricao}
+
+
+
+ {isCredito ? 'Crédito' : 'Débito'}
+ {isCredito && item.dia_vencimento ? ` · Dia ${item.dia_vencimento}` : ''}
+
+
+
+
+
+
+ vs período anterior
+
+
+
+
+
+
+ {isCredito ? 'Dívida' : 'Saldo'}
+
+
+ = 0
+ ? 'success.main'
+ : 'error.main'
+ }
+ >
+ {formatarValor(item.saldo)}
+
+
+
+ Antes: {formatarValor(item.saldo_mes_anterior)}
+
+
+
+
+ );
+}
+
+function GraficosDashboard({ dashboard }: { dashboard: DashboardResumo }) {
+ const gastos = dashboard.graficos?.gastosPorCentroCusto || [];
+ const entradasSaidas = dashboard.graficos?.entradasSaidas;
+
+ const barrasData = [
+ {
+ nome: 'Entradas',
+ valor: entradasSaidas?.entradas || 0,
+ percentual: entradasSaidas?.percentualEntradas || 0,
+ },
+ {
+ nome: 'Saídas',
+ valor: entradasSaidas?.saidas || 0,
+ percentual: entradasSaidas?.percentualSaidas || 0,
+ },
+ {
+ nome: 'Investimentos',
+ valor: entradasSaidas?.investimentos || 0,
+ percentual: entradasSaidas?.percentualInvestimentos || 0,
+ },
+ {
+ nome: 'Resultado',
+ valor: entradasSaidas?.resultado || 0,
+ percentual: entradasSaidas?.percentualResultado || 0,
+ },
+ ];
+
+ return (
+
+
+
+
+ Gastos por centro de custo
+
+
+
+ Distribuição das despesas operacionais no período selecionado.
+
+
+ {gastos.length === 0 ? (
+
+ Nenhum gasto encontrado no período.
+
+ ) : (
+
+
+
+ entry.descricao}
+ >
+ {gastos.map((_, index) => (
+ |
+ ))}
+
+ formatarValor(Number(value || 0))}
+ />
+
+
+
+ )}
+
+
+
+
+
+
+ Entradas, saídas, investimentos e resultado
+
+
+
+ Percentuais calculados com base no total de entradas.
+
+
+
+
+
+
+
+ `${Number(value) / 1000}k`} />
+ {
+ const percentual = props?.payload?.percentual;
+ return [
+ `${formatarValor(Number(value || 0))} (${formatarPercentual(percentual)})`,
+ 'Valor',
+ ];
+ }}
+ />
+
+ {barrasData.map((item, index) => (
+ = 0
+ ? '#1565C0'
+ : '#ED6C02'
+ }
+ />
+ ))}
+ |
+
+
+
+
+ {barrasData.map((item) => (
+ = 0
+ ? 'primary'
+ : 'warning'
+ }
+ sx={{ fontWeight: 800 }}
+ />
+ ))}
+
+
+
+
+ );
+}
+
+export function DashboardPage() {
+ const [dataInicio, setDataInicio] = useState(inicioMesAtual());
+ const [dataFim, setDataFim] = useState(fimMesAtual());
+
+ const [dashboard, setDashboard] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [erro, setErro] = useState('');
+
+ const cards = dashboard?.cards;
+
+ const resultadoColor = useMemo(() => {
+ if (!cards) return 'primary';
+ return cards.resultado >= 0 ? 'success' : 'error';
+ }, [cards]);
+
+ async function carregarDashboard() {
+ try {
+ setLoading(true);
+ setErro('');
+
+ const data = await buscarDashboardResumo({
+ dataInicio,
+ dataFim,
+ });
+
+ setDashboard(data);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar a dashboard.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ carregarDashboard();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+
+
+
+
+ Dashboard
+
+
+
+ Visão geral de saldos, vencimentos, crédito e movimentações.
+
+
+
+
+ }
+ sx={{
+ minHeight: 48,
+ px: 2.5,
+ borderRadius: 3,
+ boxShadow: 3,
+ }}
+ >
+ Novo movimento
+
+
+ }
+ onClick={carregarDashboard}
+ disabled={loading}
+ sx={{
+ minHeight: 48,
+ px: 2.5,
+ borderRadius: 3,
+ }}
+ >
+ Atualizar
+
+
+
+
+ {erro && (
+
+ {erro}
+
+ )}
+
+
+
+
+
+
+ Período analisado
+
+
+ Os cards de entradas, saídas e resultado usam esse intervalo.
+
+
+
+
+ setDataInicio(event.target.value)}
+ InputLabelProps={{ shrink: true }}
+ size="small"
+ />
+
+ setDataFim(event.target.value)}
+ InputLabelProps={{ shrink: true }}
+ size="small"
+ />
+
+
+
+
+
+
+
+ {loading ? (
+
+
+ Carregando dashboard...
+
+ ) : !dashboard || !cards ? (
+
+ Nenhum dado encontrado.
+
+ ) : (
+ <>
+
+ }
+ color="success"
+ />
+
+ }
+ color="warning"
+ />
+
+ }
+ color="success"
+ percent={cards.percentualEntradas}
+ percentLabel="base"
+ />
+
+ }
+ color="error"
+ percent={cards.percentualSaidas}
+ percentLabel="das entradas"
+ />
+
+ }
+ color="info"
+ percent={cards.percentualInvestimentos}
+ percentLabel="das entradas"
+ />
+
+ }
+ color={resultadoColor as any}
+ percent={cards.percentualResultado}
+ percentLabel="das entradas"
+ />
+
+ }
+ color="success"
+ />
+
+ }
+ color="error"
+ />
+
+ }
+ color="info"
+ />
+
+ }
+ color={cards.vencidos > 0 ? 'error' : 'success'}
+ />
+
+ }
+ color="success"
+ />
+
+ }
+ color="warning"
+ />
+
+ }
+ color="info"
+ />
+
+
+
+
+
+
+
+
+
+
+ Próximos vencimentos
+
+
+ Contas abertas ordenadas por vencimento.
+
+
+
+
+
+
+
+
+ {dashboard.proximosVencimentos.length === 0 ? (
+
+ Nenhum vencimento em aberto.
+
+ ) : (
+ }>
+ {dashboard.proximosVencimentos.map((item) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ Créditos em aberto
+
+
+
+ Bancos de crédito com dívida acumulada.
+
+
+
+
+ {dashboard.creditos.length === 0 ? (
+
+ Nenhum crédito em aberto.
+
+ ) : (
+ }>
+ {dashboard.creditos.map((item) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+
+ Carteiras
+
+
+
+ Saldos de débito e dívidas de crédito habilitadas.
+
+
+
+
+ {dashboard.carteiras.length === 0 ? (
+
+ Nenhuma carteira habilitada.
+
+ ) : (
+ }>
+ {dashboard.carteiras.map((item) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+
+ Últimos movimentos
+
+
+
+ Lançamentos mais recentes cadastrados no sistema.
+
+
+
+ }
+ sx={{ borderRadius: 2 }}
+ >
+ Movimentos
+
+
+
+
+
+ {dashboard.ultimosMovimentos.length === 0 ? (
+
+ Nenhum movimento cadastrado.
+
+ ) : (
+ }>
+ {dashboard.ultimosMovimentos.map((item) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ Período carregado: {formatarData(dashboard.periodo.dataInicio)} até{' '}
+ {formatarData(dashboard.periodo.dataFim)}.
+
+
+ >
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/dashboard/services/dashboardService.ts b/financeiro-web/src/features/dashboard/services/dashboardService.ts
new file mode 100644
index 0000000..b1fb1d3
--- /dev/null
+++ b/financeiro-web/src/features/dashboard/services/dashboardService.ts
@@ -0,0 +1,20 @@
+import { api } from '../../../services/api';
+import type {
+ ApiResponse,
+ DashboardResumo,
+} from '../types/dashboardTypes';
+
+export type BuscarDashboardResumoParams = {
+ dataInicio?: string;
+ dataFim?: string;
+};
+
+export async function buscarDashboardResumo(
+ params?: BuscarDashboardResumoParams
+): Promise {
+ const response = await api.get>('/dashboard/resumo', {
+ params,
+ });
+
+ return response.data.data;
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/dashboard/types/dashboardTypees.ts b/financeiro-web/src/features/dashboard/types/dashboardTypees.ts
new file mode 100644
index 0000000..1e8f5bc
--- /dev/null
+++ b/financeiro-web/src/features/dashboard/types/dashboardTypees.ts
@@ -0,0 +1,146 @@
+export type DashboardPeriodo = {
+ dataInicio: string;
+ dataFim: string;
+};
+
+export type DashboardCards = {
+ saldoDebito: number;
+ dividaCredito: number;
+ quantidadeCarteiras: number;
+ carteirasHabilitadas: number;
+
+ entradas: number;
+ saidas: number;
+ resultado: number;
+
+ aberto: number;
+ abertoEntradas: number;
+ abertoSaidas: number;
+ quantidadeAbertoEntradas: number;
+ quantidadeAbertoSaidas: number;
+
+ baixado: number;
+ quantidadeMovimentos: number;
+
+ vencidos: number;
+ valorVencido: number;
+
+ venceHoje: number;
+ valorVenceHoje: number;
+
+ venceEmBreve: number;
+ valorVenceEmBreve: number;
+
+ venceEmBreveEntradas: number;
+ venceEmBreveSaidas: number;
+ venceEmBreveResultado: number;
+ quantidadeVenceEmBreveEntradas: number;
+ quantidadeVenceEmBreveSaidas: number;
+
+ investimentos: number;
+ percentualEntradas: number;
+ percentualSaidas: number;
+ percentualInvestimentos: number;
+ percentualResultado: number;
+
+ abertoInvestimentos: number;
+ quantidadeAbertoInvestimentos: number;
+
+ venceEmBreveInvestimentos: number;
+ quantidadeVenceEmBreveInvestimentos: number;
+};
+
+export type DashboardMovimentoResumo = {
+ idcontasapagar: number;
+ movimento: string;
+ descricao: string;
+ valor: number;
+ status: string;
+ dataentrada: string | null;
+ datavencimento: string | null;
+ databaixa: string | null;
+ parcela?: number;
+ parcelas?: number;
+ competencia?: string | null;
+ origem?: string | null;
+ saldo_processado?: number;
+ idbancos?: number | null;
+ banco_descricao?: string | null;
+ banco_debito?: number | null;
+ idcentrodecustos?: number | null;
+ centro_custo_descricao?: string | null;
+ dias_para_vencer?: number;
+};
+
+export type DashboardCredito = {
+ idbancos: number;
+ descricao: string;
+ saldo: number;
+ debito: number;
+ habilitado: number;
+ dia_vencimento: number | null;
+
+ quantidade_aberta: number;
+ total_aberto: number;
+
+ fatura_atual: number;
+ quantidade_fatura_atual: number;
+
+ proximas_faturas: number;
+ quantidade_proximas_faturas: number;
+};
+
+export type DashboardCarteira = {
+ idbancos: number;
+ descricao: string;
+ saldo: number;
+ debito: number;
+ habilitado: number;
+ dia_vencimento: number | null;
+ insert_date: string | null;
+ update_date: string | null;
+
+ saldo_mes_anterior: number;
+ variacao_valor: number;
+ variacao_percentual: number | null;
+ tendencia_boa: boolean;
+};
+
+export type DashboardGastoCentroCusto = {
+ idcentrodecustos: number | null;
+ descricao: string;
+ valor: number;
+};
+
+export type DashboardEntradasSaidasGrafico = {
+ entradas: number;
+ saidas: number;
+ investimentos: number;
+ resultado: number;
+
+ percentualEntradas: number;
+ percentualSaidas: number;
+ percentualInvestimentos: number;
+ percentualResultado: number;
+};
+
+export type DashboardGraficos = {
+ gastosPorCentroCusto: DashboardGastoCentroCusto[];
+ entradasSaidas: DashboardEntradasSaidasGrafico;
+};
+
+export type DashboardResumo = {
+ periodo: DashboardPeriodo;
+ cards: DashboardCards;
+ proximosVencimentos: DashboardMovimentoResumo[];
+ creditos: DashboardCredito[];
+ carteiras: DashboardCarteira[];
+ ultimosMovimentos: DashboardMovimentoResumo[];
+ graficos: DashboardGraficos;
+};
+
+export type ApiResponse = {
+ ok: boolean;
+ message?: string;
+ data: T;
+};
\ No newline at end of file
diff --git a/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx b/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx
index 06f9fc1..e85a027 100644
--- a/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx
+++ b/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx
@@ -61,6 +61,32 @@ function hojeISO() {
return new Date().toISOString().slice(0, 10);
}
+function calcularVencimentoCreditoProximoMes(diaVencimento: number, dataBaseISO?: string) {
+ const dataBase = dataBaseISO
+ ? new Date(`${dataBaseISO}T00:00:00`)
+ : new Date();
+
+ const ano = dataBase.getFullYear();
+ const mes = dataBase.getMonth();
+
+ const proximoMes = new Date(ano, mes + 1, 1);
+ const ultimoDiaProximoMes = new Date(
+ proximoMes.getFullYear(),
+ proximoMes.getMonth() + 1,
+ 0
+ ).getDate();
+
+ const diaSeguro = Math.min(Number(diaVencimento), ultimoDiaProximoMes);
+
+ return new Date(
+ proximoMes.getFullYear(),
+ proximoMes.getMonth(),
+ diaSeguro
+ )
+ .toISOString()
+ .slice(0, 10);
+}
+
function toNumberOrNull(value: string): number | null {
if (!value) return null;
return Number(value);
@@ -186,6 +212,12 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
const temParcelamento = Number(parcelas || 1) > 1;
+ const bancoSelecionado = useMemo(() => {
+ if (!idBanco) return null;
+
+ return bancos.find((banco) => String(banco.id) === String(idBanco)) || null;
+ }, [bancos, idBanco]);
+
const statusDisponiveis = useMemo(() => {
if (movimento === 'Entrada') return ['Recebido', 'A receber'];
if (movimento === 'Saida') return ['Pago', 'A pagar'];
@@ -286,6 +318,32 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
dataBaixa,
]);
+ useEffect(() => {
+ if (isEdit) return;
+ if (movimento !== 'Saida') return;
+ if (!bancoSelecionado) return;
+ if (Number(bancoSelecionado.debito) !== 0) return;
+ if (!bancoSelecionado.dia_vencimento) return;
+
+ const novaDataVencimento = calcularVencimentoCreditoProximoMes(
+ Number(bancoSelecionado.dia_vencimento),
+ dataEntrada
+ );
+
+ setDataVencimento(novaDataVencimento);
+
+ if (status === 'Pago') {
+ setStatus('A pagar');
+ setDataBaixa('');
+ }
+ }, [
+ isEdit,
+ movimento,
+ bancoSelecionado,
+ dataEntrada,
+ status,
+ ]);
+
function limparFormulario() {
setDescricao('');
setValor('');
@@ -617,6 +675,23 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
fullWidth
sx={fieldSx}
/>
+
+ setIdBanco(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+ {bancos.map((banco) => (
+
+ ))}
+
- setIdBanco(event.target.value)}
- fullWidth
- sx={fieldSx}
- >
-
- {bancos.map((banco) => (
-
- ))}
-
-
= {
+ manual: 'Manual',
+ parcelamento: 'Parcelamento',
+ movimento_fixo: 'Movimento fixo',
+ quitacao: 'Quitação',
+ ajuste_saldo: 'Ajuste de saldo',
+ };
+
+ return labels[origem] || origem;
+}
+
+function statusChipColor(status: string) {
+ if (status === 'Pago' || status === 'Recebido') return 'success';
+ if (status === 'A pagar' || status === 'A receber') return 'warning';
+
+ return 'default';
+}
+
+function impactoColor(valorDelta: number) {
+ return valorDelta >= 0 ? 'success.main' : 'error.main';
+}
+
+export function QuitacoesCreditoPage() {
+ const [bancosCredito, setBancosCredito] = useState([]);
+ const [bancosPagamento, setBancosPagamento] = useState([]);
+
+ const [idBancoCredito, setIdBancoCredito] = useState('');
+ const [idBancoPagamento, setIdBancoPagamento] = useState('');
+ const [dataInicio, setDataInicio] = useState(inicioMesAtual());
+ const [dataFim, setDataFim] = useState(fimMesAtual());
+ const [dataQuitacao, setDataQuitacao] = useState(hojeISO());
+ const [descricao, setDescricao] = useState('');
+
+ const [preview, setPreview] = useState(null);
+ const [idsSelecionados, setIdsSelecionados] = useState([]);
+ const [impactoSaldo, setImpactoSaldo] = useState([]);
+
+ const [loadingRefs, setLoadingRefs] = useState(true);
+ const [loadingPreview, setLoadingPreview] = useState(false);
+ const [quitando, setQuitando] = useState(false);
+ const [erro, setErro] = useState('');
+ const [sucesso, setSucesso] = useState('');
+
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
+
+ const bancoCreditoSelecionado = useMemo(() => {
+ return bancosCredito.find((banco) => obterIdBanco(banco) === Number(idBancoCredito)) || null;
+ }, [bancosCredito, idBancoCredito]);
+
+ const bancoPagamentoSelecionado = useMemo(() => {
+ return bancosPagamento.find((banco) => obterIdBanco(banco) === Number(idBancoPagamento)) || null;
+ }, [bancosPagamento, idBancoPagamento]);
+
+ const contas = preview?.contas || [];
+
+ const contasSelecionadas = useMemo(() => {
+ const selecionados = new Set(idsSelecionados);
+ return contas.filter((conta) => selecionados.has(conta.idcontasapagar));
+ }, [contas, idsSelecionados]);
+
+ const totalSelecionado = useMemo(() => {
+ return contasSelecionadas.reduce((total, conta) => total + Number(conta.valor || 0), 0);
+ }, [contasSelecionadas]);
+
+ const todasSelecionadas = contas.length > 0 && idsSelecionados.length === contas.length;
+ const algumasSelecionadas = idsSelecionados.length > 0 && idsSelecionados.length < contas.length;
+
+ const podeQuitar =
+ Boolean(idBancoCredito) &&
+ Boolean(idBancoPagamento) &&
+ idsSelecionados.length > 0 &&
+ totalSelecionado > 0 &&
+ !quitando &&
+ !loadingPreview;
+
+ async function carregarReferencias() {
+ try {
+ setLoadingRefs(true);
+ setErro('');
+
+ const [creditos, pagamentos] = await Promise.all([
+ listarBancosCredito(),
+ listarBancosPagamento(),
+ ]);
+
+ setBancosCredito(creditos);
+ setBancosPagamento(pagamentos);
+
+ if (!idBancoCredito && creditos.length > 0) {
+ setIdBancoCredito(obterIdBanco(creditos[0]));
+ }
+
+ if (!idBancoPagamento && pagamentos.length > 0) {
+ setIdBancoPagamento(obterIdBanco(pagamentos[0]));
+ }
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar os bancos para quitação.';
+
+ setErro(message);
+ } finally {
+ setLoadingRefs(false);
+ }
+ }
+
+ async function carregarPreview() {
+ if (!idBancoCredito) {
+ setErro('Selecione o banco/cartão de crédito.');
+ return;
+ }
+
+ try {
+ setLoadingPreview(true);
+ setErro('');
+ setSucesso('');
+ setImpactoSaldo([]);
+
+ const data = await buscarPreviewQuitacaoCredito({
+ idBancoCredito: Number(idBancoCredito),
+ dataInicio: dataInicio || undefined,
+ dataFim: dataFim || undefined,
+ limite: LIMITE_PADRAO,
+ page: 1,
+ });
+
+ setPreview(data);
+ setIdsSelecionados(data.contas.map((conta) => conta.idcontasapagar));
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar a prévia da quitação.';
+
+ setErro(message);
+ setPreview(null);
+ setIdsSelecionados([]);
+ } finally {
+ setLoadingPreview(false);
+ }
+ }
+
+ function alternarConta(idcontasapagar: number) {
+ setIdsSelecionados((current) => {
+ if (current.includes(idcontasapagar)) {
+ return current.filter((id) => id !== idcontasapagar);
+ }
+
+ return [...current, idcontasapagar];
+ });
+ }
+
+ function alternarTodas() {
+ if (todasSelecionadas) {
+ setIdsSelecionados([]);
+ return;
+ }
+
+ setIdsSelecionados(contas.map((conta) => conta.idcontasapagar));
+ }
+
+ function limparSelecao() {
+ setIdsSelecionados([]);
+ setImpactoSaldo([]);
+ setSucesso('');
+ }
+
+ async function handleQuitar() {
+ if (!idBancoCredito) {
+ setErro('Selecione o banco/cartão de crédito.');
+ return;
+ }
+
+ if (!idBancoPagamento) {
+ setErro('Selecione o banco/carteira que fará o pagamento.');
+ return;
+ }
+
+ if (Number(idBancoCredito) === Number(idBancoPagamento)) {
+ setErro('Banco de crédito e banco de pagamento não podem ser iguais.');
+ return;
+ }
+
+ if (idsSelecionados.length === 0) {
+ setErro('Selecione pelo menos uma conta para quitar.');
+ return;
+ }
+
+ const confirmou = window.confirm(
+ `Confirmar quitação de ${idsSelecionados.length} conta(s)?\n\n` +
+ `Banco de crédito: ${bancoCreditoSelecionado?.descricao || '-'}\n` +
+ `Banco de pagamento: ${bancoPagamentoSelecionado?.descricao || '-'}\n` +
+ `Valor total: ${formatarValor(totalSelecionado)}`
+ );
+
+ if (!confirmou) return;
+
+ try {
+ setQuitando(true);
+ setErro('');
+ setSucesso('');
+
+ const response = await criarQuitacaoCredito({
+ idBancoCredito: Number(idBancoCredito),
+ idBancoPagamento: Number(idBancoPagamento),
+ idsContas: idsSelecionados,
+ dataQuitacao,
+ descricao: descricao.trim() || null,
+ });
+
+ setSucesso(
+ `Quitação realizada com sucesso. Valor quitado: ${formatarValor(response.valor)}.`
+ );
+
+ setImpactoSaldo(response.impactoSaldo || []);
+ setIdsSelecionados([]);
+
+ await carregarReferencias();
+ await carregarPreview();
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível realizar a quitação.';
+
+ setErro(message);
+ } finally {
+ setQuitando(false);
+ }
+ }
+
+ useEffect(() => {
+ carregarReferencias();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ if (!idBancoCredito || loadingRefs) return;
+
+ carregarPreview();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [idBancoCredito]);
+
+ return (
+
+
+
+ }
+ label="Central de quitação"
+ color="primary"
+ variant="outlined"
+ sx={{ marginBottom: 1.25, fontWeight: 700 }}
+ />
+
+
+ Quitar créditos
+
+
+
+ Selecione os movimentos em aberto de um banco de crédito e baixe a dívida com uma carteira de pagamento.
+
+
+
+ : }
+ disabled={!podeQuitar}
+ onClick={handleQuitar}
+ sx={{
+ minHeight: 48,
+ borderRadius: 3,
+ px: 2.5,
+ boxShadow: 3,
+ }}
+ >
+ {quitando ? 'Quitando...' : 'Confirmar quitação'}
+
+
+
+ {erro && (
+
+ {erro}
+
+ )}
+
+ {sucesso && (
+
+ {sucesso}
+
+ )}
+
+
+
+
+
+
+
+
+ Prévia da quitação
+
+
+
+ {loadingRefs ? (
+
+
+ Carregando bancos...
+
+ ) : (
+ <>
+
+
+ setIdBancoCredito(
+ event.target.value === '' ? '' : Number(event.target.value)
+ )
+ }
+ fullWidth
+ >
+
+ {bancosCredito.map((banco) => (
+
+ ))}
+
+
+
+ setIdBancoPagamento(
+ event.target.value === '' ? '' : Number(event.target.value)
+ )
+ }
+ fullWidth
+ >
+
+ {bancosPagamento.map((banco) => (
+
+ ))}
+
+
+ setDataInicio(event.target.value)}
+ InputLabelProps={{ shrink: true }}
+ fullWidth
+ />
+
+ setDataFim(event.target.value)}
+ InputLabelProps={{ shrink: true }}
+ fullWidth
+ />
+
+ setDataQuitacao(event.target.value)}
+ InputLabelProps={{ shrink: true }}
+ fullWidth
+ />
+
+ setDescricao(event.target.value)}
+ placeholder="Ex: Quitação cartão Maio/2026"
+ fullWidth
+ sx={{
+ gridColumn: {
+ xs: 'auto',
+ md: 'span 3',
+ },
+ }}
+ />
+
+
+
+ }
+ onClick={limparSelecao}
+ >
+ Limpar seleção
+
+
+ : }
+ onClick={carregarPreview}
+ disabled={loadingPreview || !idBancoCredito}
+ >
+ {loadingPreview ? 'Carregando...' : 'Atualizar prévia'}
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ Contas em aberto
+
+
+ {preview?.summary?.quantidade || 0}
+
+
+
+
+
+
+
+ Total em aberto
+
+
+ {formatarValor(preview?.summary?.totalAberto || 0)}
+
+
+
+
+
+
+
+ Selecionado
+
+
+ {formatarValor(totalSelecionado)}
+
+
+
+
+
+
+
+ Saldo crédito após quitação
+
+
+ {formatarValor(
+ Number(bancoCreditoSelecionado?.saldo || 0) - totalSelecionado
+ )}
+
+
+
+
+
+
+
+ {loadingPreview ? (
+
+
+ Carregando contas em aberto...
+
+ ) : contas.length === 0 ? (
+
+
+ Nenhuma conta em aberto encontrada.
+
+
+ Selecione um banco de crédito ou ajuste o período da prévia.
+
+
+ ) : isMobile ? (
+ }>
+ {contas.map((conta) => {
+ const selecionado = idsSelecionados.includes(conta.idcontasapagar);
+
+ return (
+
+
+
+ alternarConta(conta.idcontasapagar)}
+ />
+ }
+ label={
+
+
+ {conta.descricao}
+
+
+ Vencimento: {formatarData(conta.datavencimento)}
+
+
+ }
+ sx={{ margin: 0 }}
+ />
+
+
+ {formatarValor(conta.valor)}
+
+
+
+
+
+
+
+
+
+
+ Centro: {conta.centro_custo_descricao || '-'}
+
+
+
+ );
+ })}
+
+ ) : (
+
+
+
+
+
+
+
+ Descrição
+ Entrada
+ Vencimento
+ Valor
+ Parcela
+ Situação
+ Competência
+ Origem
+ Centro de custo
+ Observação
+
+
+
+
+ {contas.map((conta) => {
+ const selecionado = idsSelecionados.includes(conta.idcontasapagar);
+
+ return (
+
+
+ alternarConta(conta.idcontasapagar)}
+ />
+
+
+
+
+
+ {conta.descricao}
+
+
+
+
+ {formatarData(conta.dataentrada)}
+ {formatarData(conta.datavencimento)}
+
+
+
+ {formatarValor(conta.valor)}
+
+
+
+
+ {conta.parcela}/{conta.parcelas}
+
+
+
+
+
+
+ {conta.competencia || '-'}
+
+
+
+
+
+
+
+ {conta.centro_custo_descricao || '-'}
+
+
+
+
+
+
+ {conta.observacao || '-'}
+
+
+
+
+ );
+ })}
+
+
+
+ )}
+
+
+
+
+
+
+ Resumo da baixa
+
+
+
+ Confira o impacto antes de confirmar a operação.
+
+
+
+
+
+
+
+
+
+ Crédito quitado
+
+
+
+ {bancoCreditoSelecionado?.descricao || 'Selecione'}
+
+
+ Saldo atual: {formatarValor(bancoCreditoSelecionado?.saldo || 0)}
+
+
+
+
+
+
+
+ Pagamento saindo de
+
+
+
+ {bancoPagamentoSelecionado?.descricao || 'Selecione'}
+
+
+ Saldo atual: {formatarValor(bancoPagamentoSelecionado?.saldo || 0)}
+
+
+
+
+
+
+
+ Contas selecionadas
+
+
+ {idsSelecionados.length}
+
+
+
+
+
+ Valor da quitação
+
+
+ {formatarValor(totalSelecionado)}
+
+
+
+
+
+ Após confirmar
+
+
+
+
+
+ {bancoPagamentoSelecionado?.descricao || 'Banco pagador'}
+
+
+ -{formatarValor(totalSelecionado)}
+
+
+ {formatarValor(bancoPagamentoSelecionado?.saldo || 0)} →{' '}
+ {formatarValor(Number(bancoPagamentoSelecionado?.saldo || 0) - totalSelecionado)}
+
+
+
+
+
+ {bancoCreditoSelecionado?.descricao || 'Banco de crédito'}
+
+
+ -{formatarValor(totalSelecionado)}
+
+
+ {formatarValor(bancoCreditoSelecionado?.saldo || 0)} →{' '}
+ {formatarValor(Number(bancoCreditoSelecionado?.saldo || 0) - totalSelecionado)}
+
+
+
+
+
+ {impactoSaldo.length > 0 && (
+ <>
+
+
+
+
+ Impacto registrado pela API
+
+
+
+ {impactoSaldo.map((impacto, index) => (
+
+
+ {impacto.banco_descricao || `Banco ${impacto.idbancos}`}
+
+
+
+ {impacto.valor_delta >= 0 ? '+' : ''}
+ {formatarValor(impacto.valor_delta)}
+
+
+
+ {formatarValor(impacto.saldo_anterior)} →{' '}
+ {formatarValor(impacto.saldo_posterior)}
+
+
+ ))}
+
+
+ >
+ )}
+
+ : }
+ disabled={!podeQuitar}
+ onClick={handleQuitar}
+ sx={{
+ minHeight: 48,
+ borderRadius: 3,
+ boxShadow: 3,
+ }}
+ >
+ {quitando ? 'Quitando...' : 'Quitar selecionados'}
+
+
+
+
+
+ );
+}
diff --git a/financeiro-web/src/features/quitacoesCredito/services/quitacoesCreditoService.ts b/financeiro-web/src/features/quitacoesCredito/services/quitacoesCreditoService.ts
new file mode 100644
index 0000000..5ebbc81
--- /dev/null
+++ b/financeiro-web/src/features/quitacoesCredito/services/quitacoesCreditoService.ts
@@ -0,0 +1,80 @@
+import { api } from '../../../services/api';
+import type {
+ ApiResponse,
+ BancoQuitacao,
+ CriarQuitacaoCreditoRequest,
+ QuitacaoCreditoHistoricoItem,
+ QuitacaoCreditoPreview,
+ QuitacaoCreditoPreviewParams,
+ QuitacaoCreditoResponse,
+} from '../types/quitacaoCreditoTypes';
+
+function unwrapResponse(payload: ApiResponse | T): T {
+ if (
+ payload &&
+ typeof payload === 'object' &&
+ 'data' in payload &&
+ 'ok' in payload
+ ) {
+ return (payload as ApiResponse).data;
+ }
+
+ return payload as T;
+}
+
+export async function listarBancosCredito(): Promise {
+ const response = await api.get | BancoQuitacao[]>(
+ '/quitacoes-credito/bancos-credito'
+ );
+
+ return unwrapResponse(response.data);
+}
+
+export async function listarBancosPagamento(): Promise {
+ const response = await api.get | BancoQuitacao[]>(
+ '/quitacoes-credito/bancos-pagamento'
+ );
+
+ return unwrapResponse(response.data);
+}
+
+export async function buscarPreviewQuitacaoCredito(
+ params: QuitacaoCreditoPreviewParams
+): Promise {
+ const response = await api.get | QuitacaoCreditoPreview>(
+ '/quitacoes-credito/preview',
+ {
+ params,
+ }
+ );
+
+ return unwrapResponse(response.data);
+}
+
+export async function criarQuitacaoCredito(
+ data: CriarQuitacaoCreditoRequest
+): Promise {
+ const response = await api.post | QuitacaoCreditoResponse>(
+ '/quitacoes-credito',
+ data
+ );
+
+ return unwrapResponse(response.data);
+}
+
+export async function listarHistoricoQuitacoesCredito(params?: {
+ dataInicio?: string;
+ dataFim?: string;
+ idBancoCredito?: number | '';
+ idBancoPagamento?: number | '';
+ limite?: number;
+ page?: number;
+}): Promise {
+ const response = await api.get<
+ ApiResponse | QuitacaoCreditoHistoricoItem[]
+ >('/quitacoes-credito/historico', {
+ params,
+ });
+
+ return unwrapResponse(response.data);
+}
diff --git a/financeiro-web/src/features/quitacoesCredito/types/quitacaoCreditoTypes.ts b/financeiro-web/src/features/quitacoesCredito/types/quitacaoCreditoTypes.ts
new file mode 100644
index 0000000..39fe419
--- /dev/null
+++ b/financeiro-web/src/features/quitacoesCredito/types/quitacaoCreditoTypes.ts
@@ -0,0 +1,110 @@
+export type BancoQuitacao = {
+ id?: number;
+ idbancos?: number;
+ descricao: string;
+ saldo: number;
+ debito: number;
+ habilitado?: number | null;
+};
+
+export type ContaCreditoQuitacao = {
+ idcontasapagar: number;
+ movimento: string;
+ descricao: string;
+ dataentrada: string | null;
+ datavencimento: string | null;
+ databaixa: string | null;
+ valor: number;
+ parcela: number;
+ parcelas: number;
+ status: string;
+ idcentrodecustos: number | null;
+ idbancos: number | null;
+ idbancos_p: number | null;
+ competencia: string | null;
+ origem: string | null;
+ observacao: string | null;
+ banco_descricao?: string | null;
+ centro_custo_descricao?: string | null;
+};
+
+export type QuitacaoCreditoPagination = {
+ total: number;
+ limite: number;
+ offset: number;
+ page: number;
+ totalPages: number;
+};
+
+export type QuitacaoCreditoSummary = {
+ quantidade: number;
+ totalAberto: number;
+ saldoAtualCredito: number;
+ saldoAposQuitacaoTotal: number;
+};
+
+export type QuitacaoCreditoPreview = {
+ bancoCredito: BancoQuitacao;
+ contas: ContaCreditoQuitacao[];
+ pagination: QuitacaoCreditoPagination;
+ summary: QuitacaoCreditoSummary;
+};
+
+export type QuitacaoCreditoPreviewParams = {
+ idBancoCredito: number;
+ dataInicio?: string;
+ dataFim?: string;
+ limite?: number;
+ page?: number;
+};
+
+export type CriarQuitacaoCreditoRequest = {
+ idBancoCredito: number;
+ idBancoPagamento: number;
+ idsContas: number[];
+ dataQuitacao: string;
+ descricao?: string | null;
+};
+
+export type QuitacaoCreditoImpactoSaldo = {
+ idbancos: number;
+ banco_descricao?: string | null;
+ valor_delta: number;
+ saldo_anterior: number;
+ saldo_posterior: number;
+ descricao?: string | null;
+};
+
+export type QuitacaoCreditoResponse = {
+ idcontasquitadas: number;
+ idbancos: number;
+ idbancos_p: number;
+ valor: number;
+ descricao: string | null;
+ data_quitacao: string | null;
+ contasQuitadas?: ContaCreditoQuitacao[];
+ impactoSaldo?: QuitacaoCreditoImpactoSaldo[];
+};
+
+export type QuitacaoCreditoHistoricoItem = {
+ idcontasquitadas: number;
+ idbancos: number;
+ idbancos_p: number;
+ banco_pagamento_descricao?: string | null;
+ banco_credito_descricao?: string | null;
+ valor: number;
+ descricao: string | null;
+ data_quitacao: string | null;
+ idusuarios: number | null;
+ usuario_nome?: string | null;
+ quantidade_contas?: number;
+ insert_date: string | null;
+ update_date: string | null;
+ deleted_at: string | null;
+};
+
+export type ApiResponse = {
+ ok: boolean;
+ message?: string;
+ data: T;
+};
diff --git a/financeiro-web/src/features/referencias/types/referenciasTypes.ts b/financeiro-web/src/features/referencias/types/referenciasTypes.ts
index ae524c4..12c6f41 100644
--- a/financeiro-web/src/features/referencias/types/referenciasTypes.ts
+++ b/financeiro-web/src/features/referencias/types/referenciasTypes.ts
@@ -3,6 +3,7 @@ export type Banco = {
descricao: string;
saldo: number | null;
debito: number | null;
+ dia_vencimento?: number | null;
};
export type CentroCusto = {
diff --git a/financeiro-web/src/pages/DashboardPage.tsx b/financeiro-web/src/pages/DashboardPage.tsx
deleted file mode 100644
index 6d2ba61..0000000
--- a/financeiro-web/src/pages/DashboardPage.tsx
+++ /dev/null
@@ -1,230 +0,0 @@
-import {
- Box,
- Button,
- Card,
- CardContent,
- Chip,
- Stack,
- Typography,
-} from '@mui/material';
-import AddIcon from '@mui/icons-material/Add';
-import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
-import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
-import AssessmentIcon from '@mui/icons-material/Assessment';
-import { Link as RouterLink } from 'react-router-dom';
-
-export function DashboardPage() {
- return (
-
-
-
-
-
-
- Dashboard
-
-
-
- Seu centro de comando para entradas, saídas, sangrias e estornos.
-
-
-
- }
- sx={{
- minHeight: 52,
- px: 3,
- borderRadius: 3,
- boxShadow: 3,
- }}
- >
- Novo movimento
-
-
-
-
-
-
-
-
-
-
-
-
-
- Movimentos
-
-
- Ativo
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Carteiras
-
-
- Em breve
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Relatórios
-
-
- Em breve
-
-
-
-
-
-
-
-
-
-
-
- Acesso rápido
-
-
-
- A operação principal já está disponível. Cadastre ou consulte os movimentos financeiros.
-
-
-
- }
- sx={{
- borderRadius: 2.5,
- minHeight: 44,
- }}
- >
- Cadastrar movimento
-
-
- }
- sx={{
- borderRadius: 2.5,
- minHeight: 44,
- }}
- >
- Ver movimentos
-
-
-
-
-
-
-
-
- Próximos passos
-
-
-
-
- • Melhorar lista de movimentos
-
-
- • Criar filtros por data, banco e status
-
-
- • Adicionar relatórios e saldos
-
-
-
-
-
-
- );
-}
\ No newline at end of file