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')
AND COALESCE(cc.investimento, 0) = 0
THEN cp.valor
ELSE 0
END), 0) AS saidas,
/* 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
COALESCE(SUM(CASE
WHEN cp.movimento IN ('Saida', 'Sangria')
AND COALESCE(cc.investimento, 0) = 1
THEN cp.valor
ELSE 0
END), 0) AS investimentos,
ELSE 0
END
), 0) AS entradas,
COALESCE(SUM(CASE
WHEN cp.status = 'A receber'
AND cp.movimento IN ('Entrada', 'Estorno')
THEN cp.valor
ELSE 0
END), 0) AS abertoEntradas,
COALESCE(SUM(
CASE
/* Saída operacional normal */
WHEN cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 0
THEN cp.valor
COUNT(CASE
WHEN cp.status = 'A receber'
AND cp.movimento IN ('Entrada', 'Estorno')
THEN 1
ELSE NULL
END) AS quantidadeAbertoEntradas,
/* 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
COALESCE(SUM(CASE
WHEN cp.status = 'A pagar'
AND cp.movimento IN ('Saida', 'Sangria')
AND COALESCE(cc.investimento, 0) = 0
THEN cp.valor
ELSE 0
END), 0) AS abertoSaidas,
ELSE 0
END
), 0) AS saidas,
COUNT(CASE
WHEN cp.status = 'A pagar'
AND cp.movimento IN ('Saida', 'Sangria')
AND COALESCE(cc.investimento, 0) = 0
THEN 1
ELSE NULL
END) AS quantidadeAbertoSaidas,
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
COALESCE(SUM(CASE
WHEN cp.status = 'A pagar'
AND cp.movimento IN ('Saida', 'Sangria')
AND COALESCE(cc.investimento, 0) = 1
THEN cp.valor
ELSE 0
END), 0) AS abertoInvestimentos,
/* 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
COUNT(CASE
WHEN cp.status = 'A pagar'
AND cp.movimento IN ('Saida', 'Sangria')
AND COALESCE(cc.investimento, 0) = 1
THEN 1
ELSE NULL
END) AS quantidadeAbertoInvestimentos,
ELSE 0
END
), 0) AS investimentos,
COALESCE(SUM(CASE
WHEN cp.status IN ('Pago', 'Recebido') THEN cp.valor
ELSE 0
END), 0) AS baixado,
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,
COUNT(
CASE
WHEN cp.status = 'A receber'
AND cp.movimento IN ('Entrada', 'Estorno')
AND banco_origem.idusuarios = ?
THEN 1
ELSE NULL
END
) AS quantidadeAbertoEntradas,
COALESCE(SUM(
CASE
WHEN cp.status = 'A pagar'
AND cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 0
THEN cp.valor
ELSE 0
END
), 0) AS abertoSaidas,
COUNT(
CASE
WHEN cp.status = 'A pagar'
AND cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 0
THEN 1
ELSE NULL
END
) AS quantidadeAbertoSaidas,
COALESCE(SUM(
CASE
WHEN cp.status = 'A pagar'
AND cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 1
THEN cp.valor
ELSE 0
END
), 0) AS abertoInvestimentos,
COUNT(
CASE
WHEN cp.status = 'A pagar'
AND cp.movimento = 'Saida'
AND banco_origem.idusuarios = ?
AND COALESCE(cc.investimento, 0) = 1
THEN 1
ELSE NULL
END
) AS quantidadeAbertoInvestimentos,
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,
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,35 +573,54 @@ async function buscarAlertasVencimento(idUsuario) {
}
async function buscarProximosVencimentos(idUsuario) {
const [rows] = await pool.query(`
SELECT
cp.idcontasapagar,
cp.movimento,
cp.descricao,
cp.valor,
cp.status,
cp.dataentrada,
cp.datavencimento,
cp.databaixa,
cp.parcela,
cp.parcelas,
cp.idbancos,
bc.descricao AS banco_descricao,
bc.debito AS banco_debito,
cp.idcentrodecustos,
cc.descricao AS centro_custo_descricao,
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
WHERE cp.deleted_at IS NULL
AND bc.idusuarios = ?
AND cp.status IN ('A pagar', 'A receber')
AND cp.datavencimento IS NOT NULL
ORDER BY DATE(cp.datavencimento) ASC, cp.idcontasapagar ASC
LIMIT 10
`,
[idUsuario]);
const [rows] = await pool.query(
`
SELECT
cp.idcontasapagar,
cp.movimento,
cp.descricao,
cp.valor,
cp.status,
cp.dataentrada,
cp.datavencimento,
cp.databaixa,
cp.parcela,
cp.parcelas,
cp.idbancos,
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
FROM contasapagar cp
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 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
LIMIT 10
`,
[idUsuario]
);
return rows;
}
@ -626,104 +788,380 @@ async function buscarCarteiras(idUsuario, dataInicio, dataFim) {
}
async function buscarUltimosMovimentos(idUsuario) {
const [rows] = await pool.query(`
SELECT
cp.idcontasapagar,
cp.movimento,
cp.descricao,
cp.valor,
cp.status,
cp.dataentrada,
cp.datavencimento,
cp.databaixa,
cp.parcela,
cp.parcelas,
cp.competencia,
cp.origem,
cp.saldo_processado,
cp.insert_date,
cp.update_date,
cp.idbancos,
bc.descricao AS banco_descricao,
bc.debito AS banco_debito,
cp.idcentrodecustos,
cc.descricao AS centro_custo_descricao
FROM contasapagar cp
INNER JOIN bancos bc ON bc.idbancos = cp.idbancos
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
LIMIT 10
`,
[idUsuario]);
const [rows] = await pool.query(
`
SELECT
cp.idcontasapagar,
cp.movimento,
cp.descricao,
cp.valor,
cp.status,
cp.dataentrada,
cp.datavencimento,
cp.databaixa,
cp.parcela,
cp.parcelas,
cp.competencia,
cp.origem,
cp.saldo_processado,
cp.insert_date,
cp.update_date,
return rows;
cp.idbancos,
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,
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 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 (
banco_origem.idusuarios = ?
OR banco_destino.idusuarios = ?
)
ORDER BY
cp.insert_date DESC,
cp.idcontasapagar DESC
LIMIT 10
`,
[
/* movimento_dashboard */
idUsuario,
idUsuario,
idUsuario,
idUsuario,
/* 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
LEFT JOIN centrodecustos cc
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')
AND COALESCE(cc.investimento, 0) = 0
THEN cp.valor
ELSE 0
END), 0) AS saidas,
/* 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
/* 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
COALESCE(SUM(CASE
WHEN cp.movimento IN ('Saida', 'Sangria')
AND COALESCE(cc.investimento, 0) = 1
THEN cp.valor
ELSE 0
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,10 +247,12 @@ function MovimentoLinha({ item }: { item: DashboardMovimentoResumo }) {
border: '1px solid',
borderColor: 'divider',
borderLeft: '6px solid',
borderLeftColor: movimentoBorderColor(item.movimento),
background: 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%)',
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%)',
}}
>
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" spacing={2}>
@ -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,11 +413,19 @@ 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;
}
return valor * -1;
if (movimentoEhSaida(item) || movimentoEhInvestimento(item)) {
return valor * -1;
}
return 0;
}
function criarResumoGrupo() {
@ -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,15 +1177,19 @@ export function MovimentosPage() {
<Typography
fontWeight={950}
color={
item.movimento === 'Entrada' || item.movimento === 'Estorno'
? 'success.main'
: 'error.main'
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)}
</Typography>
@ -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,9 +1927,14 @@ export function MovimentosPage() {
<Typography
fontWeight={950}
color={
item.movimento === 'Entrada' || item.movimento === 'Estorno'
? 'success.main'
: 'text.primary'
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,9 +2151,14 @@ export function MovimentosPage() {
<Typography
fontWeight={950}
color={
item.movimento === 'Entrada' || item.movimento === 'Estorno'
? 'success.main'
: 'text.primary'
movimentoEhTransferenciaInterna(item)
? 'info.main'
: movimentoEhEntrada(item)
? 'success.main'
: movimentoEhSaida(item)
|| movimentoEhInvestimento(item)
? 'error.main'
: 'text.primary'
}
>
{formatarValor(item.valor)}
@ -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> = {