1966 lines
67 KiB
TypeScript
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>
|
|
);
|
|
} |