adicionado dashboard

This commit is contained in:
Diego Freitas 2026-05-31 14:54:43 -03:00
parent c6418df307
commit 345d6033a8
25 changed files with 4344 additions and 257 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: <EditarUsuarioPage />,
},
{
path: '/quitacoes-credito',
element: <QuitacoesCreditoPage />,
}
],
},
],

View File

@ -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: <SwapHorizIcon />,
},
{
label: 'Quitar créditos',
path: '/quitacoes-credito',
icon: <CreditCardIcon />,
},
{
label: 'Clientes',
path: '/clientes',

View File

@ -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<HTMLFormElement>) {
@ -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) {
<MenuItem value="0">Crédito</MenuItem>
</TextField>
{Number(debito) === 0 && (
<TextField
label="Dia de vencimento"
type="number"
value={diaVencimento}
onChange={(event) => 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}
/>
)}
<TextField
label="Saldo"
type="number"
@ -386,6 +418,17 @@ export function BancoForm({ mode, initialData }: BancoFormProps) {
</Typography>
</Box>
{Number(debito) === 0 && (
<Box>
<Typography variant="caption" color="text.secondary">
Dia de vencimento
</Typography>
<Typography fontWeight={800}>
{diaVencimento ? `Dia ${diaVencimento}` : 'Não informado'}
</Typography>
</Box>
)}
<Box>
<Typography variant="caption" color="text.secondary">
Saldo

View File

@ -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<Banco[]>([]);
const [pagination, setPagination] = useState<BancosPagination>({
@ -103,6 +110,7 @@ export function BancosPage() {
const [busca, setBusca] = useState('');
const [debito, setDebito] = useState<number | ''>('');
const [habilitado, setHabilitado] = useState<number | ''>('');
const [diaVencimento, setDiaVencimento] = useState<number | ''>('');
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() {
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Carteiras de crédito
Dívida em crédito
</Typography>
<Typography variant="h5" fontWeight={950} color="success.main">
{formatarValor(summary.saldoCarteiras)}
@ -337,7 +348,7 @@ export function BancosPage() {
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Carteiras de débito
Saldo em débito
</Typography>
<Typography variant="h5" fontWeight={950} color="warning.main">
{formatarValor(summary.saldoDebito)}
@ -397,6 +408,23 @@ export function BancosPage() {
<MenuItem value={1}>Débito</MenuItem>
</TextField>
<TextField
label="Dia vencimento"
type="number"
value={diaVencimento}
onChange={(event) =>
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
/>
<TextField
select
label="Status"
@ -421,6 +449,7 @@ export function BancosPage() {
<MenuItem value="descricao">Descrição</MenuItem>
<MenuItem value="saldo">Saldo</MenuItem>
<MenuItem value="debito">Tipo</MenuItem>
<MenuItem value="dia_vencimento">Dia vencimento</MenuItem>
<MenuItem value="insert_date">Cadastro</MenuItem>
<MenuItem value="update_date">Atualização</MenuItem>
</TextField>
@ -606,6 +635,7 @@ export function BancosPage() {
<TableRow>
<TableCell sx={{ minWidth: 260 }}>Descrição</TableCell>
<TableCell sx={{ minWidth: 180 }}>Tipo</TableCell>
<TableCell sx={{ minWidth: 150 }}>Vencimento</TableCell>
<TableCell sx={{ minWidth: 140 }} align="right">Saldo</TableCell>
<TableCell sx={{ minWidth: 130 }}>Cadastro</TableCell>
<TableCell sx={{ minWidth: 130 }}>Atualização</TableCell>
@ -641,6 +671,21 @@ export function BancosPage() {
/>
</TableCell>
<TableCell>
{Number(item.debito) === 0 ? (
<Chip
label={vencimentoBancoLabel(item)}
size="small"
color="info"
variant="outlined"
/>
) : (
<Typography variant="body2" color="text.secondary">
-
</Typography>
)}
</TableCell>
<TableCell align="right">
<Typography
fontWeight={950}

View File

@ -16,6 +16,7 @@ export type ListarBancosParams = {
busca?: string;
debito?: number | '';
habilitado?: number | '';
dia_vencimento?: number | '';
orderBy?: string;
orderDirection?: OrderDirection;
};

View File

@ -4,6 +4,7 @@ export type Banco = {
saldo: number;
debito: number;
habilitado: number;
dia_vencimento: number | null;
insert_date: string | null;
update_date: string | null;
};
@ -13,6 +14,7 @@ export type CriarBancoRequest = {
saldo: number;
debito: number;
habilitado: number;
dia_vencimento: number | null;
};
export type AtualizarBancoRequest = CriarBancoRequest;

File diff suppressed because it is too large Load Diff

View File

@ -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<DashboardResumo> {
const response = await api.get<ApiResponse<DashboardResumo>>('/dashboard/resumo', {
params,
});
return response.data.data;
}

View File

@ -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<T> = {
ok: boolean;
message?: string;
data: T;
};

View File

@ -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<MovimentoStatus[]>(() => {
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}
/>
<TextField
select
label="Banco/Carteira"
value={idBanco}
onChange={(event) => setIdBanco(event.target.value)}
fullWidth
sx={fieldSx}
>
<MenuItem value="">Nenhum</MenuItem>
{bancos.map((banco) => (
<MenuItem key={banco.id} value={String(banco.id)}>
{banco.descricao}
{Number(banco.debito) === 0 ? ' · Crédito' : ''}
</MenuItem>
))}
</TextField>
</FormSection>
<FormSection
@ -733,22 +808,6 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
))}
</TextField>
<TextField
select
label="Banco/Carteira"
value={idBanco}
onChange={(event) => setIdBanco(event.target.value)}
fullWidth
sx={fieldSx}
>
<MenuItem value="">Nenhum</MenuItem>
{bancos.map((banco) => (
<MenuItem key={banco.id} value={String(banco.id)}>
{banco.descricao}
</MenuItem>
))}
</TextField>
<TextField
select
label="Referência"

View File

@ -0,0 +1,970 @@
import { useEffect, useMemo, useState } from 'react';
import {
Alert,
Box,
Button,
Card,
CardContent,
Checkbox,
Chip,
CircularProgress,
Divider,
FormControlLabel,
MenuItem,
Paper,
Stack,
TextField,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
useMediaQuery,
useTheme,
} from '@mui/material';
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import CreditCardIcon from '@mui/icons-material/CreditCard';
import PaymentsIcon from '@mui/icons-material/Payments';
import PreviewIcon from '@mui/icons-material/Preview';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import SearchIcon from '@mui/icons-material/Search';
import {
buscarPreviewQuitacaoCredito,
criarQuitacaoCredito,
listarBancosCredito,
listarBancosPagamento,
} from '../services/quitacoesCreditoService';
import type {
BancoQuitacao,
ContaCreditoQuitacao,
QuitacaoCreditoImpactoSaldo,
QuitacaoCreditoPreview,
} from '../types/quitacaoCreditoTypes';
const LIMITE_PADRAO = 100;
function hojeISO() {
return new Date().toISOString().slice(0, 10);
}
function inicioMesAtual() {
const hoje = new Date();
return new Date(hoje.getFullYear(), hoje.getMonth(), 1).toISOString().slice(0, 10);
}
function fimMesAtual() {
const hoje = new Date();
return new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0).toISOString().slice(0, 10);
}
function formatarValor(valor: number | string | null | undefined) {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(Number(valor || 0));
}
function formatarData(data: string | null | undefined) {
if (!data) return '-';
return new Date(`${String(data).slice(0, 10)}T00:00:00`).toLocaleDateString('pt-BR');
}
function obterIdBanco(banco: BancoQuitacao | null | undefined) {
return Number(banco?.id ?? banco?.idbancos ?? 0);
}
function origemLabel(origem: string | null | undefined) {
if (!origem) return 'Manual';
const labels: Record<string, string> = {
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<BancoQuitacao[]>([]);
const [bancosPagamento, setBancosPagamento] = useState<BancoQuitacao[]>([]);
const [idBancoCredito, setIdBancoCredito] = useState<number | ''>('');
const [idBancoPagamento, setIdBancoPagamento] = useState<number | ''>('');
const [dataInicio, setDataInicio] = useState(inicioMesAtual());
const [dataFim, setDataFim] = useState(fimMesAtual());
const [dataQuitacao, setDataQuitacao] = useState(hojeISO());
const [descricao, setDescricao] = useState('');
const [preview, setPreview] = useState<QuitacaoCreditoPreview | null>(null);
const [idsSelecionados, setIdsSelecionados] = useState<number[]>([]);
const [impactoSaldo, setImpactoSaldo] = useState<QuitacaoCreditoImpactoSaldo[]>([]);
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 (
<Box>
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', md: 'center' }}
spacing={2}
marginBottom={{ xs: 3, md: 4 }}
>
<Box>
<Chip
icon={<CreditCardIcon />}
label="Central de quitação"
color="primary"
variant="outlined"
sx={{ marginBottom: 1.25, fontWeight: 700 }}
/>
<Typography variant="h4" fontWeight={950}>
Quitar créditos
</Typography>
<Typography color="text.secondary" sx={{ marginTop: 0.5 }}>
Selecione os movimentos em aberto de um banco de crédito e baixe a dívida com uma carteira de pagamento.
</Typography>
</Box>
<Button
variant="contained"
startIcon={quitando ? <CircularProgress size={18} color="inherit" /> : <CheckCircleIcon />}
disabled={!podeQuitar}
onClick={handleQuitar}
sx={{
minHeight: 48,
borderRadius: 3,
px: 2.5,
boxShadow: 3,
}}
>
{quitando ? 'Quitando...' : 'Confirmar quitação'}
</Button>
</Stack>
{erro && (
<Alert severity="error" sx={{ marginBottom: 2.5 }}>
{erro}
</Alert>
)}
{sucesso && (
<Alert severity="success" sx={{ marginBottom: 2.5 }}>
{sucesso}
</Alert>
)}
<Stack
direction={{ xs: 'column', xl: 'row' }}
spacing={{ xs: 2.5, md: 3 }}
alignItems="flex-start"
>
<Box sx={{ width: '100%', flex: 1 }}>
<Card sx={{ marginBottom: { xs: 2.5, md: 3 } }}>
<CardContent sx={{ padding: { xs: 2, md: 3 } }}>
<Stack direction="row" spacing={1} alignItems="center" marginBottom={2.5}>
<PreviewIcon color="primary" />
<Typography variant="h6" fontWeight={900}>
Prévia da quitação
</Typography>
</Stack>
{loadingRefs ? (
<Box display="flex" alignItems="center" gap={2}>
<CircularProgress size={22} />
<Typography>Carregando bancos...</Typography>
</Box>
) : (
<>
<Box
display="grid"
gridTemplateColumns={{
xs: '1fr',
md: 'repeat(4, 1fr)',
}}
gap={{ xs: 2, md: 2.25 }}
>
<TextField
select
label="Banco/cartão de crédito"
value={idBancoCredito}
onChange={(event) =>
setIdBancoCredito(
event.target.value === '' ? '' : Number(event.target.value)
)
}
fullWidth
>
<MenuItem value="">Selecione</MenuItem>
{bancosCredito.map((banco) => (
<MenuItem key={obterIdBanco(banco)} value={obterIdBanco(banco)}>
{banco.descricao} · {formatarValor(banco.saldo)}
</MenuItem>
))}
</TextField>
<TextField
select
label="Banco/carteira de pagamento"
value={idBancoPagamento}
onChange={(event) =>
setIdBancoPagamento(
event.target.value === '' ? '' : Number(event.target.value)
)
}
fullWidth
>
<MenuItem value="">Selecione</MenuItem>
{bancosPagamento.map((banco) => (
<MenuItem key={obterIdBanco(banco)} value={obterIdBanco(banco)}>
{banco.descricao} · {formatarValor(banco.saldo)}
</MenuItem>
))}
</TextField>
<TextField
label="Data inicial"
type="date"
value={dataInicio}
onChange={(event) => setDataInicio(event.target.value)}
InputLabelProps={{ shrink: true }}
fullWidth
/>
<TextField
label="Data final"
type="date"
value={dataFim}
onChange={(event) => setDataFim(event.target.value)}
InputLabelProps={{ shrink: true }}
fullWidth
/>
<TextField
label="Data da quitação"
type="date"
value={dataQuitacao}
onChange={(event) => setDataQuitacao(event.target.value)}
InputLabelProps={{ shrink: true }}
fullWidth
/>
<TextField
label="Descrição da quitação"
value={descricao}
onChange={(event) => setDescricao(event.target.value)}
placeholder="Ex: Quitação cartão Maio/2026"
fullWidth
sx={{
gridColumn: {
xs: 'auto',
md: 'span 3',
},
}}
/>
</Box>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={1.5}
justifyContent="flex-end"
marginTop={2.5}
>
<Button
variant="outlined"
startIcon={<RestartAltIcon />}
onClick={limparSelecao}
>
Limpar seleção
</Button>
<Button
variant="contained"
startIcon={loadingPreview ? <CircularProgress size={18} color="inherit" /> : <SearchIcon />}
onClick={carregarPreview}
disabled={loadingPreview || !idBancoCredito}
>
{loadingPreview ? 'Carregando...' : 'Atualizar prévia'}
</Button>
</Stack>
</>
)}
</CardContent>
</Card>
<Box
display="grid"
gridTemplateColumns={{
xs: '1fr',
md: 'repeat(4, 1fr)',
}}
gap={{ xs: 2, md: 2.5 }}
marginBottom={{ xs: 2.5, md: 3 }}
>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Contas em aberto
</Typography>
<Typography variant="h5" fontWeight={950}>
{preview?.summary?.quantidade || 0}
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Total em aberto
</Typography>
<Typography variant="h5" fontWeight={950} color="warning.main">
{formatarValor(preview?.summary?.totalAberto || 0)}
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Selecionado
</Typography>
<Typography variant="h5" fontWeight={950} color="error.main">
{formatarValor(totalSelecionado)}
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Saldo crédito após quitação
</Typography>
<Typography variant="h5" fontWeight={950}>
{formatarValor(
Number(bancoCreditoSelecionado?.saldo || 0) - totalSelecionado
)}
</Typography>
</CardContent>
</Card>
</Box>
<Card>
<CardContent sx={{ padding: 0 }}>
{loadingPreview ? (
<Box padding={3} display="flex" alignItems="center" gap={2}>
<CircularProgress size={22} />
<Typography>Carregando contas em aberto...</Typography>
</Box>
) : contas.length === 0 ? (
<Box padding={3}>
<Typography fontWeight={800}>
Nenhuma conta em aberto encontrada.
</Typography>
<Typography color="text.secondary" marginTop={0.5}>
Selecione um banco de crédito ou ajuste o período da prévia.
</Typography>
</Box>
) : isMobile ? (
<Stack divider={<Divider />}>
{contas.map((conta) => {
const selecionado = idsSelecionados.includes(conta.idcontasapagar);
return (
<Box
key={conta.idcontasapagar}
sx={{
padding: 2,
backgroundColor: selecionado ? 'rgba(25,118,210,0.06)' : 'transparent',
}}
>
<Stack spacing={1.25}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-start"
spacing={2}
>
<FormControlLabel
control={
<Checkbox
checked={selecionado}
onChange={() => alternarConta(conta.idcontasapagar)}
/>
}
label={
<Box>
<Typography fontWeight={900}>
{conta.descricao}
</Typography>
<Typography variant="body2" color="text.secondary">
Vencimento: {formatarData(conta.datavencimento)}
</Typography>
</Box>
}
sx={{ margin: 0 }}
/>
<Typography fontWeight={950} whiteSpace="nowrap">
{formatarValor(conta.valor)}
</Typography>
</Stack>
<Stack direction="row" flexWrap="wrap" spacing={1} useFlexGap>
<Chip
label={conta.status}
size="small"
variant="outlined"
color={statusChipColor(conta.status) as any}
/>
<Chip
label={`${conta.parcela}/${conta.parcelas}`}
size="small"
variant="outlined"
/>
<Chip
label={origemLabel(conta.origem)}
size="small"
variant="outlined"
/>
</Stack>
<Typography variant="body2" color="text.secondary">
Centro: <strong>{conta.centro_custo_descricao || '-'}</strong>
</Typography>
</Stack>
</Box>
);
})}
</Stack>
) : (
<TableContainer sx={{ overflowX: 'auto' }}>
<Table
size="small"
sx={{
minWidth: 1180,
'& th': {
whiteSpace: 'nowrap',
fontWeight: 900,
backgroundColor: 'rgba(15,23,42,0.04)',
},
'& td': {
whiteSpace: 'nowrap',
},
}}
>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
checked={todasSelecionadas}
indeterminate={algumasSelecionadas}
onChange={alternarTodas}
/>
</TableCell>
<TableCell sx={{ minWidth: 260 }}>Descrição</TableCell>
<TableCell sx={{ minWidth: 120 }}>Entrada</TableCell>
<TableCell sx={{ minWidth: 130 }}>Vencimento</TableCell>
<TableCell sx={{ minWidth: 120 }} align="right">Valor</TableCell>
<TableCell sx={{ minWidth: 90 }} align="center">Parcela</TableCell>
<TableCell sx={{ minWidth: 120 }}>Situação</TableCell>
<TableCell sx={{ minWidth: 130 }}>Competência</TableCell>
<TableCell sx={{ minWidth: 150 }}>Origem</TableCell>
<TableCell sx={{ minWidth: 180 }}>Centro de custo</TableCell>
<TableCell sx={{ minWidth: 220 }}>Observação</TableCell>
</TableRow>
</TableHead>
<TableBody>
{contas.map((conta) => {
const selecionado = idsSelecionados.includes(conta.idcontasapagar);
return (
<TableRow
key={conta.idcontasapagar}
hover
selected={selecionado}
>
<TableCell padding="checkbox">
<Checkbox
checked={selecionado}
onChange={() => alternarConta(conta.idcontasapagar)}
/>
</TableCell>
<TableCell>
<Box sx={{ maxWidth: 260 }}>
<Typography fontWeight={800} noWrap title={conta.descricao}>
{conta.descricao}
</Typography>
</Box>
</TableCell>
<TableCell>{formatarData(conta.dataentrada)}</TableCell>
<TableCell>{formatarData(conta.datavencimento)}</TableCell>
<TableCell align="right">
<Typography fontWeight={950}>
{formatarValor(conta.valor)}
</Typography>
</TableCell>
<TableCell align="center">
{conta.parcela}/{conta.parcelas}
</TableCell>
<TableCell>
<Chip
label={conta.status}
size="small"
variant="outlined"
color={statusChipColor(conta.status) as any}
/>
</TableCell>
<TableCell>{conta.competencia || '-'}</TableCell>
<TableCell>
<Chip
label={origemLabel(conta.origem)}
size="small"
variant="outlined"
/>
</TableCell>
<TableCell>
<Typography
variant="body2"
noWrap
title={conta.centro_custo_descricao || '-'}
>
{conta.centro_custo_descricao || '-'}
</Typography>
</TableCell>
<TableCell>
<Box sx={{ maxWidth: 220 }}>
<Typography
variant="body2"
noWrap
title={conta.observacao || '-'}
>
{conta.observacao || '-'}
</Typography>
</Box>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
)}
</CardContent>
</Card>
</Box>
<Paper
elevation={0}
sx={{
width: { xs: '100%', xl: 360 },
padding: 3,
borderRadius: 4,
border: '1px solid',
borderColor: 'divider',
position: { xl: 'sticky' },
top: { xl: 24 },
background: 'linear-gradient(180deg, #FFFFFF 0%, #F8FAFC 100%)',
}}
>
<Typography variant="h6" fontWeight={900} gutterBottom>
Resumo da baixa
</Typography>
<Typography variant="body2" color="text.secondary" marginBottom={2.5}>
Confira o impacto antes de confirmar a operação.
</Typography>
<Divider sx={{ marginBottom: 2.5 }} />
<Stack spacing={2}>
<Box>
<Stack direction="row" spacing={1} alignItems="center">
<CreditCardIcon fontSize="small" color="primary" />
<Typography variant="caption" color="text.secondary">
Crédito quitado
</Typography>
</Stack>
<Typography fontWeight={800}>
{bancoCreditoSelecionado?.descricao || 'Selecione'}
</Typography>
<Typography variant="body2" color="text.secondary">
Saldo atual: {formatarValor(bancoCreditoSelecionado?.saldo || 0)}
</Typography>
</Box>
<Box>
<Stack direction="row" spacing={1} alignItems="center">
<AccountBalanceWalletIcon fontSize="small" color="primary" />
<Typography variant="caption" color="text.secondary">
Pagamento saindo de
</Typography>
</Stack>
<Typography fontWeight={800}>
{bancoPagamentoSelecionado?.descricao || 'Selecione'}
</Typography>
<Typography variant="body2" color="text.secondary">
Saldo atual: {formatarValor(bancoPagamentoSelecionado?.saldo || 0)}
</Typography>
</Box>
<Divider />
<Box>
<Typography variant="caption" color="text.secondary">
Contas selecionadas
</Typography>
<Typography variant="h5" fontWeight={950}>
{idsSelecionados.length}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Valor da quitação
</Typography>
<Typography variant="h4" fontWeight={950}>
{formatarValor(totalSelecionado)}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Após confirmar
</Typography>
<Stack spacing={1} marginTop={1}>
<Box
sx={{
padding: 1.25,
borderRadius: 2,
backgroundColor: 'rgba(15,23,42,0.04)',
}}
>
<Typography variant="body2" fontWeight={900}>
{bancoPagamentoSelecionado?.descricao || 'Banco pagador'}
</Typography>
<Typography variant="body2" fontWeight={900} color="error.main">
-{formatarValor(totalSelecionado)}
</Typography>
<Typography variant="caption" color="text.secondary">
{formatarValor(bancoPagamentoSelecionado?.saldo || 0)} {' '}
{formatarValor(Number(bancoPagamentoSelecionado?.saldo || 0) - totalSelecionado)}
</Typography>
</Box>
<Box
sx={{
padding: 1.25,
borderRadius: 2,
backgroundColor: 'rgba(15,23,42,0.04)',
}}
>
<Typography variant="body2" fontWeight={900}>
{bancoCreditoSelecionado?.descricao || 'Banco de crédito'}
</Typography>
<Typography variant="body2" fontWeight={900} color="error.main">
-{formatarValor(totalSelecionado)}
</Typography>
<Typography variant="caption" color="text.secondary">
{formatarValor(bancoCreditoSelecionado?.saldo || 0)} {' '}
{formatarValor(Number(bancoCreditoSelecionado?.saldo || 0) - totalSelecionado)}
</Typography>
</Box>
</Stack>
</Box>
{impactoSaldo.length > 0 && (
<>
<Divider />
<Box>
<Typography variant="caption" color="text.secondary">
Impacto registrado pela API
</Typography>
<Stack spacing={1} marginTop={1}>
{impactoSaldo.map((impacto, index) => (
<Box
key={`${impacto.idbancos}-${index}`}
sx={{
padding: 1.25,
borderRadius: 2,
backgroundColor: 'rgba(15,23,42,0.04)',
}}
>
<Typography variant="body2" fontWeight={900}>
{impacto.banco_descricao || `Banco ${impacto.idbancos}`}
</Typography>
<Typography
variant="body2"
fontWeight={900}
color={impactoColor(impacto.valor_delta)}
>
{impacto.valor_delta >= 0 ? '+' : ''}
{formatarValor(impacto.valor_delta)}
</Typography>
<Typography variant="caption" color="text.secondary">
{formatarValor(impacto.saldo_anterior)} {' '}
{formatarValor(impacto.saldo_posterior)}
</Typography>
</Box>
))}
</Stack>
</Box>
</>
)}
<Button
variant="contained"
startIcon={quitando ? <CircularProgress size={18} color="inherit" /> : <PaymentsIcon />}
disabled={!podeQuitar}
onClick={handleQuitar}
sx={{
minHeight: 48,
borderRadius: 3,
boxShadow: 3,
}}
>
{quitando ? 'Quitando...' : 'Quitar selecionados'}
</Button>
</Stack>
</Paper>
</Stack>
</Box>
);
}

View File

@ -0,0 +1,80 @@
import { api } from '../../../services/api';
import type {
ApiResponse,
BancoQuitacao,
CriarQuitacaoCreditoRequest,
QuitacaoCreditoHistoricoItem,
QuitacaoCreditoPreview,
QuitacaoCreditoPreviewParams,
QuitacaoCreditoResponse,
} from '../types/quitacaoCreditoTypes';
function unwrapResponse<T>(payload: ApiResponse<T> | T): T {
if (
payload &&
typeof payload === 'object' &&
'data' in payload &&
'ok' in payload
) {
return (payload as ApiResponse<T>).data;
}
return payload as T;
}
export async function listarBancosCredito(): Promise<BancoQuitacao[]> {
const response = await api.get<ApiResponse<BancoQuitacao[]> | BancoQuitacao[]>(
'/quitacoes-credito/bancos-credito'
);
return unwrapResponse<BancoQuitacao[]>(response.data);
}
export async function listarBancosPagamento(): Promise<BancoQuitacao[]> {
const response = await api.get<ApiResponse<BancoQuitacao[]> | BancoQuitacao[]>(
'/quitacoes-credito/bancos-pagamento'
);
return unwrapResponse<BancoQuitacao[]>(response.data);
}
export async function buscarPreviewQuitacaoCredito(
params: QuitacaoCreditoPreviewParams
): Promise<QuitacaoCreditoPreview> {
const response = await api.get<ApiResponse<QuitacaoCreditoPreview> | QuitacaoCreditoPreview>(
'/quitacoes-credito/preview',
{
params,
}
);
return unwrapResponse<QuitacaoCreditoPreview>(response.data);
}
export async function criarQuitacaoCredito(
data: CriarQuitacaoCreditoRequest
): Promise<QuitacaoCreditoResponse> {
const response = await api.post<ApiResponse<QuitacaoCreditoResponse> | QuitacaoCreditoResponse>(
'/quitacoes-credito',
data
);
return unwrapResponse<QuitacaoCreditoResponse>(response.data);
}
export async function listarHistoricoQuitacoesCredito(params?: {
dataInicio?: string;
dataFim?: string;
idBancoCredito?: number | '';
idBancoPagamento?: number | '';
limite?: number;
page?: number;
}): Promise<QuitacaoCreditoHistoricoItem[]> {
const response = await api.get<
ApiResponse<QuitacaoCreditoHistoricoItem[]> | QuitacaoCreditoHistoricoItem[]
>('/quitacoes-credito/historico', {
params,
});
return unwrapResponse<QuitacaoCreditoHistoricoItem[]>(response.data);
}

View File

@ -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<T> = {
ok: boolean;
message?: string;
data: T;
};

View File

@ -3,6 +3,7 @@ export type Banco = {
descricao: string;
saldo: number | null;
debito: number | null;
dia_vencimento?: number | null;
};
export type CentroCusto = {

View File

@ -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 (
<Box>
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', md: 'center' }}
spacing={3}
marginBottom={{ xs: 3, md: 4 }}
>
<Box>
<Chip
label="MVP Financeiro"
color="primary"
variant="outlined"
sx={{ marginBottom: 1.5 }}
/>
<Typography variant="h4" fontWeight={900}>
Dashboard
</Typography>
<Typography color="text.secondary" sx={{ marginTop: 0.5 }}>
Seu centro de comando para entradas, saídas, sangrias e estornos.
</Typography>
</Box>
<Button
component={RouterLink}
to="/movimentos/novo"
variant="contained"
size="large"
startIcon={<AddIcon />}
sx={{
minHeight: 52,
px: 3,
borderRadius: 3,
boxShadow: 3,
}}
>
Novo movimento
</Button>
</Stack>
<Box
display="grid"
gridTemplateColumns={{
xs: '1fr',
md: 'repeat(3, 1fr)',
}}
gap={{ xs: 2, md: 3 }}
marginBottom={{ xs: 2.5, md: 3 }}
>
<Card sx={{ height: '100%' }}>
<CardContent sx={{ padding: 3 }}>
<Stack direction="row" spacing={2} alignItems="center">
<Box
sx={{
width: 52,
height: 52,
borderRadius: 3,
display: 'grid',
placeItems: 'center',
backgroundColor: 'rgba(21,101,192,0.10)',
color: 'primary.main',
flexShrink: 0,
}}
>
<SwapHorizIcon />
</Box>
<Box>
<Typography color="text.secondary" variant="body2">
Movimentos
</Typography>
<Typography variant="h5" fontWeight={900}>
Ativo
</Typography>
</Box>
</Stack>
</CardContent>
</Card>
<Card sx={{ height: '100%' }}>
<CardContent sx={{ padding: 3 }}>
<Stack direction="row" spacing={2} alignItems="center">
<Box
sx={{
width: 52,
height: 52,
borderRadius: 3,
display: 'grid',
placeItems: 'center',
backgroundColor: 'rgba(46,125,50,0.10)',
color: 'secondary.main',
flexShrink: 0,
}}
>
<AccountBalanceWalletIcon />
</Box>
<Box>
<Typography color="text.secondary" variant="body2">
Carteiras
</Typography>
<Typography variant="h5" fontWeight={900}>
Em breve
</Typography>
</Box>
</Stack>
</CardContent>
</Card>
<Card sx={{ height: '100%' }}>
<CardContent sx={{ padding: 3 }}>
<Stack direction="row" spacing={2} alignItems="center">
<Box
sx={{
width: 52,
height: 52,
borderRadius: 3,
display: 'grid',
placeItems: 'center',
backgroundColor: 'rgba(124,58,237,0.10)',
color: '#7C3AED',
flexShrink: 0,
}}
>
<AssessmentIcon />
</Box>
<Box>
<Typography color="text.secondary" variant="body2">
Relatórios
</Typography>
<Typography variant="h5" fontWeight={900}>
Em breve
</Typography>
</Box>
</Stack>
</CardContent>
</Card>
</Box>
<Box
display="grid"
gridTemplateColumns={{
xs: '1fr',
lg: '1.35fr 0.65fr',
}}
gap={{ xs: 2, md: 3 }}
>
<Card>
<CardContent sx={{ padding: 3 }}>
<Typography variant="h6" fontWeight={900} gutterBottom>
Acesso rápido
</Typography>
<Typography color="text.secondary" marginBottom={3}>
A operação principal está disponível. Cadastre ou consulte os movimentos financeiros.
</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5}>
<Button
component={RouterLink}
to="/movimentos/novo"
variant="contained"
startIcon={<AddIcon />}
sx={{
borderRadius: 2.5,
minHeight: 44,
}}
>
Cadastrar movimento
</Button>
<Button
component={RouterLink}
to="/movimentos"
variant="outlined"
startIcon={<SwapHorizIcon />}
sx={{
borderRadius: 2.5,
minHeight: 44,
}}
>
Ver movimentos
</Button>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent sx={{ padding: 3 }}>
<Typography variant="h6" fontWeight={900} gutterBottom>
Próximos passos
</Typography>
<Stack spacing={1.25} sx={{ marginTop: 1.5 }}>
<Typography variant="body2" color="text.secondary">
Melhorar lista de movimentos
</Typography>
<Typography variant="body2" color="text.secondary">
Criar filtros por data, banco e status
</Typography>
<Typography variant="body2" color="text.secondary">
Adicionar relatórios e saldos
</Typography>
</Stack>
</CardContent>
</Card>
</Box>
</Box>
);
}