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