diff --git a/financeiro-api/src/modules/movimentos/services/movimentos.service.js b/financeiro-api/src/modules/movimentos/services/movimentos.service.js index 320b702..872a175 100644 --- a/financeiro-api/src/modules/movimentos/services/movimentos.service.js +++ b/financeiro-api/src/modules/movimentos/services/movimentos.service.js @@ -29,6 +29,8 @@ const CAMPOS_MOVIMENTO_SELECT = ` cp.insert_date, cp.update_date, cp.deleted_at, + NULL AS data_extrato, + NULL AS ocorrencia_extrato, b.descricao AS banco_descricao, b.debito AS banco_debito, cc.descricao AS centro_custo_descricao, @@ -47,6 +49,48 @@ const FROM_MOVIMENTO_JOIN = ` LEFT JOIN movimentosfixos mf ON mf.idmovimentosfixos = cp.idmovimentosfixos `; +function montarSelectExtrato(campoDataSql, ocorrencia) { + return ` + 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, + DATE(${campoDataSql}) AS data_extrato, + '${ocorrencia}' AS ocorrencia_extrato, + 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, + mf.descricao AS movimento_fixo_descricao + ${FROM_MOVIMENTO_JOIN} + `; +} + function normalizarNumeroOuNull(valor) { if (valor === undefined || valor === null || valor === '') { return null; @@ -189,6 +233,8 @@ function resolverOrdenacao(orderBy) { banco_descricao: 'b.descricao', centro_custo_descricao: 'cc.descricao', cliente_nome: 'c.nome', + extrato: 'data_extrato', + data_extrato: 'data_extrato', }; return camposPermitidos[orderBy] || 'cp.datavencimento'; @@ -199,6 +245,28 @@ function resolverDirecao(orderDirection) { } function montarWhereMovimentos(filtros = {}) { + const { where, params } = montarWhereMovimentosBase(filtros); + const campoData = resolverCampoData(filtros.dataCampo); + + if (filtros.dataInicio) { + where.push(`DATE(${campoData}) >= ?`); + params.push(filtros.dataInicio); + } + + if (filtros.dataFim) { + where.push(`DATE(${campoData}) <= ?`); + params.push(filtros.dataFim); + } + + const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''; + + return { + whereSql, + params, + }; +} + +function montarWhereMovimentosBase(filtros = {}) { const where = []; const params = []; @@ -261,7 +329,11 @@ function montarWhereMovimentos(filtros = {}) { params.push(filtros.origem); } - if (filtros.saldo_processado !== undefined && filtros.saldo_processado !== null && filtros.saldo_processado !== '') { + if ( + filtros.saldo_processado !== undefined && + filtros.saldo_processado !== null && + filtros.saldo_processado !== '' + ) { where.push('cp.saldo_processado = ?'); params.push(Number(filtros.saldo_processado)); } @@ -278,18 +350,6 @@ function montarWhereMovimentos(filtros = {}) { where.push('cp.idclientes IS NULL AND cp.idbancos_p IS NULL'); } - const campoData = resolverCampoData(filtros.dataCampo); - - if (filtros.dataInicio) { - where.push(`DATE(${campoData}) >= ?`); - params.push(filtros.dataInicio); - } - - if (filtros.dataFim) { - where.push(`DATE(${campoData}) <= ?`); - params.push(filtros.dataFim); - } - if (filtros.busca) { where.push(` ( @@ -322,15 +382,158 @@ function montarWhereMovimentos(filtros = {}) { ); } - const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''; - return { - whereSql, + where, params, }; } +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 orderDirection = resolverDirecao(filtros.orderDirection); + + const base = montarWhereMovimentosBase(filtros); + + function montarWhereOcorrencia(campoDataSql, extras = []) { + const where = [...base.where]; + + 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) { + params.push(filtros.dataFim); + } + + return { + whereSql: where.length > 0 ? `WHERE ${where.join(' AND ')}` : '', + params, + }; + } + + const lancamento = montarWhereOcorrencia('cp.dataentrada'); + + const baixa = montarWhereOcorrencia('cp.databaixa', [ + `( + cp.dataentrada IS NULL + OR DATE(cp.databaixa) <> DATE(cp.dataentrada) + )`, + ]); + + const selectLancamento = ` + ${montarSelectExtrato('cp.dataentrada', 'lancamento')} + ${lancamento.whereSql} + `; + + const selectBaixa = ` + ${montarSelectExtrato('cp.databaixa', 'baixa')} + ${baixa.whereSql} + `; + + const unionSql = ` + ( + ${selectLancamento} + UNION ALL + ${selectBaixa} + ) extrato + `; + + const paramsUnion = [ + ...lancamento.params, + ...baixa.params, + ]; + + const [rows] = await pool.query( + ` + SELECT * + FROM ${unionSql} + ORDER BY data_extrato ${orderDirection}, idcontasapagar DESC + LIMIT ? OFFSET ? + `, + [...paramsUnion, limite, offset] + ); + + const [countRows] = await pool.query( + ` + SELECT COUNT(*) AS total + FROM ${unionSql} + `, + 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 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), + 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), + }, + }; +} + 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 diff --git a/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx b/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx index 58875fc..5c7f869 100644 --- a/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx +++ b/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx @@ -132,6 +132,7 @@ function formatarData(data: string | null) { function campoDataLabel(campo: DataCampo) { const labels: Record = { + extrato: 'Extrato', dataentrada: 'Entrada', datavencimento: 'Vencimento', databaixa: 'Baixa', @@ -157,11 +158,16 @@ function statusChipColor(status: string) { } function getDataPrincipal(item: Movimento, campo: DataCampo) { + if (campo === 'extrato') { + return item.data_extrato || item.databaixa || item.dataentrada; + } + if (campo === 'dataentrada') return item.dataentrada; if (campo === 'datavencimento') return item.datavencimento; if (campo === 'databaixa') return item.databaixa; if (campo === 'insert_date') return item.insert_date; if (campo === 'update_date') return item.update_date; + if (campo === 'deleted_at') return item.deleted_at; return item.datavencimento; } @@ -491,7 +497,7 @@ export function MovimentosPage() { const [erro, setErro] = useState(''); const [page, setPage] = useState(1); - const [dataCampo, setDataCampo] = useState('dataentrada'); + const [dataCampo, setDataCampo] = useState('extrato'); const [dataInicio, setDataInicio] = useState(inicioMesAtual()); const [dataFim, setDataFim] = useState(fimMesAtual()); const [busca, setBusca] = useState(''); @@ -568,7 +574,8 @@ export function MovimentosPage() { async function carregarMovimentos( pageToLoad = page, - modoParaCarregar = modoVisualizacao + modoParaCarregar = modoVisualizacao, + dataCampoParaCarregar = dataCampo ) { try { setLoading(true); @@ -577,7 +584,7 @@ export function MovimentosPage() { const params: ListarMovimentosParams = { limite: modoParaCarregar === 'agrupado' ? LIMITE_AGRUPADO : LIMITE_PADRAO, page: modoParaCarregar === 'agrupado' ? 1 : pageToLoad, - dataCampo, + dataCampo: dataCampoParaCarregar, dataInicio, dataFim, busca: busca.trim() || undefined, @@ -591,7 +598,7 @@ export function MovimentosPage() { origem: origem || undefined, saldo_processado: saldoProcessado, incluirExcluidos, - orderBy: dataCampo, + orderBy: dataCampoParaCarregar, orderDirection: 'DESC', }; @@ -619,7 +626,7 @@ export function MovimentosPage() { } function limparFiltros() { - setDataCampo('dataentrada'); + setDataCampo('extrato'); setDataInicio(inicioMesAtual()); setDataFim(fimMesAtual()); setBusca(''); @@ -668,6 +675,13 @@ export function MovimentosPage() { return Number(valor) === 1 ? 'Saldo processado' : 'Sem impacto'; } + function ocorrenciaExtratoLabel(item: Movimento) { + if (item.ocorrencia_extrato === 'baixa') return 'Baixa'; + if (item.ocorrencia_extrato === 'lancamento') return 'Lançamento'; + + return null; + } + function saldoProcessadoColor(valor: number | null | undefined) { return Number(valor) === 1 ? 'success' : 'default'; } @@ -720,12 +734,18 @@ export function MovimentosPage() { function mudarModoVisualizacao(novoModo: ModoVisualizacao | null) { if (!novoModo) return; + const novoCampoData: DataCampo = + novoModo === 'agrupado' && dataCampo === 'dataentrada' + ? 'extrato' + : dataCampo; + setModoVisualizacao(novoModo); + setDataCampo(novoCampoData); setGruposAbertos({}); setPage(1); setTimeout(() => { - carregarMovimentos(1, novoModo); + carregarMovimentos(1, novoModo, novoCampoData); }, 0); } @@ -953,7 +973,7 @@ export function MovimentosPage() { return ( - {campoDataLabel(dataCampo)}: {formatarData(dataPrincipal)} ·{' '} + {campoDataLabel(dataCampo)}: {formatarData(dataPrincipal)} + {dataCampo === 'extrato' && ocorrenciaExtratoLabel(item) + ? ` · ${ocorrenciaExtratoLabel(item)}` + : ''} + {' · '} Ref.: {getReferencia(item)} · {origemLabel(item.origem)} @@ -1248,6 +1272,7 @@ export function MovimentosPage() { onChange={(event) => setDataCampo(event.target.value as DataCampo)} fullWidth > + Extrato real Data de vencimento Data de entrada Data de baixa @@ -1532,7 +1557,7 @@ export function MovimentosPage() { return ( Tipo + Ocorrência Descrição Entrada Vencimento @@ -1727,7 +1753,7 @@ export function MovimentosPage() { return ( + + {dataCampo === 'extrato' && ocorrenciaExtratoLabel(item) ? ( + + ) : ( + '-' + )} + + diff --git a/financeiro-web/src/features/movimentos/services/movimentosService.ts b/financeiro-web/src/features/movimentos/services/movimentosService.ts index ef3059d..e8b335c 100644 --- a/financeiro-web/src/features/movimentos/services/movimentosService.ts +++ b/financeiro-web/src/features/movimentos/services/movimentosService.ts @@ -37,6 +37,15 @@ export type ListarMovimentosParams = { orderDirection?: OrderDirection; }; +export type DataCampo = + | 'extrato' + | 'dataentrada' + | 'datavencimento' + | 'databaixa' + | 'insert_date' + | 'update_date' + | 'deleted_at'; + export async function listarMovimentos( params?: ListarMovimentosParams ): Promise { diff --git a/financeiro-web/src/features/movimentos/types/movimentoTypes.ts b/financeiro-web/src/features/movimentos/types/movimentoTypes.ts index e2a956d..fd14051 100644 --- a/financeiro-web/src/features/movimentos/types/movimentoTypes.ts +++ b/financeiro-web/src/features/movimentos/types/movimentoTypes.ts @@ -11,6 +11,7 @@ export type Movimento = { dataentrada: string | null; datavencimento: string | null; databaixa: string | null; + data_extrato?: string | null; valor: number; parcela: number; parcelas: number; @@ -41,6 +42,7 @@ export type Movimento = { banco_referencia_descricao?: string | null; banco_referencia_debito?: number | null; movimento_fixo_descricao?: string | null; + ocorrencia_extrato?: 'lancamento' | 'baixa' | null; }; export type CriarMovimentoRequest = {