criado agrupamento por data de extrato

This commit is contained in:
Diego Freitas 2026-06-03 20:31:16 -03:00
parent 759e642567
commit 09ef64f491
4 changed files with 280 additions and 27 deletions

View File

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

View File

@ -132,6 +132,7 @@ function formatarData(data: string | null) {
function campoDataLabel(campo: DataCampo) {
const labels: Record<DataCampo, string> = {
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<DataCampo>('dataentrada');
const [dataCampo, setDataCampo] = useState<DataCampo>('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 (
<Paper
key={item.idcontasapagar}
key={`${item.idcontasapagar}:${item.ocorrencia_extrato || 'normal'}:${getDataPrincipal(item, dataCampo) || ''}`}
elevation={0}
sx={{
padding: 1.25,
@ -1000,7 +1020,11 @@ export function MovimentosPage() {
</Typography>
<Typography variant="caption" color="text.secondary">
{campoDataLabel(dataCampo)}: {formatarData(dataPrincipal)} ·{' '}
{campoDataLabel(dataCampo)}: {formatarData(dataPrincipal)}
{dataCampo === 'extrato' && ocorrenciaExtratoLabel(item)
? ` · ${ocorrenciaExtratoLabel(item)}`
: ''}
{' · '}
Ref.: {getReferencia(item)} · {origemLabel(item.origem)}
</Typography>
</Box>
@ -1248,6 +1272,7 @@ export function MovimentosPage() {
onChange={(event) => setDataCampo(event.target.value as DataCampo)}
fullWidth
>
<MenuItem value="extrato">Extrato real</MenuItem>
<MenuItem value="datavencimento">Data de vencimento</MenuItem>
<MenuItem value="dataentrada">Data de entrada</MenuItem>
<MenuItem value="databaixa">Data de baixa</MenuItem>
@ -1532,7 +1557,7 @@ export function MovimentosPage() {
return (
<Box
key={item.idcontasapagar}
key={`${item.idcontasapagar}:${item.ocorrencia_extrato || 'normal'}:${getDataPrincipal(item, dataCampo) || ''}`}
sx={{
padding: 2,
backgroundColor: rowVisual.backgroundColor,
@ -1683,7 +1708,7 @@ export function MovimentosPage() {
<Table
size="small"
sx={{
minWidth: 1720,
minWidth: 1840,
'& th': {
whiteSpace: 'nowrap',
fontWeight: 900,
@ -1697,6 +1722,7 @@ export function MovimentosPage() {
<TableHead>
<TableRow>
<TableCell sx={{ minWidth: 70 }} align="center">Tipo</TableCell>
<TableCell sx={{ minWidth: 120 }}>Ocorrência</TableCell>
<TableCell sx={{ minWidth: 260 }}>Descrição</TableCell>
<TableCell sx={{ minWidth: 120 }}>Entrada</TableCell>
<TableCell sx={{ minWidth: 130 }}>Vencimento</TableCell>
@ -1727,7 +1753,7 @@ export function MovimentosPage() {
return (
<TableRow
key={item.idcontasapagar}
key={`${item.idcontasapagar}:${item.ocorrencia_extrato || 'normal'}:${getDataPrincipal(item, dataCampo) || ''}`}
hover
sx={{
backgroundColor: rowVisual.backgroundColor,
@ -1762,6 +1788,19 @@ export function MovimentosPage() {
</Tooltip>
</TableCell>
<TableCell>
{dataCampo === 'extrato' && ocorrenciaExtratoLabel(item) ? (
<Chip
label={ocorrenciaExtratoLabel(item)}
size="small"
color={item.ocorrencia_extrato === 'baixa' ? 'success' : 'info'}
variant="outlined"
/>
) : (
'-'
)}
</TableCell>
<TableCell>
<Box sx={{ maxWidth: 260 }}>
<Typography fontWeight={800} noWrap title={item.descricao}>

View File

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

View File

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