financeiro/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx

1966 lines
67 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import {
Alert,
Box,
Button,
Card,
CardContent,
Chip,
CircularProgress,
Collapse,
Divider,
IconButton,
MenuItem,
Pagination,
Paper,
Stack,
TextField,
ToggleButton,
ToggleButtonGroup,
Tooltip,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
useMediaQuery,
useTheme,
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import FilterAltIcon from '@mui/icons-material/FilterAlt';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import SearchIcon from '@mui/icons-material/Search';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import DeleteIcon from '@mui/icons-material/Delete';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import UndoIcon from '@mui/icons-material/Undo';
import HelpIcon from '@mui/icons-material/Help';
import AccountBalanceIcon from '@mui/icons-material/AccountBalance';
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import CategoryIcon from '@mui/icons-material/Category';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import TableRowsIcon from '@mui/icons-material/TableRows';
import ViewAgendaIcon from '@mui/icons-material/ViewAgenda';
import { Link as RouterLink } from 'react-router-dom';
import {
deletarMovimento,
listarMovimentos,
type DataCampo,
type ListarMovimentosParams,
} from '../services/movimentosService';
import type {
Movimento,
MovimentoStatus,
MovimentoTipo,
MovimentosPagination,
MovimentosSummary,
} from '../types/movimentoTypes';
import {
listarBancos,
listarCentrosCusto,
listarClientes,
} from '../../referencias/services/referenciasService';
import type {
Banco,
CentroCusto,
Cliente,
} from '../../referencias/types/referenciasTypes';
const LIMITE_PADRAO = 20;
const DIAS_ALERTA_VENCIMENTO = 5;
const LIMITE_AGRUPADO = 500;
type ModoVisualizacao = 'tabela' | 'agrupado';
type GrupoCentroMovimentos = {
chave: string;
nome: string;
totalEntradas: number;
totalSaidas: number;
saldo: number;
quantidade: number;
movimentos: Movimento[];
};
type GrupoBancoMovimentos = {
chave: string;
nome: string;
totalEntradas: number;
totalSaidas: number;
saldo: number;
quantidade: number;
centros: GrupoCentroMovimentos[];
};
type GrupoDiaMovimentos = {
chave: string;
dataLabel: string;
totalEntradas: number;
totalSaidas: number;
saldo: number;
quantidade: number;
bancos: GrupoBancoMovimentos[];
};
function inicioMesAtual() {
const hoje = new Date();
return new Date(hoje.getFullYear(), hoje.getMonth(), 1).toISOString().slice(0, 10);
}
function fimMesAtual() {
const hoje = new Date();
return new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0).toISOString().slice(0, 10);
}
function formatarValor(valor: number) {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(Number(valor || 0));
}
function formatarData(data: string | null) {
if (!data) return '-';
return new Date(`${String(data).slice(0, 10)}T00:00:00`).toLocaleDateString('pt-BR');
}
function campoDataLabel(campo: DataCampo) {
const labels: Record<DataCampo, string> = {
dataentrada: 'Entrada',
datavencimento: 'Vencimento',
databaixa: 'Baixa',
insert_date: 'Cadastro',
update_date: 'Atualização',
deleted_at: 'Exclusão',
};
return labels[campo];
}
function movimentoChipColor(movimento: string) {
if (movimento === 'Entrada') return 'success';
if (movimento === 'Saida') return 'error';
if (movimento === 'Sangria') return 'warning';
return 'default';
}
function statusChipColor(status: string) {
if (status === 'Pago' || status === 'Recebido') return 'success';
if (status === 'A pagar' || status === 'A receber') return 'warning';
return 'default';
}
function getDataPrincipal(item: Movimento, campo: DataCampo) {
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;
return item.datavencimento;
}
function getReferencia(item: Movimento) {
if (item.cliente_nome) return item.cliente_nome;
if (item.banco_referencia_descricao) return item.banco_referencia_descricao;
return '-';
}
function dataSomenteDia(data: string | null | undefined) {
if (!data) return null;
const texto = String(data).slice(0, 10);
if (!/^\d{4}-\d{2}-\d{2}$/.test(texto)) {
return null;
}
return new Date(`${texto}T00:00:00`);
}
function diferencaDias(dataAlvo: Date, dataBase: Date) {
const umDiaMs = 24 * 60 * 60 * 1000;
const alvo = new Date(
dataAlvo.getFullYear(),
dataAlvo.getMonth(),
dataAlvo.getDate()
);
const base = new Date(
dataBase.getFullYear(),
dataBase.getMonth(),
dataBase.getDate()
);
return Math.floor((alvo.getTime() - base.getTime()) / umDiaMs);
}
function movimentoCompensado(item: Movimento) {
return item.status === 'Pago' || item.status === 'Recebido';
}
function movimentoEmAberto(item: Movimento) {
return item.status === 'A pagar' || item.status === 'A receber';
}
function getMovimentoRowVisual(item: Movimento) {
if (movimentoExcluido(item)) {
return {
backgroundColor: 'rgba(148,163,184,0.10)',
hoverColor: 'rgba(148,163,184,0.16)',
borderLeftColor: 'rgba(100,116,139,0.65)',
};
}
if (movimentoCompensado(item)) {
return {
backgroundColor: 'rgba(34,197,94,0.08)',
hoverColor: 'rgba(34,197,94,0.13)',
borderLeftColor: 'rgba(34,197,94,0.75)',
};
}
if (movimentoEmAberto(item)) {
const vencimento = dataSomenteDia(item.datavencimento);
if (vencimento) {
const dias = diferencaDias(vencimento, new Date());
if (dias < 0) {
return {
backgroundColor: 'rgba(239,68,68,0.08)',
hoverColor: 'rgba(239,68,68,0.14)',
borderLeftColor: 'rgba(239,68,68,0.78)',
};
}
if (dias <= DIAS_ALERTA_VENCIMENTO) {
return {
backgroundColor: 'rgba(245,158,11,0.10)',
hoverColor: 'rgba(245,158,11,0.16)',
borderLeftColor: 'rgba(245,158,11,0.85)',
};
}
}
}
return {
backgroundColor: '#FFFFFF',
hoverColor: 'rgba(15,23,42,0.02)',
borderLeftColor: 'transparent',
};
}
function getMovimentoIconVisual(movimento: MovimentoTipo) {
if (movimento === 'Entrada') {
return {
icon: <ArrowDownwardIcon fontSize="small" />,
label: 'Entrada',
color: 'success' as const,
};
}
if (movimento === 'Saida') {
return {
icon: <ArrowUpwardIcon fontSize="small" />,
label: 'Saída',
color: 'error' as const,
};
}
if (movimento === 'Sangria') {
return {
icon: <SwapHorizIcon fontSize="small" />,
label: 'Sangria',
color: 'warning' as const,
};
}
if (movimento === 'Estorno') {
return {
icon: <UndoIcon fontSize="small" />,
label: 'Estorno',
color: 'info' as const,
};
}
return {
icon: <HelpIcon fontSize="small" />,
label: movimento || 'Movimento',
color: 'default' as const,
};
}
function formatarDataGrupo(dataISO: string) {
if (!dataISO || dataISO === 'sem-data') return 'Sem data';
return new Date(`${dataISO}T00:00:00`).toLocaleDateString('pt-BR', {
weekday: 'short',
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
}
function getDataGrupo(item: Movimento, campo: DataCampo) {
const data = getDataPrincipal(item, campo);
const texto = data ? String(data).slice(0, 10) : '';
if (!/^\d{4}-\d{2}-\d{2}$/.test(texto)) {
return 'sem-data';
}
return texto;
}
function valorAssinadoMovimento(item: Movimento) {
const valor = Number(item.valor || 0);
if (item.movimento === 'Entrada' || item.movimento === 'Estorno') {
return valor;
}
return valor * -1;
}
function criarResumoGrupo() {
return {
totalEntradas: 0,
totalSaidas: 0,
saldo: 0,
quantidade: 0,
};
}
function aplicarMovimentoNoResumo<T extends ReturnType<typeof criarResumoGrupo>>(
resumo: T,
item: Movimento
) {
const valorAssinado = valorAssinadoMovimento(item);
if (valorAssinado >= 0) {
resumo.totalEntradas += valorAssinado;
} else {
resumo.totalSaidas += Math.abs(valorAssinado);
}
resumo.saldo += valorAssinado;
resumo.quantidade += 1;
}
function agruparMovimentos(movimentos: Movimento[], campo: DataCampo): GrupoDiaMovimentos[] {
const gruposDia = new Map<string, {
resumo: ReturnType<typeof criarResumoGrupo>;
bancos: Map<string, {
nome: string;
resumo: ReturnType<typeof criarResumoGrupo>;
centros: Map<string, {
nome: string;
resumo: ReturnType<typeof criarResumoGrupo>;
movimentos: Movimento[];
}>;
}>;
}>();
for (const item of movimentos) {
const chaveDia = getDataGrupo(item, campo);
const bancoNome = item.banco_descricao || 'Sem banco';
const centroNome = item.centro_custo_descricao || 'Sem centro de custo';
const chaveBanco = String(item.idbancos || bancoNome);
const chaveCentro = String(item.idcentrodecustos || centroNome);
if (!gruposDia.has(chaveDia)) {
gruposDia.set(chaveDia, {
resumo: criarResumoGrupo(),
bancos: new Map(),
});
}
const grupoDia = gruposDia.get(chaveDia)!;
aplicarMovimentoNoResumo(grupoDia.resumo, item);
if (!grupoDia.bancos.has(chaveBanco)) {
grupoDia.bancos.set(chaveBanco, {
nome: bancoNome,
resumo: criarResumoGrupo(),
centros: new Map(),
});
}
const grupoBanco = grupoDia.bancos.get(chaveBanco)!;
aplicarMovimentoNoResumo(grupoBanco.resumo, item);
if (!grupoBanco.centros.has(chaveCentro)) {
grupoBanco.centros.set(chaveCentro, {
nome: centroNome,
resumo: criarResumoGrupo(),
movimentos: [],
});
}
const grupoCentro = grupoBanco.centros.get(chaveCentro)!;
aplicarMovimentoNoResumo(grupoCentro.resumo, item);
grupoCentro.movimentos.push(item);
}
return Array
.from(gruposDia.entries())
.sort(([a], [b]) => b.localeCompare(a))
.map(([chaveDia, dia]) => ({
chave: chaveDia,
dataLabel: formatarDataGrupo(chaveDia),
totalEntradas: dia.resumo.totalEntradas,
totalSaidas: dia.resumo.totalSaidas,
saldo: dia.resumo.saldo,
quantidade: dia.resumo.quantidade,
bancos: Array
.from(dia.bancos.entries())
.sort(([, a], [, b]) => a.nome.localeCompare(b.nome))
.map(([chaveBanco, banco]) => ({
chave: chaveBanco,
nome: banco.nome,
totalEntradas: banco.resumo.totalEntradas,
totalSaidas: banco.resumo.totalSaidas,
saldo: banco.resumo.saldo,
quantidade: banco.resumo.quantidade,
centros: Array
.from(banco.centros.entries())
.sort(([, a], [, b]) => a.nome.localeCompare(b.nome))
.map(([chaveCentro, centro]) => ({
chave: chaveCentro,
nome: centro.nome,
totalEntradas: centro.resumo.totalEntradas,
totalSaidas: centro.resumo.totalSaidas,
saldo: centro.resumo.saldo,
quantidade: centro.resumo.quantidade,
movimentos: centro.movimentos.sort((a, b) => {
const dataA = String(getDataPrincipal(a, campo) || '');
const dataB = String(getDataPrincipal(b, campo) || '');
return dataB.localeCompare(dataA);
}),
})),
})),
}));
}
function movimentoExcluido(item: Movimento) {
return Boolean(item.deleted_at);
}
export function MovimentosPage() {
const [sucesso, setSucesso] = useState('');
const [movimentos, setMovimentos] = useState<Movimento[]>([]);
const [pagination, setPagination] = useState<MovimentosPagination>({
total: 0,
limite: LIMITE_PADRAO,
offset: 0,
page: 1,
totalPages: 1,
});
const [summary, setSummary] = useState<MovimentosSummary>({
quantidade: 0,
totalEntradas: 0,
totalSaidas: 0,
totalSangrias: 0,
totalEstornos: 0,
totalBaixado: 0,
totalAberto: 0,
saldoProcessado: 0,
saldo: 0,
});
const [modoVisualizacao, setModoVisualizacao] = useState<ModoVisualizacao>('tabela');
const [gruposAbertos, setGruposAbertos] = useState<Record<string, boolean>>({});
const [bancos, setBancos] = useState<Banco[]>([]);
const [centrosCusto, setCentrosCusto] = useState<CentroCusto[]>([]);
const [clientes, setClientes] = useState<Cliente[]>([]);
const [loading, setLoading] = useState(true);
const [loadingRefs, setLoadingRefs] = useState(true);
const [erro, setErro] = useState('');
const [page, setPage] = useState(1);
const [dataCampo, setDataCampo] = useState<DataCampo>('dataentrada');
const [dataInicio, setDataInicio] = useState(inicioMesAtual());
const [dataFim, setDataFim] = useState(fimMesAtual());
const [busca, setBusca] = useState('');
const [movimento, setMovimento] = useState<MovimentoTipo | ''>('');
const [status, setStatus] = useState<MovimentoStatus | ''>('');
const [idBanco, setIdBanco] = useState<number | ''>('');
const [idCentroCusto, setIdCentroCusto] = useState<number | ''>('');
const [idCliente, setIdCliente] = useState<number | ''>('');
const [referenciaTipo, setReferenciaTipo] = useState('');
const [competencia, setCompetencia] = useState('');
const [origem, setOrigem] = useState('');
const [saldoProcessado, setSaldoProcessado] = useState<number | ''>('');
const [incluirExcluidos, setIncluirExcluidos] = useState<number | ''>('');
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const filtrosAtivos = useMemo(() => {
let count = 0;
if (busca.trim()) count += 1;
if (movimento) count += 1;
if (status) count += 1;
if (idBanco) count += 1;
if (idCentroCusto) count += 1;
if (idCliente) count += 1;
if (referenciaTipo) count += 1;
if (dataInicio || dataFim) count += 1;
if (competencia.trim()) count += 1;
if (origem) count += 1;
if (saldoProcessado !== '') count += 1;
if (incluirExcluidos !== '') count += 1;
return count;
}, [
busca,
movimento,
status,
idBanco,
idCentroCusto,
idCliente,
referenciaTipo,
dataInicio,
dataFim,
competencia,
origem,
saldoProcessado,
incluirExcluidos,
]);
const movimentosAgrupados = useMemo(() => {
return agruparMovimentos(movimentos, dataCampo);
}, [movimentos, dataCampo]);
const totalMaiorQueLimiteAgrupado = modoVisualizacao === 'agrupado' && pagination.total > LIMITE_AGRUPADO;
async function carregarReferencias() {
try {
setLoadingRefs(true);
const [bancosData, centrosData, clientesData] = await Promise.all([
listarBancos(),
listarCentrosCusto(),
listarClientes(),
]);
setBancos(bancosData);
setCentrosCusto(centrosData);
setClientes(clientesData);
} finally {
setLoadingRefs(false);
}
}
async function carregarMovimentos(
pageToLoad = page,
modoParaCarregar = modoVisualizacao
) {
try {
setLoading(true);
setErro('');
const params: ListarMovimentosParams = {
limite: modoParaCarregar === 'agrupado' ? LIMITE_AGRUPADO : LIMITE_PADRAO,
page: modoParaCarregar === 'agrupado' ? 1 : pageToLoad,
dataCampo,
dataInicio,
dataFim,
busca: busca.trim() || undefined,
movimento: movimento || undefined,
status: status || undefined,
idbancos: idBanco || undefined,
idcentrodecustos: idCentroCusto || undefined,
idclientes: idCliente || undefined,
referenciaTipo: referenciaTipo || undefined,
competencia: competencia.trim() || undefined,
origem: origem || undefined,
saldo_processado: saldoProcessado,
incluirExcluidos,
orderBy: dataCampo,
orderDirection: 'DESC',
};
const response = await listarMovimentos(params);
setMovimentos(response.data);
setPagination(response.pagination);
setSummary(response.summary);
setPage(response.pagination.page);
} catch (error: any) {
const message =
error?.response?.data?.message ||
'Não foi possível carregar os movimentos.';
setErro(message);
} finally {
setLoading(false);
}
}
function aplicarFiltros() {
setGruposAbertos({});
setPage(1);
carregarMovimentos(1, modoVisualizacao);
}
function limparFiltros() {
setDataCampo('dataentrada');
setDataInicio(inicioMesAtual());
setDataFim(fimMesAtual());
setBusca('');
setMovimento('');
setStatus('');
setIdBanco('');
setIdCentroCusto('');
setIdCliente('');
setReferenciaTipo('');
setCompetencia('');
setOrigem('');
setSaldoProcessado('');
setIncluirExcluidos('');
setGruposAbertos({});
setTimeout(() => {
setPage(1);
carregarMovimentos(1, modoVisualizacao);
}, 0);
}
function origemLabel(origem: string | null | undefined) {
if (!origem) return 'Manual';
const labels: Record<string, string> = {
manual: 'Manual',
parcelamento: 'Parcelamento',
movimento_fixo: 'Movimento fixo',
quitacao: 'Quitação',
ajuste_saldo: 'Ajuste de saldo',
};
return labels[origem] || origem;
}
function origemChipColor(origem: string | null | undefined) {
if (origem === 'movimento_fixo') return 'info';
if (origem === 'parcelamento') return 'secondary';
if (origem === 'quitacao') return 'success';
if (origem === 'ajuste_saldo') return 'warning';
return 'default';
}
function saldoProcessadoLabel(valor: number | null | undefined) {
return Number(valor) === 1 ? 'Saldo processado' : 'Sem impacto';
}
function saldoProcessadoColor(valor: number | null | undefined) {
return Number(valor) === 1 ? 'success' : 'default';
}
async function handleDeletarMovimento(item: Movimento) {
const confirmou = window.confirm(
`Deseja realmente excluir "${item.descricao}"?\n\n` +
'Se o movimento já impactou saldo, a API vai reverter automaticamente.'
);
if (!confirmou) return;
try {
setLoading(true);
setErro('');
setSucesso('');
await deletarMovimento(item.idcontasapagar);
setSucesso('Movimento excluído com sucesso. O saldo foi revertido se necessário.');
await carregarMovimentos(page);
} catch (error: any) {
const message =
error?.response?.data?.message ||
'Não foi possível excluir o movimento.';
setErro(message);
} finally {
setLoading(false);
}
}
useEffect(() => {
carregarReferencias();
carregarMovimentos(1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function toggleGrupo(chave: string) {
setGruposAbertos((estadoAtual) => ({
...estadoAtual,
[chave]: !estadoAtual[chave],
}));
}
function grupoAberto(chave: string) {
return gruposAbertos[chave] === true;
}
function mudarModoVisualizacao(novoModo: ModoVisualizacao | null) {
if (!novoModo) return;
setModoVisualizacao(novoModo);
setGruposAbertos({});
setPage(1);
setTimeout(() => {
carregarMovimentos(1, novoModo);
}, 0);
}
function ResumoGrupoValores({
entradas,
saidas,
saldo,
}: {
entradas: number;
saidas: number;
saldo: number;
}) {
return (
<Stack direction="row" flexWrap="wrap" spacing={1} useFlexGap>
<Chip
size="small"
label={`Entradas ${formatarValor(entradas)}`}
color="success"
variant="outlined"
/>
<Chip
size="small"
label={`Saídas ${formatarValor(saidas)}`}
color="error"
variant="outlined"
/>
<Chip
size="small"
label={`Saldo ${formatarValor(saldo)}`}
color={saldo >= 0 ? 'success' : 'error'}
/>
</Stack>
);
}
function renderExtratoAgrupado() {
if (movimentosAgrupados.length === 0) {
return (
<Box padding={3}>
<Typography fontWeight={800}>
Nenhum movimento encontrado.
</Typography>
<Typography color="text.secondary" marginTop={0.5}>
Ajuste os filtros ou cadastre um novo movimento.
</Typography>
</Box>
);
}
return (
<Stack spacing={2} padding={{ xs: 1.5, md: 2 }}>
{movimentosAgrupados.map((dia) => {
const chaveDia = `dia:${dia.chave}`;
const abertoDia = grupoAberto(chaveDia);
return (
<Paper
key={chaveDia}
elevation={0}
sx={{
border: '1px solid',
borderColor: 'divider',
borderRadius: 3,
overflow: 'hidden',
background: 'linear-gradient(180deg, #FFFFFF 0%, #F8FAFC 100%)',
}}
>
<Box
onClick={() => toggleGrupo(chaveDia)}
sx={{
cursor: 'pointer',
padding: { xs: 1.5, md: 2 },
backgroundColor: 'rgba(15,23,42,0.035)',
}}
>
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', md: 'center' }}
spacing={1.5}
>
<Stack direction="row" alignItems="center" spacing={1.25}>
<CalendarMonthIcon color="primary" />
<Box>
<Typography fontWeight={950}>
{dia.dataLabel}
</Typography>
<Typography variant="body2" color="text.secondary">
{dia.quantidade} movimento(s)
</Typography>
</Box>
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
<ResumoGrupoValores
entradas={dia.totalEntradas}
saidas={dia.totalSaidas}
saldo={dia.saldo}
/>
{abertoDia ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</Stack>
</Stack>
</Box>
<Collapse in={abertoDia} timeout="auto" unmountOnExit>
<Stack spacing={1.5} padding={{ xs: 1.5, md: 2 }}>
{dia.bancos.map((banco) => {
const chaveBanco = `${chaveDia}:banco:${banco.chave}`;
const abertoBanco = grupoAberto(chaveBanco);
return (
<Paper
key={chaveBanco}
elevation={0}
sx={{
border: '1px solid',
borderColor: 'divider',
borderRadius: 2.5,
overflow: 'hidden',
backgroundColor: '#FFFFFF',
}}
>
<Box
onClick={() => toggleGrupo(chaveBanco)}
sx={{
cursor: 'pointer',
padding: 1.5,
}}
>
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', md: 'center' }}
spacing={1}
>
<Stack direction="row" alignItems="center" spacing={1}>
<AccountBalanceIcon color="action" />
<Box>
<Typography fontWeight={900}>
{banco.nome}
</Typography>
<Typography variant="caption" color="text.secondary">
{banco.quantidade} movimento(s)
</Typography>
</Box>
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
<ResumoGrupoValores
entradas={banco.totalEntradas}
saidas={banco.totalSaidas}
saldo={banco.saldo}
/>
{abertoBanco ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</Stack>
</Stack>
</Box>
<Collapse in={abertoBanco} timeout="auto" unmountOnExit>
<Stack spacing={1} padding={1.5} paddingTop={0}>
{banco.centros.map((centro) => {
const chaveCentro = `${chaveBanco}:centro:${centro.chave}`;
const abertoCentro = grupoAberto(chaveCentro);
return (
<Paper
key={chaveCentro}
elevation={0}
sx={{
border: '1px solid',
borderColor: 'divider',
borderRadius: 2,
overflow: 'hidden',
backgroundColor: 'rgba(248,250,252,0.9)',
}}
>
<Box
onClick={() => toggleGrupo(chaveCentro)}
sx={{
cursor: 'pointer',
padding: 1.25,
}}
>
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', md: 'center' }}
spacing={1}
>
<Stack direction="row" alignItems="center" spacing={1}>
<CategoryIcon color="action" />
<Box>
<Typography fontWeight={850}>
{centro.nome}
</Typography>
<Typography variant="caption" color="text.secondary">
{centro.quantidade} movimento(s)
</Typography>
</Box>
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
<ResumoGrupoValores
entradas={centro.totalEntradas}
saidas={centro.totalSaidas}
saldo={centro.saldo}
/>
{abertoCentro ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</Stack>
</Stack>
</Box>
<Collapse in={abertoCentro} timeout="auto" unmountOnExit>
<Stack spacing={0.75} padding={1.25} paddingTop={0}>
{centro.movimentos.map((item) => {
const rowVisual = getMovimentoRowVisual(item);
const movimentoIcon = getMovimentoIconVisual(item.movimento);
const dataPrincipal = getDataPrincipal(item, dataCampo);
return (
<Paper
key={item.idcontasapagar}
elevation={0}
sx={{
padding: 1.25,
borderRadius: 2,
border: '1px solid',
borderColor: 'divider',
borderLeft: '5px solid',
borderLeftColor: rowVisual.borderLeftColor,
backgroundColor: rowVisual.backgroundColor,
'&:hover': {
backgroundColor: rowVisual.hoverColor,
},
}}
>
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', md: 'center' }}
spacing={1.25}
>
<Stack direction="row" spacing={1} alignItems="center">
<Tooltip title={movimentoIcon.label}>
<Chip
icon={movimentoIcon.icon}
label=""
size="small"
color={movimentoIcon.color as any}
sx={{
width: 36,
'& .MuiChip-label': {
display: 'none',
},
'& .MuiChip-icon': {
marginLeft: 0,
marginRight: 0,
},
}}
/>
</Tooltip>
<Box sx={{ minWidth: 0 }}>
<Typography fontWeight={900} noWrap title={item.descricao}>
{item.descricao}
</Typography>
<Typography variant="caption" color="text.secondary">
{campoDataLabel(dataCampo)}: {formatarData(dataPrincipal)} ·{' '}
Ref.: {getReferencia(item)} · {origemLabel(item.origem)}
</Typography>
</Box>
</Stack>
<Stack
direction="row"
spacing={1}
alignItems="center"
justifyContent={{ xs: 'space-between', md: 'flex-end' }}
>
<Chip
label={item.status}
size="small"
variant="outlined"
color={statusChipColor(item.status) as any}
/>
<Typography
fontWeight={950}
color={
item.movimento === 'Entrada' || item.movimento === 'Estorno'
? 'success.main'
: 'error.main'
}
whiteSpace="nowrap"
>
{item.movimento === 'Entrada' || item.movimento === 'Estorno'
? '+'
: '-'}
{formatarValor(item.valor)}
</Typography>
{!movimentoExcluido(item) && (
<Tooltip title="Editar movimento">
<IconButton
component={RouterLink}
to={`/movimentos/${item.idcontasapagar}/editar`}
color="primary"
size="small"
>
<EditIcon />
</IconButton>
</Tooltip>
)}
</Stack>
</Stack>
</Paper>
);
})}
</Stack>
</Collapse>
</Paper>
);
})}
</Stack>
</Collapse>
</Paper>
);
})}
</Stack>
</Collapse>
</Paper>
);
})}
</Stack>
);
}
return (
<Box>
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', md: 'center' }}
spacing={2}
marginBottom={{ xs: 3, md: 4 }}
>
<Box>
<Chip
icon={<SwapHorizIcon />}
label="Central de movimentos"
color="primary"
variant="outlined"
sx={{ marginBottom: 1.25, fontWeight: 700 }}
/>
<Typography variant="h4" fontWeight={950}>
Movimentos
</Typography>
<Typography color="text.secondary" sx={{ marginTop: 0.5 }}>
Consulte, filtre e edite os lançamentos financeiros.
</Typography>
</Box>
<Button
component={RouterLink}
to="/movimentos/novo"
variant="contained"
startIcon={<AddIcon />}
sx={{
minHeight: 48,
borderRadius: 3,
px: 2.5,
boxShadow: 3,
}}
>
Novo movimento
</Button>
</Stack>
{erro && (
<Alert severity="error" sx={{ marginBottom: 2.5 }}>
{erro}
</Alert>
)}
{sucesso && (
<Alert severity="success" sx={{ marginBottom: 2.5 }}>
{sucesso}
</Alert>
)}
<Box
display="grid"
gridTemplateColumns={{
xs: '1fr',
md: 'repeat(6, 1fr)',
}}
gap={{ xs: 2, md: 2.5 }}
marginBottom={{ xs: 2.5, md: 3 }}
>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Registros encontrados
</Typography>
<Typography variant="h5" fontWeight={950}>
{summary.quantidade}
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Entradas
</Typography>
<Typography variant="h5" fontWeight={950} color="success.main">
{formatarValor(summary.totalEntradas)}
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Saídas
</Typography>
<Typography variant="h5" fontWeight={950} color="error.main">
{formatarValor(summary.totalSaidas + summary.totalSangrias)}
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Saldo filtrado
</Typography>
<Typography
variant="h5"
fontWeight={950}
color={summary.saldo >= 0 ? 'success.main' : 'error.main'}
>
{formatarValor(summary.saldo)}
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Em aberto
</Typography>
<Typography variant="h5" fontWeight={950} color="warning.main">
{formatarValor(summary.totalAberto)}
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Baixados
</Typography>
<Typography variant="h5" fontWeight={950} color="info.main">
{formatarValor(summary.totalBaixado)}
</Typography>
</CardContent>
</Card>
</Box>
<Card sx={{ marginBottom: { xs: 2.5, md: 3 } }}>
<CardContent sx={{ padding: { xs: 2, md: 3 } }}>
<Stack
direction="row"
spacing={1}
alignItems="center"
marginBottom={2.5}
>
<FilterAltIcon color="primary" />
<Typography variant="h6" fontWeight={900}>
Filtros
</Typography>
<Chip
label={`${filtrosAtivos} ativo(s)`}
size="small"
variant="outlined"
/>
</Stack>
<Box
display="grid"
gridTemplateColumns={{
xs: '1fr',
md: 'repeat(4, 1fr)',
}}
gap={{ xs: 2, md: 2.25 }}
>
<TextField
label="Buscar"
value={busca}
onChange={(event) => setBusca(event.target.value)}
placeholder="Descrição, observação, banco, cliente..."
fullWidth
/>
<TextField
select
label="Campo de data"
value={dataCampo}
onChange={(event) => setDataCampo(event.target.value as DataCampo)}
fullWidth
>
<MenuItem value="datavencimento">Data de vencimento</MenuItem>
<MenuItem value="dataentrada">Data de entrada</MenuItem>
<MenuItem value="databaixa">Data de baixa</MenuItem>
<MenuItem value="insert_date">Data de cadastro</MenuItem>
<MenuItem value="update_date">Data de atualização</MenuItem>
<MenuItem value="deleted_at">Data de exclusão</MenuItem>
</TextField>
<TextField
label="Data inicial"
type="date"
value={dataInicio}
onChange={(event) => setDataInicio(event.target.value)}
InputLabelProps={{ shrink: true }}
fullWidth
/>
<TextField
label="Data final"
type="date"
value={dataFim}
onChange={(event) => setDataFim(event.target.value)}
InputLabelProps={{ shrink: true }}
fullWidth
/>
<TextField
select
label="Movimento"
value={movimento}
onChange={(event) => setMovimento(event.target.value as MovimentoTipo | '')}
fullWidth
>
<MenuItem value="">Todos</MenuItem>
<MenuItem value="Entrada">Entrada</MenuItem>
<MenuItem value="Saida">Saída</MenuItem>
<MenuItem value="Sangria">Sangria</MenuItem>
<MenuItem value="Estorno">Estorno</MenuItem>
</TextField>
<TextField
select
label="Situação"
value={status}
onChange={(event) => setStatus(event.target.value as MovimentoStatus | '')}
fullWidth
>
<MenuItem value="">Todas</MenuItem>
<MenuItem value="Pago">Pago</MenuItem>
<MenuItem value="A pagar">A pagar</MenuItem>
<MenuItem value="Recebido">Recebido</MenuItem>
<MenuItem value="A receber">A receber</MenuItem>
</TextField>
<TextField
select
label="Banco/Carteira"
value={idBanco}
onChange={(event) =>
setIdBanco(event.target.value === '' ? '' : Number(event.target.value))
}
disabled={loadingRefs}
fullWidth
>
<MenuItem value="">Todos</MenuItem>
{bancos.map((banco) => (
<MenuItem key={banco.id} value={banco.id}>
{banco.descricao}
</MenuItem>
))}
</TextField>
<TextField
select
label="Centro de custo"
value={idCentroCusto}
onChange={(event) =>
setIdCentroCusto(event.target.value === '' ? '' : Number(event.target.value))
}
disabled={loadingRefs}
fullWidth
>
<MenuItem value="">Todos</MenuItem>
{centrosCusto.map((centro) => (
<MenuItem key={centro.id} value={centro.id}>
{centro.descricao}
</MenuItem>
))}
</TextField>
<TextField
select
label="Cliente"
value={idCliente}
onChange={(event) =>
setIdCliente(event.target.value === '' ? '' : Number(event.target.value))
}
disabled={loadingRefs}
fullWidth
>
<MenuItem value="">Todos</MenuItem>
{clientes.map((cliente) => (
<MenuItem key={cliente.id} value={cliente.id}>
{cliente.nome}
</MenuItem>
))}
</TextField>
<TextField
select
label="Referência"
value={referenciaTipo}
onChange={(event) => setReferenciaTipo(event.target.value)}
fullWidth
>
<MenuItem value="">Todas</MenuItem>
<MenuItem value="cliente">Com cliente</MenuItem>
<MenuItem value="banco">Com banco referência</MenuItem>
<MenuItem value="sem_referencia">Sem referência</MenuItem>
</TextField>
<TextField
label="Competência"
type="month"
value={competencia}
onChange={(event) => setCompetencia(event.target.value)}
InputLabelProps={{ shrink: true }}
fullWidth
/>
<TextField
select
label="Origem"
value={origem}
onChange={(event) => setOrigem(event.target.value)}
fullWidth
>
<MenuItem value="">Todas</MenuItem>
<MenuItem value="manual">Manual</MenuItem>
<MenuItem value="parcelamento">Parcelamento</MenuItem>
<MenuItem value="movimento_fixo">Movimento fixo</MenuItem>
<MenuItem value="quitacao">Quitação</MenuItem>
<MenuItem value="ajuste_saldo">Ajuste de saldo</MenuItem>
</TextField>
<TextField
select
label="Saldo processado"
value={saldoProcessado}
onChange={(event) =>
setSaldoProcessado(event.target.value === '' ? '' : Number(event.target.value))
}
fullWidth
>
<MenuItem value="">Todos</MenuItem>
<MenuItem value={1}>Sim</MenuItem>
<MenuItem value={0}>Não</MenuItem>
</TextField>
<TextField
select
label="Excluídos"
value={incluirExcluidos}
onChange={(event) =>
setIncluirExcluidos(event.target.value === '' ? '' : Number(event.target.value))
}
fullWidth
>
<MenuItem value="">Somente ativos</MenuItem>
<MenuItem value={1}>Incluir excluídos</MenuItem>
</TextField>
</Box>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={1.5}
justifyContent="flex-end"
marginTop={2.5}
>
<Button
variant="outlined"
startIcon={<RestartAltIcon />}
onClick={limparFiltros}
>
Limpar
</Button>
<Button
variant="contained"
startIcon={<SearchIcon />}
onClick={aplicarFiltros}
disabled={loading}
>
Aplicar filtros
</Button>
</Stack>
</CardContent>
</Card>
<Stack
direction="row"
flexWrap="wrap"
spacing={1}
useFlexGap
marginBottom={1.5}
>
<Chip size="small" label="Compensado" color="success" variant="outlined" />
<Chip size="small" label="Vencido em aberto" color="error" variant="outlined" />
<Chip size="small" label="Próximo do vencimento" color="warning" variant="outlined" />
<Chip size="small" label="Excluído" color="default" variant="outlined" />
</Stack>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={1.5}
marginBottom={1.5}
>
<Box>
<Typography variant="h6" fontWeight={900}>
Resultado
</Typography>
<Typography variant="body2" color="text.secondary">
Alterne entre a tabela detalhada e o extrato agrupado por dia, banco e centro.
</Typography>
</Box>
<ToggleButtonGroup
exclusive
size="small"
value={modoVisualizacao}
onChange={(_, novoModo) => mudarModoVisualizacao(novoModo)}
>
<ToggleButton value="tabela">
<Stack direction="row" spacing={0.75} alignItems="center">
<TableRowsIcon fontSize="small" />
<span>Tabela</span>
</Stack>
</ToggleButton>
<ToggleButton value="agrupado">
<Stack direction="row" spacing={0.75} alignItems="center">
<ViewAgendaIcon fontSize="small" />
<span>Extrato</span>
</Stack>
</ToggleButton>
</ToggleButtonGroup>
</Stack>
{totalMaiorQueLimiteAgrupado && (
<Alert severity="warning" sx={{ marginBottom: 1.5 }}>
Exibindo os primeiros {LIMITE_AGRUPADO} movimentos do filtro no modo extrato.
Refine o período para uma análise completa.
</Alert>
)}
<Card>
<CardContent sx={{ padding: 0 }}>
{loading ? (
<Box padding={3} display="flex" alignItems="center" gap={2}>
<CircularProgress size={22} />
<Typography>Carregando movimentos...</Typography>
</Box>
) : modoVisualizacao === 'agrupado' ? (
renderExtratoAgrupado()
) : movimentos.length === 0 ? (
<Box padding={3}>
<Typography fontWeight={800}>
Nenhum movimento encontrado.
</Typography>
<Typography color="text.secondary" marginTop={0.5}>
Ajuste os filtros ou cadastre um novo movimento.
</Typography>
</Box>
) : isMobile ? (
<Stack divider={<Divider />}>
{movimentos.map((item) => {
const dataPrincipal = getDataPrincipal(item, dataCampo);
const rowVisual = getMovimentoRowVisual(item);
const movimentoIcon = getMovimentoIconVisual(item.movimento);
return (
<Box
key={item.idcontasapagar}
sx={{
padding: 2,
backgroundColor: rowVisual.backgroundColor,
borderLeft: '5px solid',
borderLeftColor: rowVisual.borderLeftColor,
'&:hover': {
backgroundColor: rowVisual.hoverColor,
},
}}
>
<Stack spacing={1.25}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-start"
spacing={2}
>
<Box sx={{ minWidth: 0 }}>
<Stack direction="row" alignItems="center" spacing={1}>
<Tooltip title={movimentoIcon.label}>
<Chip
icon={movimentoIcon.icon}
label=""
size="small"
color={movimentoIcon.color as any}
sx={{
width: 36,
'& .MuiChip-label': {
display: 'none',
},
'& .MuiChip-icon': {
marginLeft: 0,
marginRight: 0,
},
}}
/>
</Tooltip>
<Typography fontWeight={900}>
{item.descricao}
</Typography>
</Stack>
<Typography variant="body2" color="text.secondary">
{campoDataLabel(dataCampo)}: {formatarData(dataPrincipal)}
</Typography>
</Box>
<Typography
fontWeight={950}
color={
item.movimento === 'Entrada' || item.movimento === 'Estorno'
? 'success.main'
: 'text.primary'
}
whiteSpace="nowrap"
>
{formatarValor(item.valor)}
</Typography>
</Stack>
<Stack direction="row" flexWrap="wrap" spacing={1} useFlexGap>
<Chip
label={item.movimento}
size="small"
color={movimentoChipColor(item.movimento) as any}
/>
<Chip
label={item.status}
size="small"
variant="outlined"
color={statusChipColor(item.status) as any}
/>
<Chip
label={`${item.parcela}/${item.parcelas}`}
size="small"
variant="outlined"
/>
<Chip
label={origemLabel(item.origem)}
size="small"
color={origemChipColor(item.origem) as any}
variant="outlined"
/>
<Chip
label={saldoProcessadoLabel(item.saldo_processado)}
size="small"
color={saldoProcessadoColor(item.saldo_processado) as any}
variant={Number(item.saldo_processado) === 1 ? 'filled' : 'outlined'}
/>
{movimentoExcluido(item) && (
<Chip
label="Excluído"
size="small"
color="error"
variant="outlined"
/>
)}
</Stack>
<Typography variant="body2" color="text.secondary">
Banco: <strong>{item.banco_descricao || '-'}</strong>
</Typography>
<Typography variant="body2" color="text.secondary">
Centro: <strong>{item.centro_custo_descricao || '-'}</strong>
</Typography>
<Typography variant="body2" color="text.secondary">
Referência: <strong>{getReferencia(item)}</strong>
</Typography>
<Typography variant="body2" color="text.secondary">
Competência: <strong>{item.competencia || '-'}</strong>
</Typography>
<Typography variant="body2" color="text.secondary">
Origem: <strong>{origemLabel(item.origem)}</strong>
</Typography>
<Box display="flex" justifyContent="flex-end">
<Button
component={RouterLink}
to={`/movimentos/${item.idcontasapagar}/editar`}
variant="outlined"
size="small"
startIcon={<EditIcon />}
>
Editar
</Button>
</Box>
</Stack>
</Box>
);
})}
</Stack>
) : (
<TableContainer
sx={{
overflowX: 'auto',
}}
>
<Table
size="small"
sx={{
minWidth: 1720,
'& th': {
whiteSpace: 'nowrap',
fontWeight: 900,
backgroundColor: 'rgba(15,23,42,0.04)',
},
'& td': {
whiteSpace: 'nowrap',
},
}}
>
<TableHead>
<TableRow>
<TableCell sx={{ minWidth: 70 }} align="center">Tipo</TableCell>
<TableCell sx={{ minWidth: 260 }}>Descrição</TableCell>
<TableCell sx={{ minWidth: 120 }}>Entrada</TableCell>
<TableCell sx={{ minWidth: 130 }}>Vencimento</TableCell>
<TableCell sx={{ minWidth: 120 }}>Baixa</TableCell>
<TableCell sx={{ minWidth: 120 }} align="right">Valor</TableCell>
<TableCell sx={{ minWidth: 90 }} align="center">Parcela</TableCell>
<TableCell sx={{ minWidth: 95 }} align="center">Parcelas</TableCell>
<TableCell sx={{ minWidth: 130 }}>Situação</TableCell>
<TableCell sx={{ minWidth: 150 }}>Movimento</TableCell>
<TableCell sx={{ minWidth: 130 }}>Competência</TableCell>
<TableCell sx={{ minWidth: 150 }}>Origem</TableCell>
<TableCell sx={{ minWidth: 150 }}>Saldo</TableCell>
<TableCell sx={{ minWidth: 180 }}>Centro de custo</TableCell>
<TableCell sx={{ minWidth: 180 }}>Banco</TableCell>
<TableCell sx={{ minWidth: 180 }}>Cliente</TableCell>
<TableCell sx={{ minWidth: 180 }}>Banco ref.</TableCell>
<TableCell sx={{ minWidth: 220 }}>Observação</TableCell>
<TableCell sx={{ minWidth: 130 }}>Exclusão</TableCell>
<TableCell sx={{ minWidth: 90 }} align="center">Ações</TableCell>
</TableRow>
</TableHead>
<TableBody>
{movimentos.map((item) => {
const dataPrincipal = getDataPrincipal(item, dataCampo);
const rowVisual = getMovimentoRowVisual(item);
const movimentoIcon = getMovimentoIconVisual(item.movimento);
return (
<TableRow
key={item.idcontasapagar}
hover
sx={{
backgroundColor: rowVisual.backgroundColor,
borderLeft: '5px solid',
borderLeftColor: rowVisual.borderLeftColor,
'&:hover': {
backgroundColor: rowVisual.hoverColor,
},
'&:last-child td': {
borderBottom: 0,
},
}}
>
<TableCell align="center">
<Tooltip title={movimentoIcon.label}>
<Chip
icon={movimentoIcon.icon}
label=""
size="small"
color={movimentoIcon.color as any}
sx={{
width: 38,
'& .MuiChip-label': {
display: 'none',
},
'& .MuiChip-icon': {
marginLeft: 0,
marginRight: 0,
},
}}
/>
</Tooltip>
</TableCell>
<TableCell>
<Box sx={{ maxWidth: 260 }}>
<Typography fontWeight={800} noWrap title={item.descricao}>
{item.descricao}
</Typography>
</Box>
</TableCell>
<TableCell>
{formatarData(item.dataentrada)}
</TableCell>
<TableCell>
{formatarData(item.datavencimento)}
</TableCell>
<TableCell>
{formatarData(item.databaixa)}
</TableCell>
<TableCell align="right">
<Typography
fontWeight={950}
color={
item.movimento === 'Entrada' || item.movimento === 'Estorno'
? 'success.main'
: 'text.primary'
}
>
{formatarValor(item.valor)}
</Typography>
</TableCell>
<TableCell align="center">
{item.parcela}
</TableCell>
<TableCell align="center">
{item.parcelas}
</TableCell>
<TableCell>
<Chip
label={item.status}
size="small"
variant="outlined"
color={statusChipColor(item.status) as any}
/>
</TableCell>
<TableCell>
<Chip
label={item.movimento}
size="small"
color={movimentoChipColor(item.movimento) as any}
/>
</TableCell>
<TableCell>
{item.competencia || '-'}
</TableCell>
<TableCell>
<Chip
label={origemLabel(item.origem)}
size="small"
color={origemChipColor(item.origem) as any}
variant="outlined"
/>
</TableCell>
<TableCell>
<Chip
label={saldoProcessadoLabel(item.saldo_processado)}
size="small"
color={saldoProcessadoColor(item.saldo_processado) as any}
variant={Number(item.saldo_processado) === 1 ? 'filled' : 'outlined'}
/>
</TableCell>
<TableCell>
<Typography variant="body2" noWrap title={item.centro_custo_descricao || '-'}>
{item.centro_custo_descricao || '-'}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2" noWrap title={item.banco_descricao || '-'}>
{item.banco_descricao || '-'}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2" noWrap title={item.cliente_nome || '-'}>
{item.cliente_nome || '-'}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2" noWrap title={item.banco_referencia_descricao || '-'}>
{item.banco_referencia_descricao || '-'}
</Typography>
</TableCell>
<TableCell>
<Box sx={{ maxWidth: 220 }}>
<Typography variant="body2" noWrap title={item.observacao || '-'}>
{item.observacao || '-'}
</Typography>
</Box>
</TableCell>
<TableCell>
{formatarData(item.deleted_at)}
</TableCell>
<TableCell align="center">
<Stack
direction="row"
spacing={0.5}
alignItems="center"
justifyContent="center"
>
{!movimentoExcluido(item) && (
<>
<Tooltip title="Editar movimento">
<IconButton
component={RouterLink}
to={`/movimentos/${item.idcontasapagar}/editar`}
color="primary"
size="small"
>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip title="Excluir movimento">
<IconButton
color="error"
size="small"
onClick={() => handleDeletarMovimento(item)}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
)}
</Stack>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
)}
</CardContent>
</Card>
{modoVisualizacao === 'tabela' && pagination.totalPages > 1 && (
<Stack alignItems="center" marginTop={3}>
<Pagination
page={pagination.page}
count={pagination.totalPages}
color="primary"
onChange={(_, novaPagina) => {
setPage(novaPagina);
carregarMovimentos(novaPagina);
}}
/>
</Stack>
)}
<Paper
elevation={0}
sx={{
marginTop: 2,
padding: 2,
border: '1px solid',
borderColor: 'divider',
borderRadius: 3,
backgroundColor: 'rgba(255,255,255,0.65)',
}}
>
<Typography variant="body2" color="text.secondary">
{modoVisualizacao === 'agrupado' ? (
<>
Exibindo <strong>{movimentos.length}</strong> movimento(s) no extrato agrupado,
de um total de <strong>{pagination.total}</strong> registro(s) filtrado(s).
</>
) : (
<>
Exibindo página {pagination.page} de {pagination.totalPages}, total de{' '}
<strong>{pagination.total}</strong> registro(s).
</>
)}
</Typography>
</Paper>
</Box>
);
}