From 12dcc8506f50034397946d754e2484722dba935b Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Mon, 8 Jun 2026 22:30:31 -0300 Subject: [PATCH] ajustado filtros na dashboard e movimentos --- .../dashboard/services/dashboard.service.js | 768 +++++++++++---- .../movimentos/services/movimentos.service.js | 916 +++++++++++++++--- .../dashboard/pages/DashboardPage.tsx | 65 +- .../{dashboardTypees.ts => dashboardTypes.ts} | 3 + .../movimentos/pages/MovimentosPage.tsx | 521 ++++++++-- .../movimentos/types/movimentoTypes.ts | 42 +- 6 files changed, 1924 insertions(+), 391 deletions(-) rename financeiro-web/src/features/dashboard/types/{dashboardTypees.ts => dashboardTypes.ts} (97%) diff --git a/financeiro-api/src/modules/dashboard/services/dashboard.service.js b/financeiro-api/src/modules/dashboard/services/dashboard.service.js index 01b100d..ec67872 100644 --- a/financeiro-api/src/modules/dashboard/services/dashboard.service.js +++ b/financeiro-api/src/modules/dashboard/services/dashboard.service.js @@ -157,87 +157,225 @@ async function buscarCardsMovimentos(idUsuario, 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 + /* Entradas e estornos normais */ + WHEN cp.movimento IN ('Entrada', 'Estorno') + AND banco_origem.idusuarios = ? + THEN cp.valor - COALESCE(SUM(CASE - WHEN cp.movimento IN ('Saida', 'Sangria') - AND COALESCE(cc.investimento, 0) = 0 - THEN cp.valor - ELSE 0 - END), 0) AS saidas, + /* Sangria recebida de outro usuário */ + WHEN cp.movimento = 'Sangria' + AND banco_destino.idusuarios = ? + AND ( + banco_origem.idusuarios IS NULL + OR banco_origem.idusuarios <> ? + ) + THEN cp.valor - COALESCE(SUM(CASE - WHEN cp.movimento IN ('Saida', 'Sangria') - AND COALESCE(cc.investimento, 0) = 1 - THEN cp.valor - ELSE 0 - END), 0) AS investimentos, + ELSE 0 + END + ), 0) AS entradas, - COALESCE(SUM(CASE - WHEN cp.status = 'A receber' - AND cp.movimento IN ('Entrada', 'Estorno') - THEN cp.valor - ELSE 0 - END), 0) AS abertoEntradas, + COALESCE(SUM( + CASE + /* Saída operacional normal */ + WHEN cp.movimento = 'Saida' + AND banco_origem.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 0 + THEN cp.valor - COUNT(CASE - WHEN cp.status = 'A receber' - AND cp.movimento IN ('Entrada', 'Estorno') - THEN 1 - ELSE NULL - END) AS quantidadeAbertoEntradas, + /* Sangria enviada para outro usuário */ + WHEN cp.movimento = 'Sangria' + AND banco_origem.idusuarios = ? + AND ( + banco_destino.idusuarios IS NULL + OR banco_destino.idusuarios <> ? + ) + THEN cp.valor - COALESCE(SUM(CASE - WHEN cp.status = 'A pagar' - AND cp.movimento IN ('Saida', 'Sangria') - AND COALESCE(cc.investimento, 0) = 0 - THEN cp.valor - ELSE 0 - END), 0) AS abertoSaidas, + ELSE 0 + END + ), 0) AS saidas, - COUNT(CASE - WHEN cp.status = 'A pagar' - AND cp.movimento IN ('Saida', 'Sangria') - AND COALESCE(cc.investimento, 0) = 0 - THEN 1 - ELSE NULL - END) AS quantidadeAbertoSaidas, + COALESCE(SUM( + CASE + /* Saída marcada como investimento pelo centro de custo */ + WHEN cp.movimento = 'Saida' + AND banco_origem.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 1 + THEN cp.valor - COALESCE(SUM(CASE - WHEN cp.status = 'A pagar' - AND cp.movimento IN ('Saida', 'Sangria') - AND COALESCE(cc.investimento, 0) = 1 - THEN cp.valor - ELSE 0 - END), 0) AS abertoInvestimentos, + /* Transferência para banco de investimento do próprio usuário */ + WHEN cp.movimento = 'Sangria' + AND banco_origem.idusuarios = ? + AND banco_destino.idusuarios = ? + AND COALESCE(banco_destino.investimento, 0) = 1 + THEN cp.valor - COUNT(CASE - WHEN cp.status = 'A pagar' - AND cp.movimento IN ('Saida', 'Sangria') - AND COALESCE(cc.investimento, 0) = 1 - THEN 1 - ELSE NULL - END) AS quantidadeAbertoInvestimentos, + ELSE 0 + END + ), 0) AS investimentos, - COALESCE(SUM(CASE - WHEN cp.status IN ('Pago', 'Recebido') THEN cp.valor - ELSE 0 - END), 0) AS baixado, + COALESCE(SUM( + CASE + WHEN cp.status = 'A receber' + AND cp.movimento IN ('Entrada', 'Estorno') + AND banco_origem.idusuarios = ? + THEN cp.valor + ELSE 0 + END + ), 0) AS abertoEntradas, + + COUNT( + CASE + WHEN cp.status = 'A receber' + AND cp.movimento IN ('Entrada', 'Estorno') + AND banco_origem.idusuarios = ? + THEN 1 + ELSE NULL + END + ) AS quantidadeAbertoEntradas, + + COALESCE(SUM( + CASE + WHEN cp.status = 'A pagar' + AND cp.movimento = 'Saida' + AND banco_origem.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 0 + THEN cp.valor + ELSE 0 + END + ), 0) AS abertoSaidas, + + COUNT( + CASE + WHEN cp.status = 'A pagar' + AND cp.movimento = 'Saida' + AND banco_origem.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 0 + THEN 1 + ELSE NULL + END + ) AS quantidadeAbertoSaidas, + + COALESCE(SUM( + CASE + WHEN cp.status = 'A pagar' + AND cp.movimento = 'Saida' + AND banco_origem.idusuarios = ? + 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 = 'Saida' + AND banco_origem.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 1 + THEN 1 + ELSE NULL + END + ) AS quantidadeAbertoInvestimentos, + + COALESCE(SUM( + CASE + WHEN cp.status IN ('Pago', 'Recebido') + AND ( + cp.movimento <> 'Sangria' + + OR ( + cp.movimento = 'Sangria' + AND banco_origem.idusuarios = ? + AND ( + banco_destino.idusuarios IS NULL + OR banco_destino.idusuarios <> ? + ) + ) + + OR ( + cp.movimento = 'Sangria' + AND banco_destino.idusuarios = ? + AND banco_origem.idusuarios <> ? + ) + ) + THEN cp.valor + ELSE 0 + END + ), 0) AS baixado, COUNT(*) AS quantidadeMovimentos + FROM contasapagar cp - INNER JOIN bancos b - ON b.idbancos = cp.idbancos + + INNER JOIN bancos banco_origem + ON banco_origem.idbancos = cp.idbancos + + LEFT JOIN bancos banco_destino + ON banco_destino.idbancos = cp.idbancos_p + LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos + WHERE cp.deleted_at IS NULL - AND b.idusuarios = ? - AND DATE(cp.datavencimento) BETWEEN ? AND ? + AND ( + banco_origem.idusuarios = ? + OR banco_destino.idusuarios = ? + ) + AND DATE(COALESCE(cp.dataentrada, cp.insert_date)) + BETWEEN ? AND ? `, - [idUsuario, dataInicio, dataFim] + [ + /* entradas */ + idUsuario, + idUsuario, + idUsuario, + + /* saídas */ + idUsuario, + idUsuario, + idUsuario, + + /* investimentos */ + idUsuario, + idUsuario, + idUsuario, + + /* abertoEntradas */ + idUsuario, + + /* quantidadeAbertoEntradas */ + idUsuario, + + /* abertoSaidas */ + idUsuario, + + /* quantidadeAbertoSaidas */ + idUsuario, + + /* abertoInvestimentos */ + idUsuario, + + /* quantidadeAbertoInvestimentos */ + idUsuario, + + /* baixado */ + idUsuario, + idUsuario, + idUsuario, + idUsuario, + + /* usuário na origem ou destino */ + idUsuario, + idUsuario, + + /* período */ + dataInicio, + dataFim, + ] ); const row = rows[0] || {}; @@ -267,9 +405,14 @@ async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) { abertoInvestimentos, aberto: abertoEntradas - abertoSaidas - abertoInvestimentos, - quantidadeAbertoEntradas: normalizarNumero(row.quantidadeAbertoEntradas), - quantidadeAbertoSaidas: normalizarNumero(row.quantidadeAbertoSaidas), - quantidadeAbertoInvestimentos: normalizarNumero(row.quantidadeAbertoInvestimentos), + quantidadeAbertoEntradas: + normalizarNumero(row.quantidadeAbertoEntradas), + + quantidadeAbertoSaidas: + normalizarNumero(row.quantidadeAbertoSaidas), + + quantidadeAbertoInvestimentos: + normalizarNumero(row.quantidadeAbertoInvestimentos), baixado: normalizarNumero(row.baixado), quantidadeMovimentos: normalizarNumero(row.quantidadeMovimentos), @@ -430,35 +573,54 @@ async function buscarAlertasVencimento(idUsuario) { } async function buscarProximosVencimentos(idUsuario) { - 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 - INNER JOIN bancos bc ON bc.idbancos = cp.idbancos - LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos - WHERE cp.deleted_at IS NULL - AND bc.idusuarios = ? - AND cp.status IN ('A pagar', 'A receber') - AND cp.datavencimento IS NOT NULL - ORDER BY DATE(cp.datavencimento) ASC, cp.idcontasapagar ASC - LIMIT 10 - `, - [idUsuario]); + 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, + + banco_origem.descricao AS banco_descricao, + banco_origem.debito AS banco_debito, + + cp.idcentrodecustos, + cc.descricao AS centro_custo_descricao, + + DATEDIFF( + DATE(cp.datavencimento), + CURDATE() + ) AS dias_para_vencer + + FROM contasapagar cp + + INNER JOIN bancos banco_origem + ON banco_origem.idbancos = cp.idbancos + + LEFT JOIN centrodecustos cc + ON cc.idcentrodecustos = cp.idcentrodecustos + + WHERE cp.deleted_at IS NULL + AND banco_origem.idusuarios = ? + AND cp.status IN ('A pagar', 'A receber') + AND cp.movimento IN ('Entrada', 'Estorno', 'Saida') + AND cp.datavencimento IS NOT NULL + + ORDER BY + DATE(cp.datavencimento) ASC, + cp.idcontasapagar ASC + + LIMIT 10 + `, + [idUsuario] + ); return rows; } @@ -626,104 +788,380 @@ async function buscarCarteiras(idUsuario, dataInicio, dataFim) { } async function buscarUltimosMovimentos(idUsuario) { - 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 - INNER JOIN bancos bc ON bc.idbancos = cp.idbancos - LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos - WHERE cp.deleted_at IS NULL - AND bc.idusuarios = ? - ORDER BY cp.insert_date DESC, cp.idcontasapagar DESC - LIMIT 10 - `, - [idUsuario]); + 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, - return rows; + cp.idbancos, + cp.idbancos_p, + + banco_origem.descricao AS banco_descricao, + banco_origem.descricao AS banco_origem_descricao, + banco_origem.debito AS banco_debito, + banco_origem.idusuarios AS idusuario_origem, + usuario_origem.nome AS usuario_origem_nome, + + banco_destino.descricao AS banco_destino_descricao, + banco_destino.debito AS banco_destino_debito, + banco_destino.idusuarios AS idusuario_destino, + usuario_destino.nome AS usuario_destino_nome, + + cp.idcentrodecustos, + cc.descricao AS centro_custo_descricao, + + CASE + /* + * Movimento normal: + * mantém o tipo original. + */ + WHEN cp.movimento <> 'Sangria' + THEN cp.movimento + + /* + * Origem e destino pertencem ao usuário: + * transferência interna. + */ + WHEN banco_origem.idusuarios = ? + AND banco_destino.idusuarios = ? + THEN 'TransferenciaInterna' + + /* + * Usuário é o dono do banco de origem: + * dinheiro saiu para outro usuário. + */ + WHEN banco_origem.idusuarios = ? + THEN 'Saida' + + /* + * Usuário é o dono do banco de destino: + * dinheiro entrou vindo de outro usuário. + */ + WHEN banco_destino.idusuarios = ? + THEN 'Entrada' + + ELSE cp.movimento + END AS movimento_dashboard, + + CASE + WHEN cp.movimento <> 'Sangria' + AND cp.movimento IN ('Entrada', 'Estorno') + THEN 'entrada' + + WHEN cp.movimento <> 'Sangria' + AND cp.movimento = 'Saida' + THEN 'saida' + + WHEN cp.movimento = 'Sangria' + AND banco_origem.idusuarios = ? + AND banco_destino.idusuarios = ? + THEN 'transferencia_interna' + + WHEN cp.movimento = 'Sangria' + AND banco_origem.idusuarios = ? + THEN 'saida' + + WHEN cp.movimento = 'Sangria' + AND banco_destino.idusuarios = ? + THEN 'entrada' + + ELSE 'neutro' + END AS direcao_dashboard, + + CASE + WHEN cp.movimento = 'Sangria' + AND banco_origem.idusuarios = ? + AND banco_destino.idusuarios = ? + THEN 1 + + ELSE 0 + END AS transferencia_interna + + FROM contasapagar cp + + INNER JOIN bancos banco_origem + ON banco_origem.idbancos = cp.idbancos + + LEFT JOIN bancos banco_destino + ON banco_destino.idbancos = cp.idbancos_p + + LEFT JOIN usuarios usuario_origem + ON usuario_origem.idusuarios = banco_origem.idusuarios + + LEFT JOIN usuarios usuario_destino + ON usuario_destino.idusuarios = banco_destino.idusuarios + + LEFT JOIN centrodecustos cc + ON cc.idcentrodecustos = cp.idcentrodecustos + + WHERE cp.deleted_at IS NULL + AND ( + banco_origem.idusuarios = ? + OR banco_destino.idusuarios = ? + ) + + ORDER BY + cp.insert_date DESC, + cp.idcontasapagar DESC + + LIMIT 10 + `, + [ + /* movimento_dashboard */ + idUsuario, + idUsuario, + idUsuario, + idUsuario, + + /* direcao_dashboard */ + idUsuario, + idUsuario, + idUsuario, + idUsuario, + + /* transferencia_interna */ + idUsuario, + idUsuario, + + /* WHERE */ + idUsuario, + idUsuario, + ] + ); + + return rows.map((item) => ({ + ...item, + + valor: normalizarNumero(item.valor), + + transferencia_interna: + Number(item.transferencia_interna || 0) === 1, + })); } async function buscarGraficos(idUsuario, dataInicio, dataFim) { + /* + * Gastos por centro de custo + * + * Aqui entram: + * - Saídas normais do usuário; + * - Sangrias enviadas para outro usuário; + * + * Não entram: + * - Sangrias entre bancos do próprio usuário; + * - Sangrias recebidas; + * - Movimentos classificados como investimento. + */ 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 + + INNER JOIN bancos banco_origem + ON banco_origem.idbancos = cp.idbancos + + LEFT JOIN bancos banco_destino + ON banco_destino.idbancos = cp.idbancos_p + + LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos - LEFT JOIN bancos b - ON b.idbancos = cp.idbancos + WHERE cp.deleted_at IS NULL - AND cp.movimento IN ('Saida', 'Sangria') + AND DATE(COALESCE(cp.dataentrada, cp.insert_date)) + BETWEEN ? AND ? AND COALESCE(cc.investimento, 0) = 0 - AND DATE(cp.datavencimento) BETWEEN ? AND ? - AND b.idusuarios = ? + + AND ( + ( + cp.movimento = 'Saida' + AND banco_origem.idusuarios = ? + ) + + OR + + ( + cp.movimento = 'Sangria' + AND banco_origem.idusuarios = ? + AND ( + banco_destino.idusuarios IS NULL + OR banco_destino.idusuarios <> ? + ) + ) + ) + GROUP BY cp.idcentrodecustos, COALESCE(cc.descricao, 'Sem centro de custo') + HAVING valor > 0 + ORDER BY valor DESC + LIMIT 8 `, - [dataInicio, dataFim, idUsuario] + [ + dataInicio, + dataFim, + + idUsuario, + + idUsuario, + idUsuario, + ] ); + /* + * Resumo de entradas, saídas e investimentos. + * + * Para sangria: + * - destino é do usuário e origem é de outro usuário = entrada; + * - origem é do usuário e destino é de outro usuário = saída; + * - origem e destino são do usuário = transferência interna, ignorada. + */ 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 + /* Entrada ou estorno normal */ + WHEN cp.movimento IN ('Entrada', 'Estorno') + AND banco_origem.idusuarios = ? + THEN cp.valor - COALESCE(SUM(CASE - WHEN cp.movimento IN ('Saida', 'Sangria') - AND COALESCE(cc.investimento, 0) = 0 - THEN cp.valor - ELSE 0 - END), 0) AS saidas, + /* Sangria recebida de outro usuário */ + WHEN cp.movimento = 'Sangria' + AND banco_destino.idusuarios = ? + AND banco_origem.idusuarios <> ? + THEN cp.valor + + ELSE 0 + END + ), + 0 + ) AS entradas, + + COALESCE( + SUM( + CASE + /* Saída normal */ + WHEN cp.movimento = 'Saida' + AND banco_origem.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 0 + THEN cp.valor + + /* Sangria enviada para outro usuário */ + WHEN cp.movimento = 'Sangria' + AND banco_origem.idusuarios = ? + AND ( + banco_destino.idusuarios IS NULL + OR banco_destino.idusuarios <> ? + ) + AND COALESCE(cc.investimento, 0) = 0 + THEN cp.valor + + ELSE 0 + END + ), + 0 + ) AS saidas, + + COALESCE( + SUM( + CASE + WHEN cp.movimento = 'Saida' + AND banco_origem.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 1 + THEN cp.valor + + WHEN cp.movimento = 'Sangria' + AND banco_origem.idusuarios = ? + AND banco_destino.idusuarios = ? + AND COALESCE(banco_destino.investimento, 0) = 1 + THEN cp.valor + + ELSE 0 + END + ), + 0 + ) AS investimentos - COALESCE(SUM(CASE - WHEN cp.movimento IN ('Saida', 'Sangria') - AND COALESCE(cc.investimento, 0) = 1 - THEN cp.valor - ELSE 0 - END), 0) AS investimentos FROM contasapagar cp + + INNER JOIN bancos banco_origem + ON banco_origem.idbancos = cp.idbancos + + LEFT JOIN bancos banco_destino + ON banco_destino.idbancos = cp.idbancos_p + LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos - LEFT JOIN bancos b - ON b.idbancos = cp.idbancos + WHERE cp.deleted_at IS NULL - AND DATE(cp.datavencimento) BETWEEN ? AND ? - AND b.idusuarios = ? + AND DATE(COALESCE(cp.dataentrada, cp.insert_date)) + BETWEEN ? AND ? + + AND ( + banco_origem.idusuarios = ? + OR banco_destino.idusuarios = ? + ) `, - [dataInicio, dataFim, idUsuario] + [ + /* Entradas */ + idUsuario, + idUsuario, + idUsuario, + + /* Saídas */ + idUsuario, + idUsuario, + idUsuario, + + /* Investimentos */ + idUsuario, + idUsuario, + idUsuario, + + /* Período */ + dataInicio, + dataFim, + + /* Usuário presente em uma das pontas */ + idUsuario, + idUsuario, + ] + ); + + const entradas = normalizarNumero( + entradasSaidasRows[0]?.entradas + ); + + const saidas = normalizarNumero( + entradasSaidasRows[0]?.saidas + ); + + const investimentos = normalizarNumero( + entradasSaidasRows[0]?.investimentos ); - const entradas = normalizarNumero(entradasSaidasRows[0]?.entradas); - const saidas = normalizarNumero(entradasSaidasRows[0]?.saidas); - const investimentos = normalizarNumero(entradasSaidasRows[0]?.investimentos); const resultado = entradas - saidas - investimentos; return { @@ -741,8 +1179,14 @@ async function buscarGraficos(idUsuario, dataInicio, dataFim) { percentualEntradas: entradas > 0 ? 100 : 0, percentualSaidas: calcularPercentual(saidas, entradas), - percentualInvestimentos: calcularPercentual(investimentos, entradas), - percentualResultado: calcularPercentual(resultado, entradas), + percentualInvestimentos: calcularPercentual( + investimentos, + entradas + ), + percentualResultado: calcularPercentual( + resultado, + entradas + ), }, }; } diff --git a/financeiro-api/src/modules/movimentos/services/movimentos.service.js b/financeiro-api/src/modules/movimentos/services/movimentos.service.js index 872a175..37668b7 100644 --- a/financeiro-api/src/modules/movimentos/services/movimentos.service.js +++ b/financeiro-api/src/modules/movimentos/services/movimentos.service.js @@ -37,7 +37,10 @@ const CAMPOS_MOVIMENTO_SELECT = ` c.nome AS cliente_nome, bp.descricao AS banco_referencia_descricao, bp.debito AS banco_referencia_debito, - mf.descricao AS movimento_fixo_descricao + mf.descricao AS movimento_fixo_descricao, + b.idusuarios AS idusuario_origem, + bp.idusuarios AS idusuario_destino, + bp.investimento AS banco_destino_investimento `; const FROM_MOVIMENTO_JOIN = ` @@ -86,7 +89,10 @@ function montarSelectExtrato(campoDataSql, ocorrencia) { c.nome AS cliente_nome, bp.descricao AS banco_referencia_descricao, bp.debito AS banco_referencia_debito, - mf.descricao AS movimento_fixo_descricao + mf.descricao AS movimento_fixo_descricao, + b.idusuarios AS idusuario_origem, + bp.idusuarios AS idusuario_destino, + bp.investimento AS banco_destino_investimento ${FROM_MOVIMENTO_JOIN} `; } @@ -133,6 +139,19 @@ function limitarNumero(valor, padrao, minimo, maximo) { return numero; } +function percentualParte(parte, total) { + const valorParte = Number(parte || 0); + const valorTotal = Number(total || 0); + + if (!Number.isFinite(valorTotal) || valorTotal <= 0) { + return 0; + } + + return Number( + ((valorParte / valorTotal) * 100).toFixed(2) + ); +} + function hojeDataString() { return new Date().toISOString().slice(0, 10); } @@ -170,6 +189,54 @@ function statusBaixado(status) { return status === 'Pago' || status === 'Recebido'; } +function enriquecerMovimentoParaUsuario(item, idUsuario) { + const usuarioAtual = Number(idUsuario); + const usuarioOrigem = Number(item.idusuario_origem); + const usuarioDestino = Number(item.idusuario_destino); + + let movimentoDashboard = item.movimento; + let direcaoDashboard = 'neutro'; + + if ( + item.movimento === 'Entrada' + || item.movimento === 'Estorno' + ) { + direcaoDashboard = 'entrada'; + } else if (item.movimento === 'Saida') { + direcaoDashboard = 'saida'; + } else if (item.movimento === 'Sangria') { + if ( + usuarioOrigem === usuarioAtual + && usuarioDestino === usuarioAtual + ) { + movimentoDashboard = 'TransferenciaInterna'; + direcaoDashboard = 'transferencia_interna'; + } else if (usuarioOrigem === usuarioAtual) { + movimentoDashboard = 'Saida'; + direcaoDashboard = 'saida'; + } else if (usuarioDestino === usuarioAtual) { + movimentoDashboard = 'Entrada'; + direcaoDashboard = 'entrada'; + } + } + + return { + ...item, + + movimento_dashboard: movimentoDashboard, + direcao_dashboard: direcaoDashboard, + + transferencia_interna: + movimentoDashboard === 'TransferenciaInterna', + + investimento_dashboard: + item.movimento === 'Sangria' + && usuarioOrigem === usuarioAtual + && usuarioDestino === usuarioAtual + && Number(item.banco_destino_investimento || 0) === 1, + }; +} + function normalizarMovimentoParaSalvar(idUsuario, dados, extras = {}) { const status = dados.status || null; const baixado = statusBaixado(status); @@ -266,6 +333,539 @@ function montarWhereMovimentos(filtros = {}) { }; } +async function buscarResumoMovimentos( + filtros = {}, + opcoes = {} +) { + const idUsuario = Number(filtros.idusuarios); + + if (!Number.isFinite(idUsuario) || idUsuario <= 0) { + throw new Error( + 'Não foi possível calcular o resumo: usuário inválido.' + ); + } + + const base = montarWhereMovimentosBase(filtros); + + const where = [...base.where]; + const paramsWhere = [...base.params]; + + /* + * No extrato financeiro, entradas, saídas e investimentos + * pertencem ao período de realização do movimento. + * + * Na tabela comum, o resumo respeita o campo de data + * selecionado pelo usuário. + */ + const campoDataSql = + opcoes.modoExtrato === true + ? 'COALESCE(cp.dataentrada, cp.insert_date)' + : resolverCampoData(filtros.dataCampo); + + if (filtros.dataInicio) { + where.push(`DATE(${campoDataSql}) >= ?`); + paramsWhere.push(filtros.dataInicio); + } + + if (filtros.dataFim) { + where.push(`DATE(${campoDataSql}) <= ?`); + paramsWhere.push(filtros.dataFim); + } + + const whereSql = where.length > 0 + ? `WHERE ${where.join(' AND ')}` + : ''; + + const [rows] = await pool.query( + ` + SELECT + COUNT(*) AS quantidade, + + /* ================================================== + * ENTRADAS + * ================================================== */ + + COALESCE(SUM( + CASE + WHEN cp.movimento IN ('Entrada', 'Estorno') + AND b.idusuarios = ? + THEN cp.valor + + WHEN cp.movimento = 'Sangria' + AND bp.idusuarios = ? + AND ( + b.idusuarios IS NULL + OR b.idusuarios <> ? + ) + THEN cp.valor + + ELSE 0 + END + ), 0) AS totalEntradas, + + COALESCE(SUM( + CASE + WHEN cp.movimento IN ('Entrada', 'Estorno') + AND b.idusuarios = ? + AND cp.status = 'Recebido' + THEN cp.valor + + WHEN cp.movimento = 'Sangria' + AND bp.idusuarios = ? + AND ( + b.idusuarios IS NULL + OR b.idusuarios <> ? + ) + AND cp.status IN ('Pago', 'Recebido') + THEN cp.valor + + ELSE 0 + END + ), 0) AS entradasRecebidas, + + COALESCE(SUM( + CASE + WHEN cp.movimento IN ('Entrada', 'Estorno') + AND b.idusuarios = ? + AND cp.status = 'A receber' + THEN cp.valor + + WHEN cp.movimento = 'Sangria' + AND bp.idusuarios = ? + AND ( + b.idusuarios IS NULL + OR b.idusuarios <> ? + ) + AND cp.status IN ('A receber', 'A pagar') + THEN cp.valor + + ELSE 0 + END + ), 0) AS entradasPendentes, + + COUNT( + CASE + WHEN cp.movimento IN ('Entrada', 'Estorno') + AND b.idusuarios = ? + AND cp.status = 'Recebido' + THEN 1 + + WHEN cp.movimento = 'Sangria' + AND bp.idusuarios = ? + AND ( + b.idusuarios IS NULL + OR b.idusuarios <> ? + ) + AND cp.status IN ('Pago', 'Recebido') + THEN 1 + + ELSE NULL + END + ) AS quantidadeEntradasRecebidas, + + COUNT( + CASE + WHEN cp.movimento IN ('Entrada', 'Estorno') + AND b.idusuarios = ? + AND cp.status = 'A receber' + THEN 1 + + WHEN cp.movimento = 'Sangria' + AND bp.idusuarios = ? + AND ( + b.idusuarios IS NULL + OR b.idusuarios <> ? + ) + AND cp.status IN ('A receber', 'A pagar') + THEN 1 + + ELSE NULL + END + ) AS quantidadeEntradasPendentes, + + /* ================================================== + * SAÍDAS OPERACIONAIS + * ================================================== */ + + COALESCE(SUM( + CASE + WHEN cp.movimento = 'Saida' + AND b.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 0 + THEN cp.valor + + WHEN cp.movimento = 'Sangria' + AND b.idusuarios = ? + AND ( + bp.idusuarios IS NULL + OR bp.idusuarios <> ? + ) + THEN cp.valor + + ELSE 0 + END + ), 0) AS totalSaidas, + + COALESCE(SUM( + CASE + WHEN cp.movimento = 'Saida' + AND b.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 0 + AND cp.status = 'Pago' + THEN cp.valor + + WHEN cp.movimento = 'Sangria' + AND b.idusuarios = ? + AND ( + bp.idusuarios IS NULL + OR bp.idusuarios <> ? + ) + AND cp.status IN ('Pago', 'Recebido') + THEN cp.valor + + ELSE 0 + END + ), 0) AS saidasPagas, + + COALESCE(SUM( + CASE + WHEN cp.movimento = 'Saida' + AND b.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 0 + AND cp.status = 'A pagar' + THEN cp.valor + + WHEN cp.movimento = 'Sangria' + AND b.idusuarios = ? + AND ( + bp.idusuarios IS NULL + OR bp.idusuarios <> ? + ) + AND cp.status IN ('A pagar', 'A receber') + THEN cp.valor + + ELSE 0 + END + ), 0) AS saidasPendentes, + + COUNT( + CASE + WHEN cp.movimento = 'Saida' + AND b.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 0 + AND cp.status = 'Pago' + THEN 1 + + WHEN cp.movimento = 'Sangria' + AND b.idusuarios = ? + AND ( + bp.idusuarios IS NULL + OR bp.idusuarios <> ? + ) + AND cp.status IN ('Pago', 'Recebido') + THEN 1 + + ELSE NULL + END + ) AS quantidadeSaidasPagas, + + COUNT( + CASE + WHEN cp.movimento = 'Saida' + AND b.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 0 + AND cp.status = 'A pagar' + THEN 1 + + WHEN cp.movimento = 'Sangria' + AND b.idusuarios = ? + AND ( + bp.idusuarios IS NULL + OR bp.idusuarios <> ? + ) + AND cp.status IN ('A pagar', 'A receber') + THEN 1 + + ELSE NULL + END + ) AS quantidadeSaidasPendentes, + + /* ================================================== + * INVESTIMENTOS + * ================================================== */ + + COALESCE(SUM( + CASE + WHEN cp.movimento = 'Saida' + AND b.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 1 + THEN cp.valor + + WHEN cp.movimento = 'Sangria' + AND b.idusuarios = ? + AND bp.idusuarios = ? + AND COALESCE(bp.investimento, 0) = 1 + THEN cp.valor + + ELSE 0 + END + ), 0) AS totalInvestimentos, + + COALESCE(SUM( + CASE + WHEN cp.movimento = 'Saida' + AND b.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 1 + AND cp.status = 'Pago' + THEN cp.valor + + WHEN cp.movimento = 'Sangria' + AND b.idusuarios = ? + AND bp.idusuarios = ? + AND COALESCE(bp.investimento, 0) = 1 + AND cp.status IN ('Pago', 'Recebido') + THEN cp.valor + + ELSE 0 + END + ), 0) AS investimentosRealizados, + + COALESCE(SUM( + CASE + WHEN cp.movimento = 'Saida' + AND b.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 1 + AND cp.status = 'A pagar' + THEN cp.valor + + WHEN cp.movimento = 'Sangria' + AND b.idusuarios = ? + AND bp.idusuarios = ? + AND COALESCE(bp.investimento, 0) = 1 + AND cp.status IN ('A pagar', 'A receber') + THEN cp.valor + + ELSE 0 + END + ), 0) AS investimentosPendentes, + + COUNT( + CASE + WHEN cp.movimento = 'Saida' + AND b.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 1 + AND cp.status = 'Pago' + THEN 1 + + WHEN cp.movimento = 'Sangria' + AND b.idusuarios = ? + AND bp.idusuarios = ? + AND COALESCE(bp.investimento, 0) = 1 + AND cp.status IN ('Pago', 'Recebido') + THEN 1 + + ELSE NULL + END + ) AS quantidadeInvestimentosRealizados, + + COUNT( + CASE + WHEN cp.movimento = 'Saida' + AND b.idusuarios = ? + AND COALESCE(cc.investimento, 0) = 1 + AND cp.status = 'A pagar' + THEN 1 + + WHEN cp.movimento = 'Sangria' + AND b.idusuarios = ? + AND bp.idusuarios = ? + AND COALESCE(bp.investimento, 0) = 1 + AND cp.status IN ('A pagar', 'A receber') + THEN 1 + + ELSE NULL + END + ) AS quantidadeInvestimentosPendentes, + + /* ================================================== + * INFORMAÇÕES AUXILIARES + * ================================================== */ + + COALESCE(SUM( + CASE + WHEN cp.movimento = 'Estorno' + AND b.idusuarios = ? + THEN cp.valor + ELSE 0 + END + ), 0) AS totalEstornos, + + COALESCE(SUM( + CASE + WHEN cp.saldo_processado = 1 + THEN 1 + ELSE 0 + END + ), 0) AS saldoProcessado + + ${FROM_MOVIMENTO_JOIN} + ${whereSql} + `, + [ + /* Entradas: total, recebido, pendente e quantidades */ + idUsuario, idUsuario, idUsuario, + idUsuario, idUsuario, idUsuario, + idUsuario, idUsuario, idUsuario, + idUsuario, idUsuario, idUsuario, + idUsuario, idUsuario, idUsuario, + + /* Saídas: total, pago, pendente e quantidades */ + idUsuario, idUsuario, idUsuario, + idUsuario, idUsuario, idUsuario, + idUsuario, idUsuario, idUsuario, + idUsuario, idUsuario, idUsuario, + idUsuario, idUsuario, idUsuario, + + /* Investimentos: total, realizado, pendente e quantidades */ + idUsuario, idUsuario, idUsuario, + idUsuario, idUsuario, idUsuario, + idUsuario, idUsuario, idUsuario, + idUsuario, idUsuario, idUsuario, + idUsuario, idUsuario, idUsuario, + + /* Estornos */ + idUsuario, + + /* Filtros */ + ...paramsWhere, + ] + ); + + const resumo = rows[0] || {}; + + const totalEntradas = + Number(resumo.totalEntradas || 0); + + const entradasRecebidas = + Number(resumo.entradasRecebidas || 0); + + const entradasPendentes = + Number(resumo.entradasPendentes || 0); + + const totalSaidas = + Number(resumo.totalSaidas || 0); + + const saidasPagas = + Number(resumo.saidasPagas || 0); + + const saidasPendentes = + Number(resumo.saidasPendentes || 0); + + const totalInvestimentos = + Number(resumo.totalInvestimentos || 0); + + const investimentosRealizados = + Number(resumo.investimentosRealizados || 0); + + const investimentosPendentes = + Number(resumo.investimentosPendentes || 0); + + const saldo = + totalEntradas + - totalSaidas + - totalInvestimentos; + + return { + quantidade: Number(resumo.quantidade || 0), + + totalEntradas, + entradasRecebidas, + entradasPendentes, + + quantidadeEntradasRecebidas: + Number(resumo.quantidadeEntradasRecebidas || 0), + + quantidadeEntradasPendentes: + Number(resumo.quantidadeEntradasPendentes || 0), + + percentualEntradasRecebidas: + percentualParte( + entradasRecebidas, + totalEntradas + ), + + percentualEntradasPendentes: + percentualParte( + entradasPendentes, + totalEntradas + ), + + totalSaidas, + saidasPagas, + saidasPendentes, + + quantidadeSaidasPagas: + Number(resumo.quantidadeSaidasPagas || 0), + + quantidadeSaidasPendentes: + Number(resumo.quantidadeSaidasPendentes || 0), + + percentualSaidasPagas: + percentualParte( + saidasPagas, + totalSaidas + ), + + percentualSaidasPendentes: + percentualParte( + saidasPendentes, + totalSaidas + ), + + totalInvestimentos, + investimentosRealizados, + investimentosPendentes, + + quantidadeInvestimentosRealizados: + Number( + resumo.quantidadeInvestimentosRealizados || 0 + ), + + quantidadeInvestimentosPendentes: + Number( + resumo.quantidadeInvestimentosPendentes || 0 + ), + + percentualInvestimentosRealizados: + percentualParte( + investimentosRealizados, + totalInvestimentos + ), + + percentualInvestimentosPendentes: + percentualParte( + investimentosPendentes, + totalInvestimentos + ), + + totalEstornos: + Number(resumo.totalEstornos || 0), + + saldoProcessado: + Number(resumo.saldoProcessado || 0), + + saldo, + + /* + * Campos antigos neutralizados temporariamente. + * Podem ser removidos após o front ser atualizado. + */ + totalSangrias: 0, + totalBaixado: 0, + totalAberto: 0, + }; +} + function montarWhereMovimentosBase(filtros = {}) { const where = []; const params = []; @@ -275,8 +875,17 @@ function montarWhereMovimentosBase(filtros = {}) { } if (filtros.idusuarios) { - where.push('b.idusuarios = ?'); - params.push(Number(filtros.idusuarios)); + where.push(` + ( + b.idusuarios = ? + OR bp.idusuarios = ? + ) + `); + + params.push( + Number(filtros.idusuarios), + Number(filtros.idusuarios) + ); } if (filtros.movimento) { @@ -389,70 +998,110 @@ function montarWhereMovimentosBase(filtros = {}) { } async function listarMovimentosExtrato(filtros = {}) { - const limite = limitarNumero(filtros.limite, 20, 1, 500); - const page = limitarNumero(filtros.page, 1, 1, 999999); - const offset = filtros.offset !== undefined - ? limitarNumero(filtros.offset, 0, 0, 999999999) - : (page - 1) * limite; + const limite = limitarNumero( + filtros.limite, + 20, + 1, + 500 + ); - const orderDirection = resolverDirecao(filtros.orderDirection); + const page = limitarNumero( + filtros.page, + 1, + 1, + 999999 + ); - const base = montarWhereMovimentosBase(filtros); + const offset = + filtros.offset !== undefined + ? limitarNumero( + filtros.offset, + 0, + 0, + 999999999 + ) + : (page - 1) * limite; - function montarWhereOcorrencia(campoDataSql, extras = []) { + const orderDirection = + resolverDirecao(filtros.orderDirection); + + const base = + montarWhereMovimentosBase(filtros); + + function montarWhereOcorrencia( + campoDataSql, + extras = [] + ) { const where = [...base.where]; + const params = [...base.params]; where.push(`${campoDataSql} IS NOT NULL`); if (filtros.dataInicio) { where.push(`DATE(${campoDataSql}) >= ?`); - } - - if (filtros.dataFim) { - where.push(`DATE(${campoDataSql}) <= ?`); - } - - where.push(...extras); - - const params = [...base.params]; - - if (filtros.dataInicio) { params.push(filtros.dataInicio); } if (filtros.dataFim) { + where.push(`DATE(${campoDataSql}) <= ?`); params.push(filtros.dataFim); } + where.push(...extras); + return { - whereSql: where.length > 0 ? `WHERE ${where.join(' AND ')}` : '', + whereSql: + where.length > 0 + ? `WHERE ${where.join(' AND ')}` + : '', + params, }; } - const lancamento = montarWhereOcorrencia('cp.dataentrada'); + const lancamento = + montarWhereOcorrencia( + 'cp.dataentrada' + ); - const baixa = montarWhereOcorrencia('cp.databaixa', [ - `( - cp.dataentrada IS NULL - OR DATE(cp.databaixa) <> DATE(cp.dataentrada) - )`, - ]); + /* + * A baixa só aparece como outra ocorrência quando + * aconteceu em mês diferente do lançamento. + */ + const baixa = + montarWhereOcorrencia( + 'cp.databaixa', + [ + `( + cp.dataentrada IS NULL + OR DATE_FORMAT(cp.databaixa, '%Y-%m') + <> DATE_FORMAT(cp.dataentrada, '%Y-%m') + )`, + ] + ); const selectLancamento = ` - ${montarSelectExtrato('cp.dataentrada', 'lancamento')} + ${montarSelectExtrato( + 'cp.dataentrada', + 'lancamento' + )} ${lancamento.whereSql} `; const selectBaixa = ` - ${montarSelectExtrato('cp.databaixa', 'baixa')} + ${montarSelectExtrato( + 'cp.databaixa', + 'baixa' + )} ${baixa.whereSql} `; const unionSql = ` ( ${selectLancamento} + UNION ALL + ${selectBaixa} ) extrato `; @@ -466,10 +1115,16 @@ async function listarMovimentosExtrato(filtros = {}) { ` SELECT * FROM ${unionSql} - ORDER BY data_extrato ${orderDirection}, idcontasapagar DESC + ORDER BY + data_extrato ${orderDirection}, + idcontasapagar DESC LIMIT ? OFFSET ? `, - [...paramsUnion, limite, offset] + [ + ...paramsUnion, + limite, + offset, + ] ); const [countRows] = await pool.query( @@ -480,34 +1135,30 @@ async function listarMovimentosExtrato(filtros = {}) { paramsUnion ); - const [summaryRows] = await pool.query( - ` - SELECT - COUNT(*) AS quantidade, - COALESCE(SUM(CASE WHEN movimento = 'Entrada' THEN valor ELSE 0 END), 0) AS totalEntradas, - COALESCE(SUM(CASE WHEN movimento = 'Saida' THEN valor ELSE 0 END), 0) AS totalSaidas, - COALESCE(SUM(CASE WHEN movimento = 'Sangria' THEN valor ELSE 0 END), 0) AS totalSangrias, - COALESCE(SUM(CASE WHEN movimento = 'Estorno' THEN valor ELSE 0 END), 0) AS totalEstornos, - COALESCE(SUM(CASE WHEN status IN ('Pago', 'Recebido') THEN valor ELSE 0 END), 0) AS totalBaixado, - COALESCE(SUM(CASE WHEN status IN ('A pagar', 'A receber') THEN valor ELSE 0 END), 0) AS totalAberto, - COALESCE(SUM(CASE WHEN saldo_processado = 1 THEN 1 ELSE 0 END), 0) AS saldoProcessado, - COALESCE(SUM( - CASE - WHEN movimento IN ('Entrada', 'Estorno') THEN valor - WHEN movimento IN ('Saida', 'Sangria') THEN -valor - ELSE 0 - END - ), 0) AS saldo - FROM ${unionSql} - `, - paramsUnion + const summary = + await buscarResumoMovimentos( + filtros, + { + modoExtrato: true, + } + ); + + const total = + Number(countRows[0]?.total || 0); + + const totalPages = Math.max( + 1, + Math.ceil(total / limite) ); - const total = Number(countRows[0]?.total || 0); - const totalPages = Math.max(1, Math.ceil(total / limite)); - return { - data: rows, + data: rows.map((item) => + enriquecerMovimentoParaUsuario( + item, + filtros.idusuarios + ) + ), + pagination: { total, limite, @@ -515,17 +1166,8 @@ async function listarMovimentosExtrato(filtros = {}) { page: Math.floor(offset / limite) + 1, totalPages, }, - summary: { - quantidade: Number(summaryRows[0]?.quantidade || 0), - totalEntradas: Number(summaryRows[0]?.totalEntradas || 0), - totalSaidas: Number(summaryRows[0]?.totalSaidas || 0), - totalSangrias: Number(summaryRows[0]?.totalSangrias || 0), - totalEstornos: Number(summaryRows[0]?.totalEstornos || 0), - totalBaixado: Number(summaryRows[0]?.totalBaixado || 0), - totalAberto: Number(summaryRows[0]?.totalAberto || 0), - saldoProcessado: Number(summaryRows[0]?.saldoProcessado || 0), - saldo: Number(summaryRows[0]?.saldo || 0), - }, + + summary, }; } @@ -533,68 +1175,111 @@ async function listarMovimentos(filtros = {}) { if (filtros.dataCampo === 'extrato') { return listarMovimentosExtrato(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 orderBy = resolverOrdenacao(filtros.orderBy || filtros.dataCampo); - const orderDirection = resolverDirecao(filtros.orderDirection); + const limite = limitarNumero( + filtros.limite, + 20, + 1, + 100 + ); - const { whereSql, params } = montarWhereMovimentos(filtros); + const page = limitarNumero( + filtros.page, + 1, + 1, + 999999 + ); + + const offset = + filtros.offset !== undefined + ? limitarNumero( + filtros.offset, + 0, + 0, + 999999999 + ) + : (page - 1) * limite; + + const orderBy = + resolverOrdenacao( + filtros.orderBy || filtros.dataCampo + ); + + const orderDirection = + resolverDirecao( + filtros.orderDirection + ); + + const { + whereSql, + params, + } = montarWhereMovimentos(filtros); const [rows] = await pool.query( ` SELECT ${CAMPOS_MOVIMENTO_SELECT} + ${FROM_MOVIMENTO_JOIN} + ${whereSql} - ORDER BY ${orderBy} ${orderDirection}, cp.idcontasapagar DESC + + ORDER BY + ${orderBy} ${orderDirection}, + cp.idcontasapagar DESC + LIMIT ? OFFSET ? `, - [...params, limite, offset] + [ + ...params, + limite, + offset, + ] ); const [countRows] = await pool.query( - ` - SELECT COUNT(*) AS total - ${FROM_MOVIMENTO_JOIN} - ${whereSql} - `, - params - ); - - const [summaryRows] = await pool.query( ` SELECT - COUNT(*) AS quantidade, - COALESCE(SUM(CASE WHEN cp.movimento = 'Entrada' THEN cp.valor ELSE 0 END), 0) AS totalEntradas, - COALESCE(SUM(CASE WHEN cp.movimento = 'Saida' THEN cp.valor ELSE 0 END), 0) AS totalSaidas, - COALESCE(SUM(CASE WHEN cp.movimento = 'Sangria' THEN cp.valor ELSE 0 END), 0) AS totalSangrias, - COALESCE(SUM(CASE WHEN cp.movimento = 'Estorno' THEN cp.valor ELSE 0 END), 0) AS totalEstornos, - COALESCE(SUM(CASE WHEN cp.status IN ('Pago', 'Recebido') THEN cp.valor ELSE 0 END), 0) AS totalBaixado, - COALESCE(SUM(CASE WHEN cp.status IN ('A pagar', 'A receber') THEN cp.valor ELSE 0 END), 0) AS totalAberto, - COALESCE(SUM(CASE WHEN cp.saldo_processado = 1 THEN 1 ELSE 0 END), 0) AS saldoProcessado, - COALESCE(SUM( - CASE - WHEN cp.movimento IN ('Entrada', 'Estorno') THEN cp.valor - WHEN cp.movimento IN ('Saida', 'Sangria') THEN -cp.valor - ELSE 0 - END - ), 0) AS saldo + COUNT(*) AS total + ${FROM_MOVIMENTO_JOIN} + ${whereSql} `, params ); - const total = Number(countRows[0]?.total || 0); - const totalPages = Math.max(1, Math.ceil(total / limite)); + /* + * Usa o mesmo contrato financeiro do extrato. + * + * A única diferença é que o campo de data respeita + * o filtro escolhido na tabela: + * entrada, vencimento, baixa etc. + */ + const summary = + await buscarResumoMovimentos( + filtros, + { + modoExtrato: false, + } + ); + + const total = + Number(countRows[0]?.total || 0); + + const totalPages = Math.max( + 1, + Math.ceil(total / limite) + ); return { - data: rows, + data: rows.map((item) => + enriquecerMovimentoParaUsuario( + item, + filtros.idusuarios + ) + ), + pagination: { total, limite, @@ -602,17 +1287,8 @@ async function listarMovimentos(filtros = {}) { page: Math.floor(offset / limite) + 1, totalPages, }, - summary: { - quantidade: Number(summaryRows[0]?.quantidade || 0), - totalEntradas: Number(summaryRows[0]?.totalEntradas || 0), - totalSaidas: Number(summaryRows[0]?.totalSaidas || 0), - totalSangrias: Number(summaryRows[0]?.totalSangrias || 0), - totalEstornos: Number(summaryRows[0]?.totalEstornos || 0), - totalBaixado: Number(summaryRows[0]?.totalBaixado || 0), - totalAberto: Number(summaryRows[0]?.totalAberto || 0), - saldoProcessado: Number(summaryRows[0]?.saldoProcessado || 0), - saldo: Number(summaryRows[0]?.saldo || 0), - }, + + summary, }; } diff --git a/financeiro-web/src/features/dashboard/pages/DashboardPage.tsx b/financeiro-web/src/features/dashboard/pages/DashboardPage.tsx index 90d19f0..e186a45 100644 --- a/financeiro-web/src/features/dashboard/pages/DashboardPage.tsx +++ b/financeiro-web/src/features/dashboard/pages/DashboardPage.tsx @@ -72,7 +72,14 @@ function formatarData(data: string | null | undefined) { } function movimentoColor(movimento: string) { - if (movimento === 'Entrada' || movimento === 'Estorno') return 'success.main'; + if (movimento === 'Entrada' || movimento === 'Estorno') { + return 'success.main'; + } + + if (movimento === 'TransferenciaInterna') { + return 'info.main'; + } + return 'error.main'; } @@ -95,7 +102,15 @@ function isMovimentoEntrada(movimento: string) { } function movimentoBorderColor(movimento: string) { - return isMovimentoEntrada(movimento) ? 'success.main' : 'error.main'; + if (isMovimentoEntrada(movimento)) { + return 'success.main'; + } + + if (movimento === 'TransferenciaInterna') { + return 'info.main'; + } + + return 'error.main'; } function movimentoActionLabel(item: DashboardMovimentoResumo) { @@ -213,7 +228,15 @@ function StatCard({ } function MovimentoLinha({ item }: { item: DashboardMovimentoResumo }) { - const isEntrada = isMovimentoEntrada(item.movimento); + const movimentoVisual = + item.movimento_dashboard || item.movimento; + + const isTransferenciaInterna = + movimentoVisual === 'TransferenciaInterna'; + + const isEntrada = + movimentoVisual === 'Entrada' + || movimentoVisual === 'Estorno'; return ( @@ -241,9 +266,19 @@ function MovimentoLinha({ item }: { item: DashboardMovimentoResumo }) { @@ -276,7 +311,7 @@ function MovimentoLinha({ item }: { item: DashboardMovimentoResumo }) { spacing={0.75} flexShrink={0} > - + {formatarValor(item.valor)} @@ -292,7 +327,13 @@ function MovimentoLinha({ item }: { item: DashboardMovimentoResumo }) { to={`/movimentos/${item.idcontasapagar}/editar`} size="small" variant="contained" - color={isEntrada ? 'success' : 'error'} + color={ + isTransferenciaInterna + ? 'info' + : isEntrada + ? 'success' + : 'error' + } sx={{ borderRadius: 2, boxShadow: 0 }} > {movimentoActionLabel(item)} @@ -833,7 +874,7 @@ export function DashboardPage() { } color="info" percent={cards.percentualInvestimentos} diff --git a/financeiro-web/src/features/dashboard/types/dashboardTypees.ts b/financeiro-web/src/features/dashboard/types/dashboardTypes.ts similarity index 97% rename from financeiro-web/src/features/dashboard/types/dashboardTypees.ts rename to financeiro-web/src/features/dashboard/types/dashboardTypes.ts index 1e8f5bc..e697a6f 100644 --- a/financeiro-web/src/features/dashboard/types/dashboardTypees.ts +++ b/financeiro-web/src/features/dashboard/types/dashboardTypes.ts @@ -70,6 +70,9 @@ export type DashboardMovimentoResumo = { idcentrodecustos?: number | null; centro_custo_descricao?: string | null; dias_para_vencer?: number; + movimento_dashboard?: string; + direcao_dashboard?: string; + transferencia_interna?: boolean; }; export type DashboardCredito = { diff --git a/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx b/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx index f3bbb5e..538f67c 100644 --- a/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx +++ b/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx @@ -144,10 +144,57 @@ function campoDataLabel(campo: DataCampo) { return labels[campo]; } +function getMovimentoVisual(item: Movimento) { + return item.movimento_dashboard || item.movimento; +} + +function movimentoEhEntrada(item: Movimento) { + return item.direcao_dashboard + ? item.direcao_dashboard === 'entrada' + : item.movimento === 'Entrada' + || item.movimento === 'Estorno'; +} + +function movimentoEhSaida(item: Movimento) { + return item.direcao_dashboard + ? item.direcao_dashboard === 'saida' + : item.movimento === 'Saida'; +} + +function movimentoEhTransferenciaInterna(item: Movimento) { + return item.transferencia_interna === true + || item.direcao_dashboard === 'transferencia_interna' + || getMovimentoVisual(item) === 'TransferenciaInterna'; +} + +function movimentoEhInvestimento(item: Movimento) { + return item.investimento_dashboard === true; +} + function movimentoChipColor(movimento: string) { - if (movimento === 'Entrada') return 'success'; - if (movimento === 'Saida') return 'error'; - if (movimento === 'Sangria') return 'warning'; + if (movimento === 'Entrada' || movimento === 'Estorno') { + return 'success'; + } + + if (movimento === 'Saida' || movimento === 'Saída') { + return 'error'; + } + + if ( + movimento === 'TransferenciaInterna' + || movimento === 'Transferência interna' + ) { + return 'info'; + } + + if (movimento === 'Investimento') { + return 'primary'; + } + + if (movimento === 'Sangria') { + return 'warning'; + } + return 'default'; } @@ -264,7 +311,7 @@ function getMovimentoRowVisual(item: Movimento) { }; } -function getMovimentoIconVisual(movimento: MovimentoTipo) { +function getMovimentoIconVisual(movimento: string) { if (movimento === 'Entrada') { return { icon: , @@ -273,7 +320,7 @@ function getMovimentoIconVisual(movimento: MovimentoTipo) { }; } - if (movimento === 'Saida') { + if (movimento === 'Saida' || movimento === 'Saída') { return { icon: , label: 'Saída', @@ -281,6 +328,25 @@ function getMovimentoIconVisual(movimento: MovimentoTipo) { }; } + if ( + movimento === 'TransferenciaInterna' + || movimento === 'Transferência interna' + ) { + return { + icon: , + label: 'Transferência interna', + color: 'info' as const, + }; + } + + if (movimento === 'Investimento') { + return { + icon: , + label: 'Investimento', + color: 'primary' as const, + }; + } + if (movimento === 'Sangria') { return { icon: , @@ -304,6 +370,24 @@ function getMovimentoIconVisual(movimento: MovimentoTipo) { }; } +function movimentoVisualLabel(item: Movimento) { + if (movimentoEhInvestimento(item)) { + return 'Investimento'; + } + + const movimento = getMovimentoVisual(item); + + if (movimento === 'TransferenciaInterna') { + return 'Transferência interna'; + } + + if (movimento === 'Saida') { + return 'Saída'; + } + + return movimento; +} + function formatarDataGrupo(dataISO: string) { if (!dataISO || dataISO === 'sem-data') return 'Sem data'; @@ -329,11 +413,19 @@ function getDataGrupo(item: Movimento, campo: DataCampo) { function valorAssinadoMovimento(item: Movimento) { const valor = Number(item.valor || 0); - if (item.movimento === 'Entrada' || item.movimento === 'Estorno') { + if (movimentoEhTransferenciaInterna(item)) { + return 0; + } + + if (movimentoEhEntrada(item)) { return valor; } - return valor * -1; + if (movimentoEhSaida(item) || movimentoEhInvestimento(item)) { + return valor * -1; + } + + return 0; } function criarResumoGrupo() { @@ -345,15 +437,15 @@ function criarResumoGrupo() { }; } -function aplicarMovimentoNoResumo>( - resumo: T, - item: Movimento -) { +function aplicarMovimentoNoResumo>(resumo: T, item: Movimento) { const valorAssinado = valorAssinadoMovimento(item); - if (valorAssinado >= 0) { - resumo.totalEntradas += valorAssinado; - } else { + if (movimentoEhEntrada(item)) { + resumo.totalEntradas += Math.abs(valorAssinado); + } else if ( + movimentoEhSaida(item) + || movimentoEhInvestimento(item) + ) { resumo.totalSaidas += Math.abs(valorAssinado); } @@ -377,10 +469,24 @@ function agruparMovimentos(movimentos: Movimento[], campo: DataCampo): GrupoDiaM for (const item of movimentos) { const chaveDia = getDataGrupo(item, campo); - const bancoNome = item.banco_descricao || 'Sem banco'; + const bancoNome = + item.direcao_dashboard === 'entrada' + && item.movimento === 'Sangria' + ? item.banco_referencia_descricao + || item.banco_descricao + || 'Sem banco' + : item.banco_descricao + || 'Sem banco'; const centroNome = item.centro_custo_descricao || 'Sem centro de custo'; - const chaveBanco = String(item.idbancos || bancoNome); + const idBancoVisual = + item.direcao_dashboard === 'entrada' + && item.movimento === 'Sangria' + ? item.idbancos_p + : item.idbancos; + + const chaveBanco = + String(idBancoVisual || bancoNome); const chaveCentro = String(item.idcentrodecustos || centroNome); if (!gruposDia.has(chaveDia)) { @@ -473,17 +579,41 @@ export function MovimentosPage() { page: 1, totalPages: 1, }); - const [summary, setSummary] = useState({ + + const SUMMARY_INICIAL: MovimentosSummary = { quantidade: 0, + totalEntradas: 0, + entradasRecebidas: 0, + entradasPendentes: 0, + quantidadeEntradasRecebidas: 0, + quantidadeEntradasPendentes: 0, + percentualEntradasRecebidas: 0, + percentualEntradasPendentes: 0, + totalSaidas: 0, - totalSangrias: 0, + saidasPagas: 0, + saidasPendentes: 0, + quantidadeSaidasPagas: 0, + quantidadeSaidasPendentes: 0, + percentualSaidasPagas: 0, + percentualSaidasPendentes: 0, + + totalInvestimentos: 0, + investimentosRealizados: 0, + investimentosPendentes: 0, + quantidadeInvestimentosRealizados: 0, + quantidadeInvestimentosPendentes: 0, + percentualInvestimentosRealizados: 0, + percentualInvestimentosPendentes: 0, + totalEstornos: 0, - totalBaixado: 0, - totalAberto: 0, saldoProcessado: 0, saldo: 0, - }); + }; + + const [summary, setSummary] = + useState(SUMMARY_INICIAL); const [modoVisualizacao, setModoVisualizacao] = useState('tabela'); const [gruposAbertos, setGruposAbertos] = useState>({}); @@ -968,7 +1098,8 @@ export function MovimentosPage() { {centro.movimentos.map((item) => { const rowVisual = getMovimentoRowVisual(item); - const movimentoIcon = getMovimentoIconVisual(item.movimento); + const movimentoVisual = movimentoVisualLabel(item); + const movimentoIcon = getMovimentoIconVisual(movimentoVisual); const dataPrincipal = getDataPrincipal(item, dataCampo); return ( @@ -1046,15 +1177,19 @@ export function MovimentosPage() { - {item.movimento === 'Entrada' || item.movimento === 'Estorno' - ? '+' - : '-'} + {movimentoEhTransferenciaInterna(item) + ? '' + : movimentoEhEntrada(item) + ? '+' + : '-'} {formatarValor(item.valor)} @@ -1094,6 +1229,133 @@ export function MovimentosPage() { ); } + function CardResumoFinanceiro({ + titulo, + total, + realizadoLabel, + realizado, + realizadoQuantidade, + realizadoPercentual, + pendenteLabel, + pendente, + pendenteQuantidade, + pendentePercentual, + corTotal, + }: { + titulo: string; + total: number; + realizadoLabel: string; + realizado: number; + realizadoQuantidade: number; + realizadoPercentual: number; + pendenteLabel: string; + pendente: number; + pendenteQuantidade: number; + pendentePercentual: number; + corTotal: 'success.main' | 'error.main' | 'primary.main'; + }) { + return ( + + + + {titulo} + + + + {formatarValor(total)} + + + + + + + + + {realizadoLabel} + + + + {formatarValor(realizado)} + + + + + + + + {realizadoQuantidade} movimento(s) + + + + + + {pendenteLabel} + + + + {formatarValor(pendente)} + + + + + + + + {pendenteQuantidade} movimento(s) + + + + + ); + } + return ( - + Registros encontrados - - {summary.quantidade} - - - - - - - Entradas - - - {formatarValor(summary.totalEntradas)} - - - - - - - - Saídas - - - {formatarValor(summary.totalSaidas + summary.totalSangrias)} - - - - - - - - Saldo filtrado - = 0 ? 'success.main' : 'error.main'} + > + {summary.quantidade} + + + + Movimentos financeiros filtrados + + + + + + + + + + + + + + Saldo filtrado + + + = 0 + ? 'success.main' + : 'error.main' + } + marginTop={0.25} > {formatarValor(summary.saldo)} - - - - - - Em aberto - - - {formatarValor(summary.totalAberto)} - - - + - - - - Baixados - - - {formatarValor(summary.totalBaixado)} + + Entradas menos saídas e investimentos @@ -1553,7 +1870,8 @@ export function MovimentosPage() { {movimentos.map((item) => { const dataPrincipal = getDataPrincipal(item, dataCampo); const rowVisual = getMovimentoRowVisual(item); - const movimentoIcon = getMovimentoIconVisual(item.movimento); + const movimentoVisual = movimentoVisualLabel(item); + const movimentoIcon = getMovimentoIconVisual(movimentoVisual); return ( @@ -1621,9 +1944,11 @@ export function MovimentosPage() { { const dataPrincipal = getDataPrincipal(item, dataCampo); const rowVisual = getMovimentoRowVisual(item); - const movimentoIcon = getMovimentoIconVisual(item.movimento); + const movimentoVisual = movimentoVisualLabel(item); + const movimentoIcon = getMovimentoIconVisual(movimentoVisual); return ( {formatarValor(item.valor)} @@ -1853,9 +2184,11 @@ export function MovimentosPage() { diff --git a/financeiro-web/src/features/movimentos/types/movimentoTypes.ts b/financeiro-web/src/features/movimentos/types/movimentoTypes.ts index fd14051..f5e988c 100644 --- a/financeiro-web/src/features/movimentos/types/movimentoTypes.ts +++ b/financeiro-web/src/features/movimentos/types/movimentoTypes.ts @@ -43,6 +43,15 @@ export type Movimento = { banco_referencia_debito?: number | null; movimento_fixo_descricao?: string | null; ocorrencia_extrato?: 'lancamento' | 'baixa' | null; + + movimento_dashboard?: string; + direcao_dashboard?: 'entrada' | 'saida' | 'transferencia_interna' | 'neutro'; + transferencia_interna?: boolean; + investimento_dashboard?: boolean; + + idusuario_origem?: number | null; + idusuario_destino?: number | null; + banco_destino_investimento?: number | null; }; export type CriarMovimentoRequest = { @@ -115,14 +124,41 @@ export type MovimentosPagination = { export type MovimentosSummary = { quantidade: number; + totalEntradas: number; + entradasRecebidas: number; + entradasPendentes: number; + quantidadeEntradasRecebidas: number; + quantidadeEntradasPendentes: number; + percentualEntradasRecebidas: number; + percentualEntradasPendentes: number; + totalSaidas: number; - totalSangrias: number; + saidasPagas: number; + saidasPendentes: number; + quantidadeSaidasPagas: number; + quantidadeSaidasPendentes: number; + percentualSaidasPagas: number; + percentualSaidasPendentes: number; + + totalInvestimentos: number; + investimentosRealizados: number; + investimentosPendentes: number; + quantidadeInvestimentosRealizados: number; + quantidadeInvestimentosPendentes: number; + percentualInvestimentosRealizados: number; + percentualInvestimentosPendentes: number; + totalEstornos: number; - totalBaixado: number; - totalAberto: number; saldoProcessado: number; saldo: number; + + /* + * Compatibilidade temporária com versões antigas da API. + */ + totalSangrias?: number; + totalBaixado?: number; + totalAberto?: number; }; export type ApiResponse = {