ajustado filtros na dashboard e movimentos

This commit is contained in:
Diego Freitas 2026-06-08 22:30:31 -03:00
parent 180d756195
commit 12dcc8506f
6 changed files with 1924 additions and 391 deletions

View File

@ -157,87 +157,225 @@ async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) {
const [rows] = await pool.query(
`
SELECT
COALESCE(SUM(CASE
WHEN cp.movimento IN ('Entrada', 'Estorno') THEN cp.valor
ELSE 0
END), 0) AS entradas,
COALESCE(SUM(
CASE
/* Entradas e estornos normais */
WHEN cp.movimento IN ('Entrada', 'Estorno')
AND banco_origem.idusuarios = ?
THEN cp.valor
COALESCE(SUM(CASE
WHEN cp.movimento IN ('Saida', 'Sangria')
/* Sangria recebida de outro usuário */
WHEN cp.movimento = 'Sangria'
AND banco_destino.idusuarios = ?
AND (
banco_origem.idusuarios IS NULL
OR banco_origem.idusuarios <> ?
)
THEN cp.valor
ELSE 0
END
), 0) AS entradas,
COALESCE(SUM(
CASE
/* Saída operacional normal */
WHEN cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 0
THEN cp.valor
ELSE 0
END), 0) AS saidas,
COALESCE(SUM(CASE
WHEN cp.movimento IN ('Saida', 'Sangria')
/* Sangria enviada para outro usuário */
WHEN cp.movimento = 'Sangria'
AND banco_origem.idusuarios = ?
AND (
banco_destino.idusuarios IS NULL
OR banco_destino.idusuarios <> ?
)
THEN cp.valor
ELSE 0
END
), 0) AS saidas,
COALESCE(SUM(
CASE
/* Saída marcada como investimento pelo centro de custo */
WHEN cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 1
THEN cp.valor
ELSE 0
END), 0) AS investimentos,
COALESCE(SUM(CASE
/* Transferência para banco de investimento do próprio usuário */
WHEN cp.movimento = 'Sangria'
AND banco_origem.idusuarios = ?
AND banco_destino.idusuarios = ?
AND COALESCE(banco_destino.investimento, 0) = 1
THEN cp.valor
ELSE 0
END
), 0) AS investimentos,
COALESCE(SUM(
CASE
WHEN cp.status = 'A receber'
AND cp.movimento IN ('Entrada', 'Estorno')
AND banco_origem.idusuarios = ?
THEN cp.valor
ELSE 0
END), 0) AS abertoEntradas,
END
), 0) AS abertoEntradas,
COUNT(CASE
COUNT(
CASE
WHEN cp.status = 'A receber'
AND cp.movimento IN ('Entrada', 'Estorno')
AND banco_origem.idusuarios = ?
THEN 1
ELSE NULL
END) AS quantidadeAbertoEntradas,
END
) AS quantidadeAbertoEntradas,
COALESCE(SUM(CASE
COALESCE(SUM(
CASE
WHEN cp.status = 'A pagar'
AND cp.movimento IN ('Saida', 'Sangria')
AND cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 0
THEN cp.valor
ELSE 0
END), 0) AS abertoSaidas,
END
), 0) AS abertoSaidas,
COUNT(CASE
COUNT(
CASE
WHEN cp.status = 'A pagar'
AND cp.movimento IN ('Saida', 'Sangria')
AND cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 0
THEN 1
ELSE NULL
END) AS quantidadeAbertoSaidas,
END
) AS quantidadeAbertoSaidas,
COALESCE(SUM(CASE
COALESCE(SUM(
CASE
WHEN cp.status = 'A pagar'
AND cp.movimento IN ('Saida', 'Sangria')
AND cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 1
THEN cp.valor
ELSE 0
END), 0) AS abertoInvestimentos,
END
), 0) AS abertoInvestimentos,
COUNT(CASE
COUNT(
CASE
WHEN cp.status = 'A pagar'
AND cp.movimento IN ('Saida', 'Sangria')
AND cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 1
THEN 1
ELSE NULL
END) AS quantidadeAbertoInvestimentos,
END
) AS quantidadeAbertoInvestimentos,
COALESCE(SUM(CASE
WHEN cp.status IN ('Pago', 'Recebido') THEN cp.valor
COALESCE(SUM(
CASE
WHEN cp.status IN ('Pago', 'Recebido')
AND (
cp.movimento <> 'Sangria'
OR (
cp.movimento = 'Sangria'
AND banco_origem.idusuarios = ?
AND (
banco_destino.idusuarios IS NULL
OR banco_destino.idusuarios <> ?
)
)
OR (
cp.movimento = 'Sangria'
AND banco_destino.idusuarios = ?
AND banco_origem.idusuarios <> ?
)
)
THEN cp.valor
ELSE 0
END), 0) AS baixado,
END
), 0) AS baixado,
COUNT(*) AS quantidadeMovimentos
FROM contasapagar cp
INNER JOIN bancos b
ON b.idbancos = cp.idbancos
INNER JOIN bancos banco_origem
ON banco_origem.idbancos = cp.idbancos
LEFT JOIN bancos banco_destino
ON banco_destino.idbancos = cp.idbancos_p
LEFT JOIN centrodecustos cc
ON cc.idcentrodecustos = cp.idcentrodecustos
WHERE cp.deleted_at IS NULL
AND b.idusuarios = ?
AND DATE(cp.datavencimento) BETWEEN ? AND ?
AND (
banco_origem.idusuarios = ?
OR banco_destino.idusuarios = ?
)
AND DATE(COALESCE(cp.dataentrada, cp.insert_date))
BETWEEN ? AND ?
`,
[idUsuario, dataInicio, dataFim]
[
/* entradas */
idUsuario,
idUsuario,
idUsuario,
/* saídas */
idUsuario,
idUsuario,
idUsuario,
/* investimentos */
idUsuario,
idUsuario,
idUsuario,
/* abertoEntradas */
idUsuario,
/* quantidadeAbertoEntradas */
idUsuario,
/* abertoSaidas */
idUsuario,
/* quantidadeAbertoSaidas */
idUsuario,
/* abertoInvestimentos */
idUsuario,
/* quantidadeAbertoInvestimentos */
idUsuario,
/* baixado */
idUsuario,
idUsuario,
idUsuario,
idUsuario,
/* usuário na origem ou destino */
idUsuario,
idUsuario,
/* período */
dataInicio,
dataFim,
]
);
const row = rows[0] || {};
@ -267,9 +405,14 @@ async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) {
abertoInvestimentos,
aberto: abertoEntradas - abertoSaidas - abertoInvestimentos,
quantidadeAbertoEntradas: normalizarNumero(row.quantidadeAbertoEntradas),
quantidadeAbertoSaidas: normalizarNumero(row.quantidadeAbertoSaidas),
quantidadeAbertoInvestimentos: normalizarNumero(row.quantidadeAbertoInvestimentos),
quantidadeAbertoEntradas:
normalizarNumero(row.quantidadeAbertoEntradas),
quantidadeAbertoSaidas:
normalizarNumero(row.quantidadeAbertoSaidas),
quantidadeAbertoInvestimentos:
normalizarNumero(row.quantidadeAbertoInvestimentos),
baixado: normalizarNumero(row.baixado),
quantidadeMovimentos: normalizarNumero(row.quantidadeMovimentos),
@ -430,7 +573,8 @@ async function buscarAlertasVencimento(idUsuario) {
}
async function buscarProximosVencimentos(idUsuario) {
const [rows] = await pool.query(`
const [rows] = await pool.query(
`
SELECT
cp.idcontasapagar,
cp.movimento,
@ -443,22 +587,40 @@ async function buscarProximosVencimentos(idUsuario) {
cp.parcela,
cp.parcelas,
cp.idbancos,
bc.descricao AS banco_descricao,
bc.debito AS banco_debito,
banco_origem.descricao AS banco_descricao,
banco_origem.debito AS banco_debito,
cp.idcentrodecustos,
cc.descricao AS centro_custo_descricao,
DATEDIFF(DATE(cp.datavencimento), CURDATE()) AS dias_para_vencer
DATEDIFF(
DATE(cp.datavencimento),
CURDATE()
) AS dias_para_vencer
FROM contasapagar cp
INNER JOIN bancos bc ON bc.idbancos = cp.idbancos
LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos
INNER JOIN bancos banco_origem
ON banco_origem.idbancos = cp.idbancos
LEFT JOIN centrodecustos cc
ON cc.idcentrodecustos = cp.idcentrodecustos
WHERE cp.deleted_at IS NULL
AND bc.idusuarios = ?
AND banco_origem.idusuarios = ?
AND cp.status IN ('A pagar', 'A receber')
AND cp.movimento IN ('Entrada', 'Estorno', 'Saida')
AND cp.datavencimento IS NOT NULL
ORDER BY DATE(cp.datavencimento) ASC, cp.idcontasapagar ASC
ORDER BY
DATE(cp.datavencimento) ASC,
cp.idcontasapagar ASC
LIMIT 10
`,
[idUsuario]);
[idUsuario]
);
return rows;
}
@ -626,7 +788,8 @@ async function buscarCarteiras(idUsuario, dataInicio, dataFim) {
}
async function buscarUltimosMovimentos(idUsuario) {
const [rows] = await pool.query(`
const [rows] = await pool.query(
`
SELECT
cp.idcontasapagar,
cp.movimento,
@ -643,87 +806,362 @@ async function buscarUltimosMovimentos(idUsuario) {
cp.saldo_processado,
cp.insert_date,
cp.update_date,
cp.idbancos,
bc.descricao AS banco_descricao,
bc.debito AS banco_debito,
cp.idbancos_p,
banco_origem.descricao AS banco_descricao,
banco_origem.descricao AS banco_origem_descricao,
banco_origem.debito AS banco_debito,
banco_origem.idusuarios AS idusuario_origem,
usuario_origem.nome AS usuario_origem_nome,
banco_destino.descricao AS banco_destino_descricao,
banco_destino.debito AS banco_destino_debito,
banco_destino.idusuarios AS idusuario_destino,
usuario_destino.nome AS usuario_destino_nome,
cp.idcentrodecustos,
cc.descricao AS centro_custo_descricao
cc.descricao AS centro_custo_descricao,
CASE
/*
* Movimento normal:
* mantém o tipo original.
*/
WHEN cp.movimento <> 'Sangria'
THEN cp.movimento
/*
* Origem e destino pertencem ao usuário:
* transferência interna.
*/
WHEN banco_origem.idusuarios = ?
AND banco_destino.idusuarios = ?
THEN 'TransferenciaInterna'
/*
* Usuário é o dono do banco de origem:
* dinheiro saiu para outro usuário.
*/
WHEN banco_origem.idusuarios = ?
THEN 'Saida'
/*
* Usuário é o dono do banco de destino:
* dinheiro entrou vindo de outro usuário.
*/
WHEN banco_destino.idusuarios = ?
THEN 'Entrada'
ELSE cp.movimento
END AS movimento_dashboard,
CASE
WHEN cp.movimento <> 'Sangria'
AND cp.movimento IN ('Entrada', 'Estorno')
THEN 'entrada'
WHEN cp.movimento <> 'Sangria'
AND cp.movimento = 'Saida'
THEN 'saida'
WHEN cp.movimento = 'Sangria'
AND banco_origem.idusuarios = ?
AND banco_destino.idusuarios = ?
THEN 'transferencia_interna'
WHEN cp.movimento = 'Sangria'
AND banco_origem.idusuarios = ?
THEN 'saida'
WHEN cp.movimento = 'Sangria'
AND banco_destino.idusuarios = ?
THEN 'entrada'
ELSE 'neutro'
END AS direcao_dashboard,
CASE
WHEN cp.movimento = 'Sangria'
AND banco_origem.idusuarios = ?
AND banco_destino.idusuarios = ?
THEN 1
ELSE 0
END AS transferencia_interna
FROM contasapagar cp
INNER JOIN bancos bc ON bc.idbancos = cp.idbancos
LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos
INNER JOIN bancos banco_origem
ON banco_origem.idbancos = cp.idbancos
LEFT JOIN bancos banco_destino
ON banco_destino.idbancos = cp.idbancos_p
LEFT JOIN usuarios usuario_origem
ON usuario_origem.idusuarios = banco_origem.idusuarios
LEFT JOIN usuarios usuario_destino
ON usuario_destino.idusuarios = banco_destino.idusuarios
LEFT JOIN centrodecustos cc
ON cc.idcentrodecustos = cp.idcentrodecustos
WHERE cp.deleted_at IS NULL
AND bc.idusuarios = ?
ORDER BY cp.insert_date DESC, cp.idcontasapagar DESC
AND (
banco_origem.idusuarios = ?
OR banco_destino.idusuarios = ?
)
ORDER BY
cp.insert_date DESC,
cp.idcontasapagar DESC
LIMIT 10
`,
[idUsuario]);
[
/* movimento_dashboard */
idUsuario,
idUsuario,
idUsuario,
idUsuario,
return rows;
/* direcao_dashboard */
idUsuario,
idUsuario,
idUsuario,
idUsuario,
/* transferencia_interna */
idUsuario,
idUsuario,
/* WHERE */
idUsuario,
idUsuario,
]
);
return rows.map((item) => ({
...item,
valor: normalizarNumero(item.valor),
transferencia_interna:
Number(item.transferencia_interna || 0) === 1,
}));
}
async function buscarGraficos(idUsuario, dataInicio, dataFim) {
/*
* Gastos por centro de custo
*
* Aqui entram:
* - Saídas normais do usuário;
* - Sangrias enviadas para outro usuário;
*
* Não entram:
* - Sangrias entre bancos do próprio usuário;
* - Sangrias recebidas;
* - Movimentos classificados como investimento.
*/
const [centrosRows] = await pool.query(
`
SELECT
cp.idcentrodecustos,
COALESCE(cc.descricao, 'Sem centro de custo') AS descricao,
COALESCE(SUM(cp.valor), 0) AS valor
FROM contasapagar cp
INNER JOIN bancos banco_origem
ON banco_origem.idbancos = cp.idbancos
LEFT JOIN bancos banco_destino
ON banco_destino.idbancos = cp.idbancos_p
LEFT JOIN centrodecustos cc
ON cc.idcentrodecustos = cp.idcentrodecustos
LEFT JOIN bancos b
ON b.idbancos = cp.idbancos
WHERE cp.deleted_at IS NULL
AND cp.movimento IN ('Saida', 'Sangria')
AND DATE(COALESCE(cp.dataentrada, cp.insert_date))
BETWEEN ? AND ?
AND COALESCE(cc.investimento, 0) = 0
AND DATE(cp.datavencimento) BETWEEN ? AND ?
AND b.idusuarios = ?
AND (
(
cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
)
OR
(
cp.movimento = 'Sangria'
AND banco_origem.idusuarios = ?
AND (
banco_destino.idusuarios IS NULL
OR banco_destino.idusuarios <> ?
)
)
)
GROUP BY
cp.idcentrodecustos,
COALESCE(cc.descricao, 'Sem centro de custo')
HAVING valor > 0
ORDER BY valor DESC
LIMIT 8
`,
[dataInicio, dataFim, idUsuario]
[
dataInicio,
dataFim,
idUsuario,
idUsuario,
idUsuario,
]
);
/*
* Resumo de entradas, saídas e investimentos.
*
* Para sangria:
* - destino é do usuário e origem é de outro usuário = entrada;
* - origem é do usuário e destino é de outro usuário = saída;
* - origem e destino são do usuário = transferência interna, ignorada.
*/
const [entradasSaidasRows] = await pool.query(
`
SELECT
COALESCE(SUM(CASE
WHEN cp.movimento IN ('Entrada', 'Estorno') THEN cp.valor
ELSE 0
END), 0) AS entradas,
COALESCE(
SUM(
CASE
/* Entrada ou estorno normal */
WHEN cp.movimento IN ('Entrada', 'Estorno')
AND banco_origem.idusuarios = ?
THEN cp.valor
COALESCE(SUM(CASE
WHEN cp.movimento IN ('Saida', 'Sangria')
/* Sangria recebida de outro usuário */
WHEN cp.movimento = 'Sangria'
AND banco_destino.idusuarios = ?
AND banco_origem.idusuarios <> ?
THEN cp.valor
ELSE 0
END
),
0
) AS entradas,
COALESCE(
SUM(
CASE
/* Saída normal */
WHEN cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 0
THEN cp.valor
ELSE 0
END), 0) AS saidas,
COALESCE(SUM(CASE
WHEN cp.movimento IN ('Saida', 'Sangria')
/* Sangria enviada para outro usuário */
WHEN cp.movimento = 'Sangria'
AND banco_origem.idusuarios = ?
AND (
banco_destino.idusuarios IS NULL
OR banco_destino.idusuarios <> ?
)
AND COALESCE(cc.investimento, 0) = 0
THEN cp.valor
ELSE 0
END
),
0
) AS saidas,
COALESCE(
SUM(
CASE
WHEN cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 1
THEN cp.valor
WHEN cp.movimento = 'Sangria'
AND banco_origem.idusuarios = ?
AND banco_destino.idusuarios = ?
AND COALESCE(banco_destino.investimento, 0) = 1
THEN cp.valor
ELSE 0
END), 0) AS investimentos
END
),
0
) AS investimentos
FROM contasapagar cp
INNER JOIN bancos banco_origem
ON banco_origem.idbancos = cp.idbancos
LEFT JOIN bancos banco_destino
ON banco_destino.idbancos = cp.idbancos_p
LEFT JOIN centrodecustos cc
ON cc.idcentrodecustos = cp.idcentrodecustos
LEFT JOIN bancos b
ON b.idbancos = cp.idbancos
WHERE cp.deleted_at IS NULL
AND DATE(cp.datavencimento) BETWEEN ? AND ?
AND b.idusuarios = ?
AND DATE(COALESCE(cp.dataentrada, cp.insert_date))
BETWEEN ? AND ?
AND (
banco_origem.idusuarios = ?
OR banco_destino.idusuarios = ?
)
`,
[dataInicio, dataFim, idUsuario]
[
/* Entradas */
idUsuario,
idUsuario,
idUsuario,
/* Saídas */
idUsuario,
idUsuario,
idUsuario,
/* Investimentos */
idUsuario,
idUsuario,
idUsuario,
/* Período */
dataInicio,
dataFim,
/* Usuário presente em uma das pontas */
idUsuario,
idUsuario,
]
);
const entradas = normalizarNumero(
entradasSaidasRows[0]?.entradas
);
const saidas = normalizarNumero(
entradasSaidasRows[0]?.saidas
);
const investimentos = normalizarNumero(
entradasSaidasRows[0]?.investimentos
);
const entradas = normalizarNumero(entradasSaidasRows[0]?.entradas);
const saidas = normalizarNumero(entradasSaidasRows[0]?.saidas);
const investimentos = normalizarNumero(entradasSaidasRows[0]?.investimentos);
const resultado = entradas - saidas - investimentos;
return {
@ -741,8 +1179,14 @@ async function buscarGraficos(idUsuario, dataInicio, dataFim) {
percentualEntradas: entradas > 0 ? 100 : 0,
percentualSaidas: calcularPercentual(saidas, entradas),
percentualInvestimentos: calcularPercentual(investimentos, entradas),
percentualResultado: calcularPercentual(resultado, entradas),
percentualInvestimentos: calcularPercentual(
investimentos,
entradas
),
percentualResultado: calcularPercentual(
resultado,
entradas
),
},
};
}

View File

@ -72,7 +72,14 @@ function formatarData(data: string | null | undefined) {
}
function movimentoColor(movimento: string) {
if (movimento === 'Entrada' || movimento === 'Estorno') return 'success.main';
if (movimento === 'Entrada' || movimento === 'Estorno') {
return 'success.main';
}
if (movimento === 'TransferenciaInterna') {
return 'info.main';
}
return 'error.main';
}
@ -95,7 +102,15 @@ function isMovimentoEntrada(movimento: string) {
}
function movimentoBorderColor(movimento: string) {
return isMovimentoEntrada(movimento) ? 'success.main' : 'error.main';
if (isMovimentoEntrada(movimento)) {
return 'success.main';
}
if (movimento === 'TransferenciaInterna') {
return 'info.main';
}
return 'error.main';
}
function movimentoActionLabel(item: DashboardMovimentoResumo) {
@ -213,7 +228,15 @@ function StatCard({
}
function MovimentoLinha({ item }: { item: DashboardMovimentoResumo }) {
const isEntrada = isMovimentoEntrada(item.movimento);
const movimentoVisual =
item.movimento_dashboard || item.movimento;
const isTransferenciaInterna =
movimentoVisual === 'TransferenciaInterna';
const isEntrada =
movimentoVisual === 'Entrada'
|| movimentoVisual === 'Estorno';
return (
<Box
@ -224,8 +247,10 @@ function MovimentoLinha({ item }: { item: DashboardMovimentoResumo }) {
border: '1px solid',
borderColor: 'divider',
borderLeft: '6px solid',
borderLeftColor: movimentoBorderColor(item.movimento),
background: isEntrada
borderLeftColor: movimentoBorderColor(movimentoVisual),
background: isTransferenciaInterna
? 'linear-gradient(90deg, rgba(2,136,209,0.08) 0%, rgba(255,255,255,0.9) 42%)'
: isEntrada
? 'linear-gradient(90deg, rgba(46,125,50,0.08) 0%, rgba(255,255,255,0.9) 42%)'
: 'linear-gradient(90deg, rgba(211,47,47,0.08) 0%, rgba(255,255,255,0.9) 42%)',
}}
@ -241,9 +266,19 @@ function MovimentoLinha({ item }: { item: DashboardMovimentoResumo }) {
</Typography>
<Chip
label={item.movimento}
label={
isTransferenciaInterna
? 'Transferência interna'
: movimentoVisual
}
color={
isTransferenciaInterna
? 'info'
: isEntrada
? 'success'
: 'error'
}
size="small"
color={isEntrada ? 'success' : 'error'}
variant="outlined"
/>
</Stack>
@ -276,7 +311,7 @@ function MovimentoLinha({ item }: { item: DashboardMovimentoResumo }) {
spacing={0.75}
flexShrink={0}
>
<Typography fontWeight={950} color={movimentoColor(item.movimento)}>
<Typography fontWeight={950} color={movimentoColor(movimentoVisual)}>
{formatarValor(item.valor)}
</Typography>
@ -292,7 +327,13 @@ function MovimentoLinha({ item }: { item: DashboardMovimentoResumo }) {
to={`/movimentos/${item.idcontasapagar}/editar`}
size="small"
variant="contained"
color={isEntrada ? 'success' : 'error'}
color={
isTransferenciaInterna
? 'info'
: isEntrada
? 'success'
: 'error'
}
sx={{ borderRadius: 2, boxShadow: 0 }}
>
{movimentoActionLabel(item)}
@ -833,7 +874,7 @@ export function DashboardPage() {
<StatCard
title="Investimentos"
value={formatarValor(cards.investimentos)}
subtitle="Sangrias marcadas como investimento"
subtitle="Valores destinados a investimentos"
icon={<AccountBalanceWalletIcon />}
color="info"
percent={cards.percentualInvestimentos}

View File

@ -70,6 +70,9 @@ export type DashboardMovimentoResumo = {
idcentrodecustos?: number | null;
centro_custo_descricao?: string | null;
dias_para_vencer?: number;
movimento_dashboard?: string;
direcao_dashboard?: string;
transferencia_interna?: boolean;
};
export type DashboardCredito = {

View File

@ -144,10 +144,57 @@ function campoDataLabel(campo: DataCampo) {
return labels[campo];
}
function getMovimentoVisual(item: Movimento) {
return item.movimento_dashboard || item.movimento;
}
function movimentoEhEntrada(item: Movimento) {
return item.direcao_dashboard
? item.direcao_dashboard === 'entrada'
: item.movimento === 'Entrada'
|| item.movimento === 'Estorno';
}
function movimentoEhSaida(item: Movimento) {
return item.direcao_dashboard
? item.direcao_dashboard === 'saida'
: item.movimento === 'Saida';
}
function movimentoEhTransferenciaInterna(item: Movimento) {
return item.transferencia_interna === true
|| item.direcao_dashboard === 'transferencia_interna'
|| getMovimentoVisual(item) === 'TransferenciaInterna';
}
function movimentoEhInvestimento(item: Movimento) {
return item.investimento_dashboard === true;
}
function movimentoChipColor(movimento: string) {
if (movimento === 'Entrada') return 'success';
if (movimento === 'Saida') return 'error';
if (movimento === 'Sangria') return 'warning';
if (movimento === 'Entrada' || movimento === 'Estorno') {
return 'success';
}
if (movimento === 'Saida' || movimento === 'Saída') {
return 'error';
}
if (
movimento === 'TransferenciaInterna'
|| movimento === 'Transferência interna'
) {
return 'info';
}
if (movimento === 'Investimento') {
return 'primary';
}
if (movimento === 'Sangria') {
return 'warning';
}
return 'default';
}
@ -264,7 +311,7 @@ function getMovimentoRowVisual(item: Movimento) {
};
}
function getMovimentoIconVisual(movimento: MovimentoTipo) {
function getMovimentoIconVisual(movimento: string) {
if (movimento === 'Entrada') {
return {
icon: <ArrowDownwardIcon fontSize="small" />,
@ -273,7 +320,7 @@ function getMovimentoIconVisual(movimento: MovimentoTipo) {
};
}
if (movimento === 'Saida') {
if (movimento === 'Saida' || movimento === 'Saída') {
return {
icon: <ArrowUpwardIcon fontSize="small" />,
label: 'Saída',
@ -281,6 +328,25 @@ function getMovimentoIconVisual(movimento: MovimentoTipo) {
};
}
if (
movimento === 'TransferenciaInterna'
|| movimento === 'Transferência interna'
) {
return {
icon: <SwapHorizIcon fontSize="small" />,
label: 'Transferência interna',
color: 'info' as const,
};
}
if (movimento === 'Investimento') {
return {
icon: <AccountBalanceIcon fontSize="small" />,
label: 'Investimento',
color: 'primary' as const,
};
}
if (movimento === 'Sangria') {
return {
icon: <SwapHorizIcon fontSize="small" />,
@ -304,6 +370,24 @@ function getMovimentoIconVisual(movimento: MovimentoTipo) {
};
}
function movimentoVisualLabel(item: Movimento) {
if (movimentoEhInvestimento(item)) {
return 'Investimento';
}
const movimento = getMovimentoVisual(item);
if (movimento === 'TransferenciaInterna') {
return 'Transferência interna';
}
if (movimento === 'Saida') {
return 'Saída';
}
return movimento;
}
function formatarDataGrupo(dataISO: string) {
if (!dataISO || dataISO === 'sem-data') return 'Sem data';
@ -329,13 +413,21 @@ function getDataGrupo(item: Movimento, campo: DataCampo) {
function valorAssinadoMovimento(item: Movimento) {
const valor = Number(item.valor || 0);
if (item.movimento === 'Entrada' || item.movimento === 'Estorno') {
if (movimentoEhTransferenciaInterna(item)) {
return 0;
}
if (movimentoEhEntrada(item)) {
return valor;
}
if (movimentoEhSaida(item) || movimentoEhInvestimento(item)) {
return valor * -1;
}
return 0;
}
function criarResumoGrupo() {
return {
totalEntradas: 0,
@ -345,15 +437,15 @@ function criarResumoGrupo() {
};
}
function aplicarMovimentoNoResumo<T extends ReturnType<typeof criarResumoGrupo>>(
resumo: T,
item: Movimento
) {
function aplicarMovimentoNoResumo<T extends ReturnType<typeof criarResumoGrupo>>(resumo: T, item: Movimento) {
const valorAssinado = valorAssinadoMovimento(item);
if (valorAssinado >= 0) {
resumo.totalEntradas += valorAssinado;
} else {
if (movimentoEhEntrada(item)) {
resumo.totalEntradas += Math.abs(valorAssinado);
} else if (
movimentoEhSaida(item)
|| movimentoEhInvestimento(item)
) {
resumo.totalSaidas += Math.abs(valorAssinado);
}
@ -377,10 +469,24 @@ function agruparMovimentos(movimentos: Movimento[], campo: DataCampo): GrupoDiaM
for (const item of movimentos) {
const chaveDia = getDataGrupo(item, campo);
const bancoNome = item.banco_descricao || 'Sem banco';
const bancoNome =
item.direcao_dashboard === 'entrada'
&& item.movimento === 'Sangria'
? item.banco_referencia_descricao
|| item.banco_descricao
|| 'Sem banco'
: item.banco_descricao
|| 'Sem banco';
const centroNome = item.centro_custo_descricao || 'Sem centro de custo';
const chaveBanco = String(item.idbancos || bancoNome);
const idBancoVisual =
item.direcao_dashboard === 'entrada'
&& item.movimento === 'Sangria'
? item.idbancos_p
: item.idbancos;
const chaveBanco =
String(idBancoVisual || bancoNome);
const chaveCentro = String(item.idcentrodecustos || centroNome);
if (!gruposDia.has(chaveDia)) {
@ -473,17 +579,41 @@ export function MovimentosPage() {
page: 1,
totalPages: 1,
});
const [summary, setSummary] = useState<MovimentosSummary>({
const SUMMARY_INICIAL: MovimentosSummary = {
quantidade: 0,
totalEntradas: 0,
entradasRecebidas: 0,
entradasPendentes: 0,
quantidadeEntradasRecebidas: 0,
quantidadeEntradasPendentes: 0,
percentualEntradasRecebidas: 0,
percentualEntradasPendentes: 0,
totalSaidas: 0,
totalSangrias: 0,
saidasPagas: 0,
saidasPendentes: 0,
quantidadeSaidasPagas: 0,
quantidadeSaidasPendentes: 0,
percentualSaidasPagas: 0,
percentualSaidasPendentes: 0,
totalInvestimentos: 0,
investimentosRealizados: 0,
investimentosPendentes: 0,
quantidadeInvestimentosRealizados: 0,
quantidadeInvestimentosPendentes: 0,
percentualInvestimentosRealizados: 0,
percentualInvestimentosPendentes: 0,
totalEstornos: 0,
totalBaixado: 0,
totalAberto: 0,
saldoProcessado: 0,
saldo: 0,
});
};
const [summary, setSummary] =
useState<MovimentosSummary>(SUMMARY_INICIAL);
const [modoVisualizacao, setModoVisualizacao] = useState<ModoVisualizacao>('tabela');
const [gruposAbertos, setGruposAbertos] = useState<Record<string, boolean>>({});
@ -968,7 +1098,8 @@ export function MovimentosPage() {
<Stack spacing={0.75} padding={1.25} paddingTop={0}>
{centro.movimentos.map((item) => {
const rowVisual = getMovimentoRowVisual(item);
const movimentoIcon = getMovimentoIconVisual(item.movimento);
const movimentoVisual = movimentoVisualLabel(item);
const movimentoIcon = getMovimentoIconVisual(movimentoVisual);
const dataPrincipal = getDataPrincipal(item, dataCampo);
return (
@ -1046,13 +1177,17 @@ export function MovimentosPage() {
<Typography
fontWeight={950}
color={
item.movimento === 'Entrada' || item.movimento === 'Estorno'
movimentoEhTransferenciaInterna(item)
? 'info.main'
: movimentoEhEntrada(item)
? 'success.main'
: 'error.main'
}
whiteSpace="nowrap"
>
{item.movimento === 'Entrada' || item.movimento === 'Estorno'
{movimentoEhTransferenciaInterna(item)
? ''
: movimentoEhEntrada(item)
? '+'
: '-'}
{formatarValor(item.valor)}
@ -1094,6 +1229,133 @@ export function MovimentosPage() {
);
}
function CardResumoFinanceiro({
titulo,
total,
realizadoLabel,
realizado,
realizadoQuantidade,
realizadoPercentual,
pendenteLabel,
pendente,
pendenteQuantidade,
pendentePercentual,
corTotal,
}: {
titulo: string;
total: number;
realizadoLabel: string;
realizado: number;
realizadoQuantidade: number;
realizadoPercentual: number;
pendenteLabel: string;
pendente: number;
pendenteQuantidade: number;
pendentePercentual: number;
corTotal: 'success.main' | 'error.main' | 'primary.main';
}) {
return (
<Card>
<CardContent>
<Typography
variant="body2"
color="text.secondary"
>
{titulo}
</Typography>
<Typography
variant="h5"
fontWeight={950}
color={corTotal}
marginTop={0.25}
>
{formatarValor(total)}
</Typography>
<Divider sx={{ marginY: 1.25 }} />
<Stack spacing={0.75}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={1}
>
<Box>
<Typography
variant="caption"
color="text.secondary"
>
{realizadoLabel}
</Typography>
<Typography
variant="body2"
fontWeight={850}
>
{formatarValor(realizado)}
</Typography>
</Box>
<Chip
size="small"
color="success"
variant="outlined"
label={`${realizadoPercentual.toFixed(1)}%`}
/>
</Stack>
<Typography
variant="caption"
color="text.secondary"
>
{realizadoQuantidade} movimento(s)
</Typography>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={1}
marginTop={0.5}
>
<Box>
<Typography
variant="caption"
color="text.secondary"
>
{pendenteLabel}
</Typography>
<Typography
variant="body2"
fontWeight={850}
>
{formatarValor(pendente)}
</Typography>
</Box>
<Chip
size="small"
color="warning"
variant="outlined"
label={`${pendentePercentual.toFixed(1)}%`}
/>
</Stack>
<Typography
variant="caption"
color="text.secondary"
>
{pendenteQuantidade} movimento(s)
</Typography>
</Stack>
</CardContent>
</Card>
);
}
return (
<Box>
<Stack
@ -1153,77 +1415,132 @@ export function MovimentosPage() {
display="grid"
gridTemplateColumns={{
xs: '1fr',
md: 'repeat(6, 1fr)',
md: 'repeat(2, minmax(0, 1fr))',
xl: 'repeat(5, minmax(0, 1fr))',
}}
gap={{ xs: 2, md: 2.5 }}
marginBottom={{ xs: 2.5, md: 3 }}
>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
<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'}
>
{summary.quantidade}
</Typography>
<Typography
variant="caption"
color="text.secondary"
>
Movimentos financeiros filtrados
</Typography>
</CardContent>
</Card>
<CardResumoFinanceiro
titulo="Entradas"
total={summary.totalEntradas}
realizadoLabel="Recebido"
realizado={summary.entradasRecebidas}
realizadoQuantidade={
summary.quantidadeEntradasRecebidas
}
realizadoPercentual={
summary.percentualEntradasRecebidas
}
pendenteLabel="A receber"
pendente={summary.entradasPendentes}
pendenteQuantidade={
summary.quantidadeEntradasPendentes
}
pendentePercentual={
summary.percentualEntradasPendentes
}
corTotal="success.main"
/>
<CardResumoFinanceiro
titulo="Saídas"
total={summary.totalSaidas}
realizadoLabel="Pago"
realizado={summary.saidasPagas}
realizadoQuantidade={
summary.quantidadeSaidasPagas
}
realizadoPercentual={
summary.percentualSaidasPagas
}
pendenteLabel="A pagar"
pendente={summary.saidasPendentes}
pendenteQuantidade={
summary.quantidadeSaidasPendentes
}
pendentePercentual={
summary.percentualSaidasPendentes
}
corTotal="error.main"
/>
<CardResumoFinanceiro
titulo="Investimentos"
total={summary.totalInvestimentos}
realizadoLabel="Realizado"
realizado={summary.investimentosRealizados}
realizadoQuantidade={
summary.quantidadeInvestimentosRealizados
}
realizadoPercentual={
summary.percentualInvestimentosRealizados
}
pendenteLabel="Pendente"
pendente={summary.investimentosPendentes}
pendenteQuantidade={
summary.quantidadeInvestimentosPendentes
}
pendentePercentual={
summary.percentualInvestimentosPendentes
}
corTotal="primary.main"
/>
<Card>
<CardContent>
<Typography
variant="body2"
color="text.secondary"
>
Saldo filtrado
</Typography>
<Typography
variant="h5"
fontWeight={950}
color={
summary.saldo >= 0
? 'success.main'
: 'error.main'
}
marginTop={0.25}
>
{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>
<Divider sx={{ marginY: 1.25 }} />
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">
Baixados
</Typography>
<Typography variant="h5" fontWeight={950} color="info.main">
{formatarValor(summary.totalBaixado)}
<Typography
variant="caption"
color="text.secondary"
>
Entradas menos saídas e investimentos
</Typography>
</CardContent>
</Card>
@ -1553,7 +1870,8 @@ export function MovimentosPage() {
{movimentos.map((item) => {
const dataPrincipal = getDataPrincipal(item, dataCampo);
const rowVisual = getMovimentoRowVisual(item);
const movimentoIcon = getMovimentoIconVisual(item.movimento);
const movimentoVisual = movimentoVisualLabel(item);
const movimentoIcon = getMovimentoIconVisual(movimentoVisual);
return (
<Box
@ -1609,8 +1927,13 @@ export function MovimentosPage() {
<Typography
fontWeight={950}
color={
item.movimento === 'Entrada' || item.movimento === 'Estorno'
movimentoEhTransferenciaInterna(item)
? 'info.main'
: movimentoEhEntrada(item)
? 'success.main'
: movimentoEhSaida(item)
|| movimentoEhInvestimento(item)
? 'error.main'
: 'text.primary'
}
whiteSpace="nowrap"
@ -1621,9 +1944,11 @@ export function MovimentosPage() {
<Stack direction="row" flexWrap="wrap" spacing={1} useFlexGap>
<Chip
label={item.movimento}
label={movimentoVisual}
size="small"
color={movimentoChipColor(item.movimento) as any}
color={
movimentoChipColor(movimentoVisual) as any
}
/>
<Chip
@ -1749,7 +2074,8 @@ export function MovimentosPage() {
{movimentos.map((item) => {
const dataPrincipal = getDataPrincipal(item, dataCampo);
const rowVisual = getMovimentoRowVisual(item);
const movimentoIcon = getMovimentoIconVisual(item.movimento);
const movimentoVisual = movimentoVisualLabel(item);
const movimentoIcon = getMovimentoIconVisual(movimentoVisual);
return (
<TableRow
@ -1825,8 +2151,13 @@ export function MovimentosPage() {
<Typography
fontWeight={950}
color={
item.movimento === 'Entrada' || item.movimento === 'Estorno'
movimentoEhTransferenciaInterna(item)
? 'info.main'
: movimentoEhEntrada(item)
? 'success.main'
: movimentoEhSaida(item)
|| movimentoEhInvestimento(item)
? 'error.main'
: 'text.primary'
}
>
@ -1853,9 +2184,11 @@ export function MovimentosPage() {
<TableCell>
<Chip
label={item.movimento}
label={movimentoVisual}
size="small"
color={movimentoChipColor(item.movimento) as any}
color={
movimentoChipColor(movimentoVisual) as any
}
/>
</TableCell>

View File

@ -43,6 +43,15 @@ export type Movimento = {
banco_referencia_debito?: number | null;
movimento_fixo_descricao?: string | null;
ocorrencia_extrato?: 'lancamento' | 'baixa' | null;
movimento_dashboard?: string;
direcao_dashboard?: 'entrada' | 'saida' | 'transferencia_interna' | 'neutro';
transferencia_interna?: boolean;
investimento_dashboard?: boolean;
idusuario_origem?: number | null;
idusuario_destino?: number | null;
banco_destino_investimento?: number | null;
};
export type CriarMovimentoRequest = {
@ -115,14 +124,41 @@ export type MovimentosPagination = {
export type MovimentosSummary = {
quantidade: number;
totalEntradas: number;
entradasRecebidas: number;
entradasPendentes: number;
quantidadeEntradasRecebidas: number;
quantidadeEntradasPendentes: number;
percentualEntradasRecebidas: number;
percentualEntradasPendentes: number;
totalSaidas: number;
totalSangrias: number;
saidasPagas: number;
saidasPendentes: number;
quantidadeSaidasPagas: number;
quantidadeSaidasPendentes: number;
percentualSaidasPagas: number;
percentualSaidasPendentes: number;
totalInvestimentos: number;
investimentosRealizados: number;
investimentosPendentes: number;
quantidadeInvestimentosRealizados: number;
quantidadeInvestimentosPendentes: number;
percentualInvestimentosRealizados: number;
percentualInvestimentosPendentes: number;
totalEstornos: number;
totalBaixado: number;
totalAberto: number;
saldoProcessado: number;
saldo: number;
/*
* Compatibilidade temporária com versões antigas da API.
*/
totalSangrias?: number;
totalBaixado?: number;
totalAberto?: number;
};
export type ApiResponse<T> = {