adicionado dashboard
This commit is contained in:
parent
c6418df307
commit
345d6033a8
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 />,
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export type ListarBancosParams = {
|
|||
busca?: string;
|
||||
debito?: number | '';
|
||||
habilitado?: number | '';
|
||||
dia_vencimento?: number | '';
|
||||
orderBy?: string;
|
||||
orderDirection?: OrderDirection;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@ export type Banco = {
|
|||
descricao: string;
|
||||
saldo: number | null;
|
||||
debito: number | null;
|
||||
dia_vencimento?: number | null;
|
||||
};
|
||||
|
||||
export type CentroCusto = {
|
||||
|
|
|
|||
|
|
@ -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 já 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue