ajustes em simulacoes por usuario
This commit is contained in:
parent
09ef64f491
commit
180d756195
|
|
@ -1,40 +1,72 @@
|
|||
const centrosCustoService = require('../services/centrosCusto.service');
|
||||
|
||||
function obterIdUsuario(req) {
|
||||
return Number(
|
||||
req.user?.idusuarios
|
||||
?? req.user?.idusuario
|
||||
?? req.user?.id
|
||||
);
|
||||
}
|
||||
|
||||
function normalizarFlagEntrada(valor) {
|
||||
if (valor === undefined || valor === null || valor === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (valor === true || valor === 'true') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (valor === false || valor === 'false') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number(valor);
|
||||
}
|
||||
|
||||
function validarDadosCentroCusto(dados) {
|
||||
if (!dados.descricao || !String(dados.descricao).trim()) {
|
||||
return 'Descrição é obrigatória.';
|
||||
}
|
||||
|
||||
if (dados.limite !== undefined && dados.limite !== null && dados.limite !== '') {
|
||||
const simular = normalizarFlagEntrada(dados.simular);
|
||||
|
||||
if (simular !== undefined && ![0, 1].includes(simular)) {
|
||||
return 'Campo simular inválido.';
|
||||
}
|
||||
|
||||
if (simular === 1) {
|
||||
if (dados.limite === undefined || dados.limite === null || dados.limite === '') {
|
||||
return 'Informe o limite para incluir o centro de custo na simulação.';
|
||||
}
|
||||
|
||||
const limite = Number(dados.limite);
|
||||
|
||||
if (!Number.isFinite(limite)) {
|
||||
if (!Number.isFinite(limite) || limite < 0) {
|
||||
return 'Limite inválido.';
|
||||
}
|
||||
} else if (
|
||||
dados.limite !== undefined
|
||||
&& dados.limite !== null
|
||||
&& dados.limite !== ''
|
||||
) {
|
||||
const limite = Number(dados.limite);
|
||||
|
||||
if (!Number.isFinite(limite) || limite < 0) {
|
||||
return 'Limite inválido.';
|
||||
}
|
||||
}
|
||||
|
||||
if (dados.simular !== undefined && dados.simular !== null && dados.simular !== '') {
|
||||
const simular = Number(dados.simular);
|
||||
const investimento = normalizarFlagEntrada(dados.investimento);
|
||||
|
||||
if (![0, 1].includes(simular)) {
|
||||
return 'Campo simular inválido.';
|
||||
}
|
||||
if (investimento !== undefined && ![0, 1].includes(investimento)) {
|
||||
return 'Campo investimento inválido.';
|
||||
}
|
||||
|
||||
if (dados.investimento !== undefined && dados.investimento !== null && dados.investimento !== '') {
|
||||
const investimento = Number(dados.investimento);
|
||||
const habilitado = normalizarFlagEntrada(dados.habilitado);
|
||||
|
||||
if (![0, 1].includes(investimento)) {
|
||||
return 'Campo investimento inválido.';
|
||||
}
|
||||
}
|
||||
|
||||
if (dados.habilitado !== undefined && dados.habilitado !== null && dados.habilitado !== '') {
|
||||
const habilitado = Number(dados.habilitado);
|
||||
|
||||
if (![0, 1].includes(habilitado)) {
|
||||
return 'Status inválido.';
|
||||
}
|
||||
if (habilitado !== undefined && ![0, 1].includes(habilitado)) {
|
||||
return 'Status inválido.';
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -42,7 +74,17 @@ function validarDadosCentroCusto(dados) {
|
|||
|
||||
async function listar(req, res) {
|
||||
try {
|
||||
const idUsuario = obterIdUsuario(req);
|
||||
|
||||
if (!idUsuario) {
|
||||
return res.status(401).json({
|
||||
ok: false,
|
||||
message: 'Usuário não identificado.',
|
||||
});
|
||||
}
|
||||
|
||||
const resultado = await centrosCustoService.listarCentrosCusto({
|
||||
idUsuario,
|
||||
limite: req.query.limite,
|
||||
offset: req.query.offset,
|
||||
page: req.query.page,
|
||||
|
|
@ -73,6 +115,7 @@ async function listar(req, res) {
|
|||
async function buscarPorId(req, res) {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const idUsuario = obterIdUsuario(req);
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({
|
||||
|
|
@ -81,7 +124,17 @@ async function buscarPorId(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
const centroCusto = await centrosCustoService.buscarCentroCustoPorId(id);
|
||||
if (!idUsuario) {
|
||||
return res.status(401).json({
|
||||
ok: false,
|
||||
message: 'Usuário não identificado.',
|
||||
});
|
||||
}
|
||||
|
||||
const centroCusto = await centrosCustoService.buscarCentroCustoPorId(
|
||||
id,
|
||||
idUsuario
|
||||
);
|
||||
|
||||
if (!centroCusto) {
|
||||
return res.status(404).json({
|
||||
|
|
@ -106,6 +159,15 @@ async function buscarPorId(req, res) {
|
|||
|
||||
async function criar(req, res) {
|
||||
try {
|
||||
const idUsuario = obterIdUsuario(req);
|
||||
|
||||
if (!idUsuario) {
|
||||
return res.status(401).json({
|
||||
ok: false,
|
||||
message: 'Usuário não identificado.',
|
||||
});
|
||||
}
|
||||
|
||||
const erroValidacao = validarDadosCentroCusto(req.body);
|
||||
|
||||
if (erroValidacao) {
|
||||
|
|
@ -115,7 +177,10 @@ async function criar(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
const novoCentroCusto = await centrosCustoService.criarCentroCusto(req.body);
|
||||
const novoCentroCusto = await centrosCustoService.criarCentroCusto(
|
||||
req.body,
|
||||
idUsuario
|
||||
);
|
||||
|
||||
return res.status(201).json({
|
||||
ok: true,
|
||||
|
|
@ -135,6 +200,7 @@ async function criar(req, res) {
|
|||
async function atualizar(req, res) {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const idUsuario = obterIdUsuario(req);
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({
|
||||
|
|
@ -143,6 +209,13 @@ async function atualizar(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
if (!idUsuario) {
|
||||
return res.status(401).json({
|
||||
ok: false,
|
||||
message: 'Usuário não identificado.',
|
||||
});
|
||||
}
|
||||
|
||||
const erroValidacao = validarDadosCentroCusto(req.body);
|
||||
|
||||
if (erroValidacao) {
|
||||
|
|
@ -152,7 +225,11 @@ async function atualizar(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
const centroAtualizado = await centrosCustoService.atualizarCentroCusto(id, req.body);
|
||||
const centroAtualizado = await centrosCustoService.atualizarCentroCusto(
|
||||
id,
|
||||
req.body,
|
||||
idUsuario
|
||||
);
|
||||
|
||||
if (!centroAtualizado) {
|
||||
return res.status(404).json({
|
||||
|
|
@ -179,6 +256,7 @@ async function atualizar(req, res) {
|
|||
async function alterarHabilitado(req, res) {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const idUsuario = obterIdUsuario(req);
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({
|
||||
|
|
@ -187,7 +265,14 @@ async function alterarHabilitado(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
const habilitado = Number(req.body.habilitado);
|
||||
if (!idUsuario) {
|
||||
return res.status(401).json({
|
||||
ok: false,
|
||||
message: 'Usuário não identificado.',
|
||||
});
|
||||
}
|
||||
|
||||
const habilitado = normalizarFlagEntrada(req.body.habilitado);
|
||||
|
||||
if (![0, 1].includes(habilitado)) {
|
||||
return res.status(400).json({
|
||||
|
|
@ -196,7 +281,12 @@ async function alterarHabilitado(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
const centroAtualizado = await centrosCustoService.alterarHabilitadoCentroCusto(id, habilitado);
|
||||
const centroAtualizado =
|
||||
await centrosCustoService.alterarHabilitadoCentroCusto(
|
||||
id,
|
||||
habilitado,
|
||||
idUsuario
|
||||
);
|
||||
|
||||
if (!centroAtualizado) {
|
||||
return res.status(404).json({
|
||||
|
|
@ -270,4 +360,4 @@ module.exports = {
|
|||
atualizar,
|
||||
alterarHabilitado,
|
||||
deletar,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@ router.put('/:id', centrosCustoController.atualizar);
|
|||
router.patch('/:id/habilitado', centrosCustoController.alterarHabilitado);
|
||||
router.delete('/:id', centrosCustoController.deletar);
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
const pool = require('../../../database/mysql');
|
||||
|
||||
const CAMPOS_CENTRO_CUSTO_SELECT = `
|
||||
idcentrodecustos,
|
||||
descricao,
|
||||
limite,
|
||||
simular,
|
||||
investimento,
|
||||
habilitado,
|
||||
insert_date,
|
||||
update_date
|
||||
cc.idcentrodecustos,
|
||||
cc.descricao,
|
||||
cc.investimento,
|
||||
cc.habilitado,
|
||||
cc.insert_date,
|
||||
cc.update_date,
|
||||
CASE
|
||||
WHEN ccs.idcentrodecustossimulacao IS NOT NULL THEN 1
|
||||
ELSE 0
|
||||
END AS simular,
|
||||
ccs.limite
|
||||
`;
|
||||
|
||||
function normalizarTextoOuNull(valor) {
|
||||
|
|
@ -38,9 +41,15 @@ function normalizarFlag(valor, padrao = 0) {
|
|||
return padrao;
|
||||
}
|
||||
|
||||
const numero = Number(valor);
|
||||
if (valor === true || valor === 'true') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return numero === 1 ? 1 : 0;
|
||||
if (valor === false || valor === 'false') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number(valor) === 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
function limitarNumero(valor, padrao, minimo, maximo) {
|
||||
|
|
@ -63,17 +72,17 @@ function limitarNumero(valor, padrao, minimo, maximo) {
|
|||
|
||||
function resolverOrdenacao(orderBy) {
|
||||
const camposPermitidos = {
|
||||
idcentrodecustos: 'idcentrodecustos',
|
||||
descricao: 'descricao',
|
||||
limite: 'limite',
|
||||
idcentrodecustos: 'cc.idcentrodecustos',
|
||||
descricao: 'cc.descricao',
|
||||
limite: 'ccs.limite',
|
||||
simular: 'simular',
|
||||
investimento: 'investimento',
|
||||
habilitado: 'habilitado',
|
||||
insert_date: 'insert_date',
|
||||
update_date: 'update_date',
|
||||
investimento: 'cc.investimento',
|
||||
habilitado: 'cc.habilitado',
|
||||
insert_date: 'cc.insert_date',
|
||||
update_date: 'cc.update_date',
|
||||
};
|
||||
|
||||
return camposPermitidos[orderBy] || 'descricao';
|
||||
return camposPermitidos[orderBy] || 'cc.descricao';
|
||||
}
|
||||
|
||||
function resolverDirecao(orderDirection) {
|
||||
|
|
@ -84,41 +93,51 @@ function montarWhereCentrosCusto(filtros = {}) {
|
|||
const where = [];
|
||||
const params = [];
|
||||
|
||||
if (filtros.simular !== undefined && filtros.simular !== null && filtros.simular !== '') {
|
||||
where.push('simular = ?');
|
||||
params.push(Number(filtros.simular));
|
||||
if (
|
||||
filtros.simular !== undefined
|
||||
&& filtros.simular !== null
|
||||
&& filtros.simular !== ''
|
||||
) {
|
||||
const simular = normalizarFlag(filtros.simular, 0);
|
||||
|
||||
where.push(
|
||||
simular === 1
|
||||
? 'ccs.idcentrodecustossimulacao IS NOT NULL'
|
||||
: 'ccs.idcentrodecustossimulacao IS NULL'
|
||||
);
|
||||
}
|
||||
|
||||
if (filtros.investimento !== undefined && filtros.investimento !== null && filtros.investimento !== '') {
|
||||
where.push('investimento = ?');
|
||||
params.push(Number(filtros.investimento));
|
||||
if (
|
||||
filtros.investimento !== undefined
|
||||
&& filtros.investimento !== null
|
||||
&& filtros.investimento !== ''
|
||||
) {
|
||||
where.push('cc.investimento = ?');
|
||||
params.push(normalizarFlag(filtros.investimento, 0));
|
||||
}
|
||||
|
||||
if (filtros.habilitado !== undefined && filtros.habilitado !== null && filtros.habilitado !== '') {
|
||||
where.push('habilitado = ?');
|
||||
params.push(Number(filtros.habilitado));
|
||||
if (
|
||||
filtros.habilitado !== undefined
|
||||
&& filtros.habilitado !== null
|
||||
&& filtros.habilitado !== ''
|
||||
) {
|
||||
where.push('cc.habilitado = ?');
|
||||
params.push(normalizarFlag(filtros.habilitado, 0));
|
||||
}
|
||||
|
||||
if (filtros.busca) {
|
||||
where.push(`
|
||||
(
|
||||
descricao LIKE ?
|
||||
)
|
||||
`);
|
||||
|
||||
const termo = `%${String(filtros.busca).trim()}%`;
|
||||
params.push(termo);
|
||||
where.push('cc.descricao LIKE ?');
|
||||
params.push(`%${String(filtros.busca).trim()}%`);
|
||||
}
|
||||
|
||||
const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
||||
|
||||
return {
|
||||
whereSql,
|
||||
whereSql: where.length > 0 ? `WHERE ${where.join(' AND ')}` : '',
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
async function listarCentrosCusto(filtros = {}) {
|
||||
const idUsuario = Number(filtros.idUsuario);
|
||||
const limite = limitarNumero(filtros.limite, 20, 1, 100);
|
||||
const page = limitarNumero(filtros.page, 1, 1, 999999);
|
||||
const offset = filtros.offset !== undefined
|
||||
|
|
@ -127,50 +146,69 @@ async function listarCentrosCusto(filtros = {}) {
|
|||
|
||||
const orderBy = resolverOrdenacao(filtros.orderBy);
|
||||
const orderDirection = resolverDirecao(filtros.orderDirection);
|
||||
|
||||
const { whereSql, params } = montarWhereCentrosCusto(filtros);
|
||||
|
||||
const joinSql = `
|
||||
FROM centrodecustos cc
|
||||
LEFT JOIN centrodecustossimulacao ccs
|
||||
ON ccs.idcentrodecustos = cc.idcentrodecustos
|
||||
AND ccs.idusuarios = ?
|
||||
`;
|
||||
|
||||
const queryParams = [idUsuario, ...params];
|
||||
|
||||
const [rows] = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
${CAMPOS_CENTRO_CUSTO_SELECT}
|
||||
FROM centrodecustos
|
||||
${joinSql}
|
||||
${whereSql}
|
||||
ORDER BY ${orderBy} ${orderDirection}, idcentrodecustos ASC
|
||||
ORDER BY ${orderBy} ${orderDirection}, cc.idcentrodecustos ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`,
|
||||
[...params, limite, offset]
|
||||
[...queryParams, limite, offset]
|
||||
);
|
||||
|
||||
const [countRows] = await pool.query(
|
||||
`
|
||||
SELECT COUNT(*) AS total
|
||||
FROM centrodecustos
|
||||
${joinSql}
|
||||
${whereSql}
|
||||
`,
|
||||
params
|
||||
queryParams
|
||||
);
|
||||
|
||||
const [summaryRows] = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*) AS quantidade,
|
||||
COALESCE(SUM(limite), 0) AS limiteTotal,
|
||||
COALESCE(SUM(CASE WHEN habilitado = 1 THEN 1 ELSE 0 END), 0) AS habilitados,
|
||||
COALESCE(SUM(CASE WHEN habilitado = 0 THEN 1 ELSE 0 END), 0) AS desabilitados,
|
||||
COALESCE(SUM(CASE WHEN investimento = 1 THEN 1 ELSE 0 END), 0) AS investimentos,
|
||||
COALESCE(SUM(CASE WHEN simular = 1 THEN 1 ELSE 0 END), 0) AS simulaveis
|
||||
FROM centrodecustos
|
||||
COALESCE(SUM(ccs.limite), 0) AS limiteTotal,
|
||||
COALESCE(SUM(CASE WHEN cc.habilitado = 1 THEN 1 ELSE 0 END), 0) AS habilitados,
|
||||
COALESCE(SUM(CASE WHEN cc.habilitado = 0 THEN 1 ELSE 0 END), 0) AS desabilitados,
|
||||
COALESCE(SUM(CASE WHEN cc.investimento = 1 THEN 1 ELSE 0 END), 0) AS investimentos,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN ccs.idcentrodecustossimulacao IS NOT NULL THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS simulaveis
|
||||
${joinSql}
|
||||
${whereSql}
|
||||
`,
|
||||
params
|
||||
queryParams
|
||||
);
|
||||
|
||||
const total = Number(countRows[0]?.total || 0);
|
||||
const totalPages = Math.max(1, Math.ceil(total / limite));
|
||||
|
||||
return {
|
||||
data: rows,
|
||||
data: rows.map((row) => ({
|
||||
...row,
|
||||
limite: row.limite === null ? null : Number(row.limite),
|
||||
simular: Number(row.simular),
|
||||
investimento: Number(row.investimento),
|
||||
habilitado: Number(row.habilitado),
|
||||
})),
|
||||
pagination: {
|
||||
total,
|
||||
limite,
|
||||
|
|
@ -189,102 +227,178 @@ async function listarCentrosCusto(filtros = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
async function buscarCentroCustoPorId(id) {
|
||||
const [rows] = await pool.query(
|
||||
async function buscarCentroCustoPorId(id, idUsuario, executor = pool) {
|
||||
const [rows] = await executor.query(
|
||||
`
|
||||
SELECT
|
||||
${CAMPOS_CENTRO_CUSTO_SELECT}
|
||||
FROM centrodecustos
|
||||
WHERE idcentrodecustos = ?
|
||||
FROM centrodecustos cc
|
||||
LEFT JOIN centrodecustossimulacao ccs
|
||||
ON ccs.idcentrodecustos = cc.idcentrodecustos
|
||||
AND ccs.idusuarios = ?
|
||||
WHERE cc.idcentrodecustos = ?
|
||||
LIMIT 1
|
||||
`,
|
||||
[id]
|
||||
[idUsuario, id]
|
||||
);
|
||||
|
||||
return rows[0] || null;
|
||||
const centro = rows[0];
|
||||
|
||||
if (!centro) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...centro,
|
||||
limite: centro.limite === null ? null : Number(centro.limite),
|
||||
simular: Number(centro.simular),
|
||||
investimento: Number(centro.investimento),
|
||||
habilitado: Number(centro.habilitado),
|
||||
};
|
||||
}
|
||||
|
||||
async function criarCentroCusto(dados) {
|
||||
const {
|
||||
descricao,
|
||||
limite,
|
||||
simular,
|
||||
investimento,
|
||||
habilitado,
|
||||
} = dados;
|
||||
async function sincronizarSimulacao(
|
||||
executor,
|
||||
idCentroCusto,
|
||||
idUsuario,
|
||||
dados
|
||||
) {
|
||||
const simular = normalizarFlag(dados.simular, 0);
|
||||
|
||||
if (simular === 1) {
|
||||
await executor.query(
|
||||
`
|
||||
INSERT INTO centrodecustossimulacao (
|
||||
idcentrodecustos,
|
||||
idusuarios,
|
||||
limite,
|
||||
insert_date,
|
||||
update_date
|
||||
) VALUES (?, ?, ?, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
limite = VALUES(limite),
|
||||
update_date = NOW()
|
||||
`,
|
||||
[
|
||||
idCentroCusto,
|
||||
idUsuario,
|
||||
normalizarNumero(dados.limite, 0),
|
||||
]
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await executor.query(
|
||||
`
|
||||
DELETE FROM centrodecustossimulacao
|
||||
WHERE idcentrodecustos = ?
|
||||
AND idusuarios = ?
|
||||
`,
|
||||
[idCentroCusto, idUsuario]
|
||||
);
|
||||
}
|
||||
|
||||
async function criarCentroCusto(dados, idUsuario) {
|
||||
const connection = await pool.getConnection();
|
||||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
const [result] = await connection.query(
|
||||
`
|
||||
INSERT INTO centrodecustos (
|
||||
descricao,
|
||||
investimento,
|
||||
habilitado,
|
||||
insert_date,
|
||||
update_date
|
||||
) VALUES (?, ?, ?, NOW(), NOW())
|
||||
`,
|
||||
[
|
||||
normalizarTextoOuNull(dados.descricao),
|
||||
normalizarFlag(dados.investimento, 0),
|
||||
normalizarFlag(dados.habilitado, 1),
|
||||
]
|
||||
);
|
||||
|
||||
await sincronizarSimulacao(
|
||||
connection,
|
||||
result.insertId,
|
||||
idUsuario,
|
||||
dados
|
||||
);
|
||||
|
||||
await connection.commit();
|
||||
|
||||
return buscarCentroCustoPorId(result.insertId, idUsuario);
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function atualizarCentroCusto(id, dados, idUsuario) {
|
||||
const connection = await pool.getConnection();
|
||||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
const [rows] = await connection.query(
|
||||
`
|
||||
SELECT idcentrodecustos
|
||||
FROM centrodecustos
|
||||
WHERE idcentrodecustos = ?
|
||||
LIMIT 1
|
||||
`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!rows[0]) {
|
||||
await connection.rollback();
|
||||
return null;
|
||||
}
|
||||
|
||||
await connection.query(
|
||||
`
|
||||
UPDATE centrodecustos
|
||||
SET
|
||||
descricao = ?,
|
||||
investimento = ?,
|
||||
habilitado = ?,
|
||||
update_date = NOW()
|
||||
WHERE idcentrodecustos = ?
|
||||
`,
|
||||
[
|
||||
normalizarTextoOuNull(dados.descricao),
|
||||
normalizarFlag(dados.investimento, 0),
|
||||
normalizarFlag(dados.habilitado, 1),
|
||||
id,
|
||||
]
|
||||
);
|
||||
|
||||
await sincronizarSimulacao(
|
||||
connection,
|
||||
id,
|
||||
idUsuario,
|
||||
dados
|
||||
);
|
||||
|
||||
await connection.commit();
|
||||
|
||||
return buscarCentroCustoPorId(id, idUsuario);
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function alterarHabilitadoCentroCusto(id, habilitado, idUsuario) {
|
||||
const [result] = await pool.query(
|
||||
`
|
||||
INSERT INTO centrodecustos (
|
||||
descricao,
|
||||
limite,
|
||||
simular,
|
||||
investimento,
|
||||
habilitado,
|
||||
insert_date,
|
||||
update_date
|
||||
) VALUES (?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`,
|
||||
[
|
||||
normalizarTextoOuNull(descricao),
|
||||
normalizarNumero(limite, 0),
|
||||
normalizarFlag(simular, 0),
|
||||
normalizarFlag(investimento, 0),
|
||||
normalizarFlag(habilitado, 1),
|
||||
]
|
||||
);
|
||||
|
||||
return buscarCentroCustoPorId(result.insertId);
|
||||
}
|
||||
|
||||
async function atualizarCentroCusto(id, dados) {
|
||||
const centroAtual = await buscarCentroCustoPorId(id);
|
||||
|
||||
if (!centroAtual) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
descricao,
|
||||
limite,
|
||||
simular,
|
||||
investimento,
|
||||
habilitado,
|
||||
} = dados;
|
||||
|
||||
await pool.query(
|
||||
`
|
||||
UPDATE centrodecustos
|
||||
SET
|
||||
descricao = ?,
|
||||
limite = ?,
|
||||
simular = ?,
|
||||
investimento = ?,
|
||||
habilitado = ?,
|
||||
update_date = NOW()
|
||||
WHERE idcentrodecustos = ?
|
||||
`,
|
||||
[
|
||||
normalizarTextoOuNull(descricao),
|
||||
normalizarNumero(limite, 0),
|
||||
normalizarFlag(simular, 0),
|
||||
normalizarFlag(investimento, 0),
|
||||
normalizarFlag(habilitado, 1),
|
||||
id,
|
||||
]
|
||||
);
|
||||
|
||||
return buscarCentroCustoPorId(id);
|
||||
}
|
||||
|
||||
async function alterarHabilitadoCentroCusto(id, habilitado) {
|
||||
const centroAtual = await buscarCentroCustoPorId(id);
|
||||
|
||||
if (!centroAtual) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
`
|
||||
UPDATE centrodecustos
|
||||
SET
|
||||
|
|
@ -298,17 +412,15 @@ async function alterarHabilitadoCentroCusto(id, habilitado) {
|
|||
]
|
||||
);
|
||||
|
||||
return buscarCentroCustoPorId(id);
|
||||
if (result.affectedRows === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buscarCentroCustoPorId(id, idUsuario);
|
||||
}
|
||||
|
||||
async function deletarCentroCusto(id) {
|
||||
const centroAtual = await buscarCentroCustoPorId(id);
|
||||
|
||||
if (!centroAtual) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
const [result] = await pool.query(
|
||||
`
|
||||
DELETE FROM centrodecustos
|
||||
WHERE idcentrodecustos = ?
|
||||
|
|
@ -316,7 +428,7 @@ async function deletarCentroCusto(id) {
|
|||
[id]
|
||||
);
|
||||
|
||||
return true;
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
@ -326,4 +438,4 @@ module.exports = {
|
|||
atualizarCentroCusto,
|
||||
alterarHabilitadoCentroCusto,
|
||||
deletarCentroCusto,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
const dashboardService = require('../services/dashboard.service');
|
||||
|
||||
function resolverIdUsuario(req) {
|
||||
return Number(req.user?.idusuarios || req.user?.id);
|
||||
return Number(
|
||||
req.usuario?.idusuarios
|
||||
?? req.user?.idusuarios
|
||||
?? req.usuario?.idusuario
|
||||
?? req.user?.idusuario
|
||||
?? req.usuario?.id
|
||||
?? req.user?.id
|
||||
?? 0
|
||||
);
|
||||
}
|
||||
|
||||
async function buscarResumo(req, res) {
|
||||
|
|
@ -27,9 +35,12 @@ async function buscarResumo(req, res) {
|
|||
} catch (error) {
|
||||
console.error('[dashboard.controller] buscarResumo:', error);
|
||||
|
||||
return res.status(500).json({
|
||||
return res.status(error.statusCode || 500).json({
|
||||
ok: false,
|
||||
message: 'Não foi possível carregar o resumo da dashboard.',
|
||||
message:
|
||||
error.statusCode
|
||||
? error.message
|
||||
: 'Não foi possível carregar o resumo da dashboard.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,49 @@
|
|||
const pool = require('../../../database/mysql');
|
||||
|
||||
function formatarDataLocal(data) {
|
||||
const ano = data.getFullYear();
|
||||
const mes = String(data.getMonth() + 1).padStart(2, '0');
|
||||
const dia = String(data.getDate()).padStart(2, '0');
|
||||
|
||||
return `${ano}-${mes}-${dia}`;
|
||||
}
|
||||
|
||||
function hojeISO() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
return formatarDataLocal(new Date());
|
||||
}
|
||||
|
||||
function inicioMesAtualISO() {
|
||||
const hoje = new Date();
|
||||
return new Date(hoje.getFullYear(), hoje.getMonth(), 1)
|
||||
.toISOString()
|
||||
.slice(0, 10);
|
||||
return formatarDataLocal(
|
||||
new Date(hoje.getFullYear(), hoje.getMonth(), 1)
|
||||
);
|
||||
}
|
||||
|
||||
function fimMesAtualISO() {
|
||||
const hoje = new Date();
|
||||
return new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0)
|
||||
.toISOString()
|
||||
.slice(0, 10);
|
||||
return formatarDataLocal(
|
||||
new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizarDataISO(valor, fallback) {
|
||||
if (!valor) return fallback;
|
||||
|
||||
const texto = String(valor).slice(0, 10);
|
||||
const data = new Date(`${texto}T00:00:00`);
|
||||
|
||||
if (Number.isNaN(data.getTime())) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(texto)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return texto;
|
||||
const [ano, mes, dia] = texto.split('-').map(Number);
|
||||
const data = new Date(ano, mes - 1, dia);
|
||||
|
||||
const valida =
|
||||
data.getFullYear() === ano
|
||||
&& data.getMonth() === mes - 1
|
||||
&& data.getDate() === dia;
|
||||
|
||||
return valida ? texto : fallback;
|
||||
}
|
||||
|
||||
function normalizarNumero(valor, padrao = 0) {
|
||||
|
|
@ -84,10 +99,7 @@ async function buscarCardsBancos(idUsuario) {
|
|||
|
||||
COUNT(*) AS quantidadeCarteiras,
|
||||
|
||||
SUM(CASE
|
||||
WHEN habilitado = 1 THEN 1
|
||||
ELSE 0
|
||||
END) AS carteirasHabilitadas,
|
||||
COUNT(*) AS carteirasHabilitadas,
|
||||
|
||||
SUM(CASE
|
||||
WHEN debito = 1
|
||||
|
|
@ -113,6 +125,7 @@ async function buscarCardsBancos(idUsuario) {
|
|||
END) AS quantidadeCreditos
|
||||
FROM bancos
|
||||
WHERE idusuarios = ?
|
||||
AND habilitado = 1
|
||||
`,
|
||||
[idUsuario]
|
||||
);
|
||||
|
|
@ -149,16 +162,18 @@ async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) {
|
|||
ELSE 0
|
||||
END), 0) AS entradas,
|
||||
|
||||
COALESCE(SUM(CASE
|
||||
WHEN cp.movimento = 'Saida' THEN cp.valor
|
||||
ELSE 0
|
||||
COALESCE(SUM(CASE
|
||||
WHEN cp.movimento IN ('Saida', 'Sangria')
|
||||
AND COALESCE(cc.investimento, 0) = 0
|
||||
THEN cp.valor
|
||||
ELSE 0
|
||||
END), 0) AS saidas,
|
||||
|
||||
COALESCE(SUM(CASE
|
||||
WHEN cp.movimento = 'Sangria'
|
||||
COALESCE(SUM(CASE
|
||||
WHEN cp.movimento IN ('Saida', 'Sangria')
|
||||
AND COALESCE(cc.investimento, 0) = 1
|
||||
THEN cp.valor
|
||||
ELSE 0
|
||||
THEN cp.valor
|
||||
ELSE 0
|
||||
END), 0) AS investimentos,
|
||||
|
||||
COALESCE(SUM(CASE
|
||||
|
|
@ -175,34 +190,36 @@ async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) {
|
|||
ELSE NULL
|
||||
END) AS quantidadeAbertoEntradas,
|
||||
|
||||
COALESCE(SUM(CASE
|
||||
COALESCE(SUM(CASE
|
||||
WHEN cp.status = 'A pagar'
|
||||
AND cp.movimento = 'Saida'
|
||||
THEN cp.valor
|
||||
ELSE 0
|
||||
AND cp.movimento IN ('Saida', 'Sangria')
|
||||
AND COALESCE(cc.investimento, 0) = 0
|
||||
THEN cp.valor
|
||||
ELSE 0
|
||||
END), 0) AS abertoSaidas,
|
||||
|
||||
COUNT(CASE
|
||||
COUNT(CASE
|
||||
WHEN cp.status = 'A pagar'
|
||||
AND cp.movimento = 'Saida'
|
||||
THEN 1
|
||||
ELSE NULL
|
||||
AND cp.movimento IN ('Saida', 'Sangria')
|
||||
AND COALESCE(cc.investimento, 0) = 0
|
||||
THEN 1
|
||||
ELSE NULL
|
||||
END) AS quantidadeAbertoSaidas,
|
||||
|
||||
COALESCE(SUM(CASE
|
||||
COALESCE(SUM(CASE
|
||||
WHEN cp.status = 'A pagar'
|
||||
AND cp.movimento = 'Sangria'
|
||||
AND cp.movimento IN ('Saida', 'Sangria')
|
||||
AND COALESCE(cc.investimento, 0) = 1
|
||||
THEN cp.valor
|
||||
ELSE 0
|
||||
THEN cp.valor
|
||||
ELSE 0
|
||||
END), 0) AS abertoInvestimentos,
|
||||
|
||||
COUNT(CASE
|
||||
COUNT(CASE
|
||||
WHEN cp.status = 'A pagar'
|
||||
AND cp.movimento = 'Sangria'
|
||||
AND cp.movimento IN ('Saida', 'Sangria')
|
||||
AND COALESCE(cc.investimento, 0) = 1
|
||||
THEN 1
|
||||
ELSE NULL
|
||||
THEN 1
|
||||
ELSE NULL
|
||||
END) AS quantidadeAbertoInvestimentos,
|
||||
|
||||
COALESCE(SUM(CASE
|
||||
|
|
@ -212,7 +229,7 @@ async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) {
|
|||
|
||||
COUNT(*) AS quantidadeMovimentos
|
||||
FROM contasapagar cp
|
||||
LEFT JOIN bancos b
|
||||
INNER JOIN bancos b
|
||||
ON b.idbancos = cp.idbancos
|
||||
LEFT JOIN centrodecustos cc
|
||||
ON cc.idcentrodecustos = cp.idcentrodecustos
|
||||
|
|
@ -320,8 +337,9 @@ async function buscarAlertasVencimento(idUsuario) {
|
|||
COALESCE(SUM(CASE
|
||||
WHEN DATE(cp.datavencimento) >= ?
|
||||
AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
|
||||
AND cp.movimento = 'Saida'
|
||||
AND cp.movimento IN ('Saida', 'Sangria')
|
||||
AND cp.status = 'A pagar'
|
||||
AND COALESCE(cc.investimento, 0) = 0
|
||||
THEN cp.valor
|
||||
ELSE 0
|
||||
END), 0) AS venceEmBreveSaidas,
|
||||
|
|
@ -329,8 +347,9 @@ async function buscarAlertasVencimento(idUsuario) {
|
|||
COUNT(CASE
|
||||
WHEN DATE(cp.datavencimento) >= ?
|
||||
AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
|
||||
AND cp.movimento = 'Saida'
|
||||
AND cp.movimento IN ('Saida', 'Sangria')
|
||||
AND cp.status = 'A pagar'
|
||||
AND COALESCE(cc.investimento, 0) = 0
|
||||
THEN 1
|
||||
ELSE NULL
|
||||
END) AS quantidadeVenceEmBreveSaidas,
|
||||
|
|
@ -338,7 +357,7 @@ async function buscarAlertasVencimento(idUsuario) {
|
|||
COALESCE(SUM(CASE
|
||||
WHEN DATE(cp.datavencimento) >= ?
|
||||
AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
|
||||
AND cp.movimento = 'Sangria'
|
||||
AND cp.movimento IN ('Saida', 'Sangria')
|
||||
AND cp.status = 'A pagar'
|
||||
AND COALESCE(cc.investimento, 0) = 1
|
||||
THEN cp.valor
|
||||
|
|
@ -348,7 +367,7 @@ async function buscarAlertasVencimento(idUsuario) {
|
|||
COUNT(CASE
|
||||
WHEN DATE(cp.datavencimento) >= ?
|
||||
AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
|
||||
AND cp.movimento = 'Sangria'
|
||||
AND cp.movimento IN ('Saida', 'Sangria')
|
||||
AND cp.status = 'A pagar'
|
||||
AND COALESCE(cc.investimento, 0) = 1
|
||||
THEN 1
|
||||
|
|
@ -430,7 +449,7 @@ async function buscarProximosVencimentos(idUsuario) {
|
|||
cc.descricao AS centro_custo_descricao,
|
||||
DATEDIFF(DATE(cp.datavencimento), CURDATE()) AS dias_para_vencer
|
||||
FROM contasapagar cp
|
||||
LEFT JOIN bancos bc ON bc.idbancos = cp.idbancos
|
||||
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 = ?
|
||||
|
|
@ -630,7 +649,7 @@ async function buscarUltimosMovimentos(idUsuario) {
|
|||
cp.idcentrodecustos,
|
||||
cc.descricao AS centro_custo_descricao
|
||||
FROM contasapagar cp
|
||||
LEFT JOIN bancos bc ON bc.idbancos = cp.idbancos
|
||||
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 = ?
|
||||
|
|
@ -655,7 +674,8 @@ async function buscarGraficos(idUsuario, dataInicio, dataFim) {
|
|||
LEFT JOIN bancos b
|
||||
ON b.idbancos = cp.idbancos
|
||||
WHERE cp.deleted_at IS NULL
|
||||
AND cp.movimento IN ('Saida')
|
||||
AND cp.movimento IN ('Saida', 'Sangria')
|
||||
AND COALESCE(cc.investimento, 0) = 0
|
||||
AND DATE(cp.datavencimento) BETWEEN ? AND ?
|
||||
AND b.idusuarios = ?
|
||||
GROUP BY
|
||||
|
|
@ -676,16 +696,18 @@ async function buscarGraficos(idUsuario, dataInicio, dataFim) {
|
|||
ELSE 0
|
||||
END), 0) AS entradas,
|
||||
|
||||
COALESCE(SUM(CASE
|
||||
WHEN cp.movimento = 'Saida' THEN cp.valor
|
||||
ELSE 0
|
||||
COALESCE(SUM(CASE
|
||||
WHEN cp.movimento IN ('Saida', 'Sangria')
|
||||
AND COALESCE(cc.investimento, 0) = 0
|
||||
THEN cp.valor
|
||||
ELSE 0
|
||||
END), 0) AS saidas,
|
||||
|
||||
COALESCE(SUM(CASE
|
||||
WHEN cp.movimento = 'Sangria'
|
||||
AND COALESCE(cc.investimento, 0) = 1
|
||||
THEN cp.valor
|
||||
ELSE 0
|
||||
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
|
||||
LEFT JOIN centrodecustos cc
|
||||
|
|
@ -753,6 +775,12 @@ async function buscarResumoDashboard(idUsuario, filtros = {}) {
|
|||
const dataInicio = normalizarDataISO(filtros.dataInicio, inicioMesAtualISO());
|
||||
const dataFim = normalizarDataISO(filtros.dataFim, fimMesAtualISO());
|
||||
|
||||
if (dataInicio > dataFim) {
|
||||
const error = new Error('A data inicial não pode ser maior que a data final.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const [
|
||||
cardsBancos,
|
||||
cardsMovimentos,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
const simulacaoMensalService = require('../services/simulacaoMensal.service');
|
||||
|
||||
function extrairIdUsuarioLogado(req) {
|
||||
return (
|
||||
req.usuario?.idusuarios ||
|
||||
req.user?.idusuarios ||
|
||||
req.usuario?.id ||
|
||||
req.user?.id ||
|
||||
null
|
||||
return Number(
|
||||
req.usuario?.idusuarios
|
||||
?? req.user?.idusuarios
|
||||
?? req.usuario?.idusuario
|
||||
?? req.user?.idusuario
|
||||
?? req.usuario?.id
|
||||
?? req.user?.id
|
||||
?? 0
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -310,23 +310,29 @@ async function buscarFixosUsuario(connection, idUsuario) {
|
|||
return rows;
|
||||
}
|
||||
|
||||
async function buscarCentrosSimulaveis(connection) {
|
||||
async function buscarCentrosSimulaveis(connection, idUsuario) {
|
||||
const [rows] = await connection.query(
|
||||
`
|
||||
SELECT
|
||||
idcentrodecustos,
|
||||
descricao,
|
||||
limite,
|
||||
simular,
|
||||
investimento
|
||||
FROM centrodecustos
|
||||
WHERE habilitado = 1
|
||||
AND simular = 1
|
||||
ORDER BY descricao ASC
|
||||
`
|
||||
cc.idcentrodecustos,
|
||||
cc.descricao,
|
||||
ccs.limite,
|
||||
cc.investimento
|
||||
FROM centrodecustossimulacao ccs
|
||||
INNER JOIN centrodecustos cc
|
||||
ON cc.idcentrodecustos = ccs.idcentrodecustos
|
||||
WHERE ccs.idusuarios = ?
|
||||
AND cc.habilitado = 1
|
||||
ORDER BY cc.descricao ASC
|
||||
`,
|
||||
[idUsuario]
|
||||
);
|
||||
|
||||
return rows;
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
limite: Number(row.limite || 0),
|
||||
investimento: Number(row.investimento || 0),
|
||||
}));
|
||||
}
|
||||
|
||||
function movimentoFixoJaExisteNoMes(fixo, movimentosDoMes) {
|
||||
|
|
@ -716,7 +722,7 @@ async function montarBaseSimulacaoMensal(opcoes = {}) {
|
|||
dataFim,
|
||||
}),
|
||||
buscarFixosUsuario(connection, idUsuario),
|
||||
buscarCentrosSimulaveis(connection),
|
||||
buscarCentrosSimulaveis(connection, idUsuario),
|
||||
buscarMovimentosParaCentrosDoMes(connection, {
|
||||
idUsuario,
|
||||
dataInicio,
|
||||
|
|
|
|||
|
|
@ -2,9 +2,16 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>financeiro-web</title>
|
||||
<title>Zendion Finance</title>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="Gestão financeira, simulações e controle de carteiras."
|
||||
/>
|
||||
|
||||
<meta name="theme-color" content="#0F172A" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
|
|
@ -9,12 +9,27 @@ const SIDEBAR_WIDTH = 280;
|
|||
export function AppLayout() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
function abrirMenuMobile() {
|
||||
setMobileOpen(true);
|
||||
}
|
||||
|
||||
function fecharMenuMobile() {
|
||||
setMobileOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', backgroundColor: 'background.default' }}>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
overflowX: 'hidden',
|
||||
backgroundColor: 'background.default',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="aside"
|
||||
sx={{
|
||||
width: SIDEBAR_WIDTH,
|
||||
height: '100vh',
|
||||
display: { xs: 'none', md: 'block' },
|
||||
position: 'fixed',
|
||||
inset: '0 auto 0 0',
|
||||
|
|
@ -26,40 +41,49 @@ export function AppLayout() {
|
|||
|
||||
<Drawer
|
||||
open={mobileOpen}
|
||||
onClose={() => setMobileOpen(false)}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
onClose={fecharMenuMobile}
|
||||
ModalProps={{
|
||||
keepMounted: true,
|
||||
}}
|
||||
sx={{
|
||||
display: { xs: 'block', md: 'none' },
|
||||
|
||||
'& .MuiDrawer-paper': {
|
||||
width: SIDEBAR_WIDTH,
|
||||
border: 0,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Sidebar onNavigate={() => setMobileOpen(false)} />
|
||||
<Sidebar onNavigate={fecharMenuMobile} />
|
||||
</Drawer>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
marginLeft: { xs: 0, md: `${SIDEBAR_WIDTH}px` },
|
||||
marginLeft: {
|
||||
xs: 0,
|
||||
md: `${SIDEBAR_WIDTH}px`,
|
||||
},
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflowX: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Topbar onMenuClick={() => setMobileOpen(true)} />
|
||||
<Topbar onMenuClick={abrirMenuMobile} />
|
||||
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
maxWidth: 1280,
|
||||
margin: '0 auto',
|
||||
maxWidth: 1440,
|
||||
marginX: 'auto',
|
||||
padding: {
|
||||
xs: 2,
|
||||
sm: 2.5,
|
||||
md: 4,
|
||||
md: 3.5,
|
||||
lg: 4,
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
|
|
@ -5,170 +6,331 @@ import {
|
|||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||
import AssessmentIcon from '@mui/icons-material/Assessment';
|
||||
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
|
||||
import SavingsIcon from '@mui/icons-material/Savings';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import PeopleAltIcon from '@mui/icons-material/PeopleAlt';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
|
||||
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
|
||||
import CreditCardIcon from '@mui/icons-material/CreditCard';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import DashboardRoundedIcon from '@mui/icons-material/DashboardRounded';
|
||||
import SwapHorizRoundedIcon from '@mui/icons-material/SwapHorizRounded';
|
||||
import AssessmentRoundedIcon from '@mui/icons-material/AssessmentRounded';
|
||||
import AccountBalanceWalletRoundedIcon from '@mui/icons-material/AccountBalanceWalletRounded';
|
||||
import SavingsRoundedIcon from '@mui/icons-material/SavingsRounded';
|
||||
import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded';
|
||||
import PeopleAltRoundedIcon from '@mui/icons-material/PeopleAltRounded';
|
||||
import AccountTreeRoundedIcon from '@mui/icons-material/AccountTreeRounded';
|
||||
import EventRepeatRoundedIcon from '@mui/icons-material/EventRepeatRounded';
|
||||
import ManageAccountsRoundedIcon from '@mui/icons-material/ManageAccountsRounded';
|
||||
import CreditCardRoundedIcon from '@mui/icons-material/CreditCardRounded';
|
||||
import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
|
||||
import InsightsRoundedIcon from '@mui/icons-material/InsightsRounded';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
type SidebarProps = {
|
||||
onNavigate?: () => void;
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
type MenuItem = {
|
||||
label: string;
|
||||
path: string;
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
type MenuSection = {
|
||||
title: string;
|
||||
items: MenuItem[];
|
||||
};
|
||||
|
||||
const menuSections: MenuSection[] = [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
icon: <DashboardIcon />,
|
||||
title: 'Visão geral',
|
||||
items: [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
icon: <DashboardRoundedIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Simulação mensal',
|
||||
path: '/simulacao-mensal',
|
||||
icon: <AutoAwesomeRoundedIcon />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Movimentos',
|
||||
path: '/movimentos',
|
||||
icon: <SwapHorizIcon />,
|
||||
title: 'Financeiro',
|
||||
items: [
|
||||
{
|
||||
label: 'Movimentos',
|
||||
path: '/movimentos',
|
||||
icon: <SwapHorizRoundedIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Movimentos fixos',
|
||||
path: '/movimentos-fixos',
|
||||
icon: <EventRepeatRoundedIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Quitar créditos',
|
||||
path: '/quitacoes-credito',
|
||||
icon: <CreditCardRoundedIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Carteiras',
|
||||
path: '/bancos',
|
||||
icon: <AccountBalanceWalletRoundedIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Rendimentos',
|
||||
path: '/rendimentos',
|
||||
icon: <SavingsRoundedIcon />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Quitar créditos',
|
||||
path: '/quitacoes-credito',
|
||||
icon: <CreditCardIcon />,
|
||||
title: 'Cadastros',
|
||||
items: [
|
||||
{
|
||||
label: 'Clientes',
|
||||
path: '/clientes',
|
||||
icon: <PeopleAltRoundedIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Centros de custo',
|
||||
path: '/centros-custo',
|
||||
icon: <AccountTreeRoundedIcon />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Clientes',
|
||||
path: '/clientes',
|
||||
icon: <PeopleAltIcon />,
|
||||
title: 'Análise',
|
||||
items: [
|
||||
{
|
||||
label: 'Relatórios',
|
||||
path: '/relatorios',
|
||||
icon: <AssessmentRoundedIcon />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Carteiras',
|
||||
path: '/bancos',
|
||||
icon: <AccountBalanceWalletIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Rendimentos',
|
||||
path: '/rendimentos',
|
||||
icon: <SavingsIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Centros de custo',
|
||||
path: '/centros-custo',
|
||||
icon: <AccountTreeIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Movimentos fixos',
|
||||
path: '/movimentos-fixos',
|
||||
icon: <EventRepeatIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Usuários',
|
||||
path: '/usuarios',
|
||||
icon: <ManageAccountsIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Relatórios',
|
||||
path: '/relatorios',
|
||||
icon: <AssessmentIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Simulação mensal',
|
||||
path: '/simulacao-mensal',
|
||||
icon: <AutoAwesomeIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Configurações',
|
||||
path: '/configuracoes',
|
||||
icon: <SettingsIcon />,
|
||||
title: 'Administração',
|
||||
items: [
|
||||
{
|
||||
label: 'Usuários',
|
||||
path: '/usuarios',
|
||||
icon: <ManageAccountsRoundedIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Configurações',
|
||||
path: '/configuracoes',
|
||||
icon: <SettingsRoundedIcon />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Sidebar({ onNavigate }: SidebarProps) {
|
||||
return (
|
||||
<Box
|
||||
component="aside"
|
||||
sx={{
|
||||
width: 280,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'linear-gradient(180deg, #0F172A 0%, #111827 100%)',
|
||||
color: '#fff',
|
||||
color: '#FFFFFF',
|
||||
background:
|
||||
'linear-gradient(180deg, #0F172A 0%, #111827 52%, #0B1220 100%)',
|
||||
borderRight: '1px solid rgba(255,255,255,0.06)',
|
||||
boxShadow: '10px 0 30px rgba(15,23,42,0.16)',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ padding: 3 }}>
|
||||
<Typography variant="h6" fontWeight={900}>
|
||||
Zendion
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
padding: 3,
|
||||
paddingBottom: 2.5,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||
<Box
|
||||
sx={{
|
||||
width: 46,
|
||||
height: 46,
|
||||
flexShrink: 0,
|
||||
borderRadius: 3,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background:
|
||||
'linear-gradient(135deg, #3B82F6 0%, #2563EB 58%, #1D4ED8 100%)',
|
||||
boxShadow: '0 12px 28px rgba(37,99,235,0.32)',
|
||||
border: '1px solid rgba(255,255,255,0.18)',
|
||||
}}
|
||||
>
|
||||
<InsightsRoundedIcon sx={{ fontSize: 27, color: '#FFFFFF' }} />
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.65)' }}>
|
||||
Financeiro
|
||||
</Typography>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
fontWeight={950}
|
||||
lineHeight={1.05}
|
||||
noWrap
|
||||
>
|
||||
Zendion Finance
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: 'rgba(255,255,255,0.58)',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.02em',
|
||||
}}
|
||||
>
|
||||
Gestão financeira
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ borderColor: 'rgba(255,255,255,0.08)' }} />
|
||||
|
||||
<List sx={{ padding: 2, flex: 1 }}>
|
||||
{menuItems.map((item) => (
|
||||
<ListItemButton
|
||||
key={item.path}
|
||||
component={NavLink}
|
||||
to={item.path}
|
||||
onClick={onNavigate}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
marginBottom: 0.75,
|
||||
color: 'rgba(255,255,255,0.72)',
|
||||
'& .MuiListItemIcon-root': {
|
||||
color: 'rgba(255,255,255,0.72)',
|
||||
minWidth: 40,
|
||||
},
|
||||
'&.active': {
|
||||
backgroundColor: 'rgba(59,130,246,0.22)',
|
||||
color: '#fff',
|
||||
'& .MuiListItemIcon-root': {
|
||||
color: '#60A5FA',
|
||||
},
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||
<List
|
||||
component="nav"
|
||||
aria-label="Menu principal"
|
||||
sx={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
padding: 2,
|
||||
paddingTop: 2.25,
|
||||
|
||||
<ListItemText
|
||||
primary={item.label}
|
||||
primaryTypographyProps={{
|
||||
fontWeight: 700,
|
||||
'&::-webkit-scrollbar': {
|
||||
width: 6,
|
||||
},
|
||||
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
borderRadius: 999,
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
},
|
||||
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{menuSections.map((section) => (
|
||||
<Box key={section.title} sx={{ marginBottom: 2.2 }}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
sx={{
|
||||
display: 'block',
|
||||
paddingX: 1.5,
|
||||
marginBottom: 0.7,
|
||||
color: 'rgba(255,255,255,0.36)',
|
||||
fontWeight: 900,
|
||||
fontSize: '0.68rem',
|
||||
lineHeight: 1.8,
|
||||
letterSpacing: '0.12em',
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
>
|
||||
{section.title}
|
||||
</Typography>
|
||||
|
||||
{section.items.map((item) => (
|
||||
<ListItemButton
|
||||
key={item.path}
|
||||
component={NavLink}
|
||||
to={item.path}
|
||||
onClick={onNavigate}
|
||||
sx={{
|
||||
minHeight: 46,
|
||||
paddingX: 1.45,
|
||||
marginBottom: 0.55,
|
||||
borderRadius: 2.5,
|
||||
color: 'rgba(255,255,255,0.70)',
|
||||
transition:
|
||||
'background-color 160ms ease, color 160ms ease, transform 160ms ease',
|
||||
|
||||
'& .MuiListItemIcon-root': {
|
||||
minWidth: 39,
|
||||
color: 'rgba(255,255,255,0.52)',
|
||||
transition: 'color 160ms ease',
|
||||
},
|
||||
|
||||
'& .MuiListItemText-primary': {
|
||||
fontWeight: 750,
|
||||
fontSize: '0.92rem',
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
color: '#FFFFFF',
|
||||
backgroundColor: 'rgba(255,255,255,0.07)',
|
||||
transform: 'translateX(2px)',
|
||||
|
||||
'& .MuiListItemIcon-root': {
|
||||
color: 'rgba(255,255,255,0.82)',
|
||||
},
|
||||
},
|
||||
|
||||
'&.active': {
|
||||
color: '#FFFFFF',
|
||||
background:
|
||||
'linear-gradient(90deg, rgba(59,130,246,0.30), rgba(59,130,246,0.12))',
|
||||
boxShadow:
|
||||
'inset 3px 0 0 #60A5FA, 0 8px 18px rgba(15,23,42,0.18)',
|
||||
|
||||
'& .MuiListItemIcon-root': {
|
||||
color: '#60A5FA',
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
background:
|
||||
'linear-gradient(90deg, rgba(59,130,246,0.34), rgba(59,130,246,0.15))',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||
|
||||
<ListItemText primary={item.label} />
|
||||
</ListItemButton>
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Divider sx={{ borderColor: 'rgba(255,255,255,0.08)' }} />
|
||||
|
||||
<Box sx={{ padding: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
padding: 2,
|
||||
borderRadius: 3,
|
||||
backgroundColor: 'rgba(255,255,255,0.06)',
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(255,255,255,0.07), rgba(255,255,255,0.035))',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" fontWeight={800}>
|
||||
MVP ativo
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={850}
|
||||
sx={{ color: 'rgba(255,255,255,0.88)' }}
|
||||
>
|
||||
Zendion Finance
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.62)' }}>
|
||||
Movimentos e carteiras em operação.
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
display: 'block',
|
||||
marginTop: 0.25,
|
||||
color: 'rgba(255,255,255,0.46)',
|
||||
}}
|
||||
>
|
||||
Versão 1.0.0
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,28 +2,146 @@ import {
|
|||
AppBar,
|
||||
Avatar,
|
||||
Box,
|
||||
Chip,
|
||||
IconButton,
|
||||
Stack,
|
||||
Toolbar,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import MenuRoundedIcon from '@mui/icons-material/MenuRounded';
|
||||
import LogoutRoundedIcon from '@mui/icons-material/LogoutRounded';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../features/auth/store/authStore';
|
||||
|
||||
type TopbarProps = {
|
||||
onMenuClick: () => void;
|
||||
};
|
||||
|
||||
type RouteInfo = {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const routeInfoMap: Record<string, RouteInfo> = {
|
||||
'/dashboard': {
|
||||
eyebrow: 'Visão geral',
|
||||
title: 'Dashboard',
|
||||
},
|
||||
'/movimentos': {
|
||||
eyebrow: 'Financeiro',
|
||||
title: 'Movimentos',
|
||||
},
|
||||
'/quitacoes-credito': {
|
||||
eyebrow: 'Financeiro',
|
||||
title: 'Quitar créditos',
|
||||
},
|
||||
'/clientes': {
|
||||
eyebrow: 'Cadastros',
|
||||
title: 'Clientes',
|
||||
},
|
||||
'/bancos': {
|
||||
eyebrow: 'Financeiro',
|
||||
title: 'Carteiras',
|
||||
},
|
||||
'/rendimentos': {
|
||||
eyebrow: 'Financeiro',
|
||||
title: 'Rendimentos',
|
||||
},
|
||||
'/centros-custo': {
|
||||
eyebrow: 'Cadastros',
|
||||
title: 'Centros de custo',
|
||||
},
|
||||
'/movimentos-fixos': {
|
||||
eyebrow: 'Financeiro',
|
||||
title: 'Movimentos fixos',
|
||||
},
|
||||
'/usuarios': {
|
||||
eyebrow: 'Administração',
|
||||
title: 'Usuários',
|
||||
},
|
||||
'/relatorios': {
|
||||
eyebrow: 'Análise',
|
||||
title: 'Relatórios',
|
||||
},
|
||||
'/simulacao-mensal': {
|
||||
eyebrow: 'Visão geral',
|
||||
title: 'Simulação mensal',
|
||||
},
|
||||
'/configuracoes': {
|
||||
eyebrow: 'Administração',
|
||||
title: 'Configurações',
|
||||
},
|
||||
};
|
||||
|
||||
function resolverInformacaoRota(pathname: string): RouteInfo {
|
||||
const rotaExata = routeInfoMap[pathname];
|
||||
|
||||
if (rotaExata) {
|
||||
return rotaExata;
|
||||
}
|
||||
|
||||
const rotaBase = Object.keys(routeInfoMap)
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.find((rota) => pathname.startsWith(`${rota}/`));
|
||||
|
||||
if (rotaBase) {
|
||||
const base = routeInfoMap[rotaBase];
|
||||
|
||||
if (pathname.endsWith('/novo')) {
|
||||
return {
|
||||
eyebrow: base.eyebrow,
|
||||
title: `Novo ${base.title.toLowerCase()}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (pathname.endsWith('/editar')) {
|
||||
return {
|
||||
eyebrow: base.eyebrow,
|
||||
title: `Editar ${base.title.toLowerCase()}`,
|
||||
};
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
return {
|
||||
eyebrow: 'Zendion Finance',
|
||||
title: 'Gestão financeira',
|
||||
};
|
||||
}
|
||||
|
||||
function obterIniciais(nome?: string | null) {
|
||||
const partes = String(nome || '')
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean);
|
||||
|
||||
if (partes.length === 0) {
|
||||
return 'U';
|
||||
}
|
||||
|
||||
if (partes.length === 1) {
|
||||
return partes[0].charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
return `${partes[0].charAt(0)}${partes[partes.length - 1].charAt(0)}`
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
export function Topbar({ onMenuClick }: TopbarProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const logout = useAuthStore((state) => state.logout);
|
||||
|
||||
const routeInfo = resolverInformacaoRota(location.pathname);
|
||||
const iniciais = obterIniciais(user?.nome);
|
||||
|
||||
function handleLogout() {
|
||||
logout();
|
||||
navigate('/login');
|
||||
navigate('/login', { replace: true });
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -31,60 +149,171 @@ export function Topbar({ onMenuClick }: TopbarProps) {
|
|||
position="sticky"
|
||||
elevation={0}
|
||||
sx={{
|
||||
backgroundColor: '#FFFFFF',
|
||||
zIndex: (theme) => theme.zIndex.drawer - 1,
|
||||
color: '#111827',
|
||||
backgroundColor: 'rgba(255,255,255,0.92)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderColor: 'rgba(15,23,42,0.08)',
|
||||
boxShadow: '0 8px 24px rgba(15,23,42,0.04)',
|
||||
}}
|
||||
>
|
||||
<Toolbar sx={{ minHeight: 72 }}>
|
||||
<IconButton
|
||||
onClick={onMenuClick}
|
||||
edge="start"
|
||||
sx={{
|
||||
display: { xs: 'inline-flex', md: 'none' },
|
||||
marginRight: 1,
|
||||
}}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Toolbar
|
||||
sx={{
|
||||
minHeight: { xs: 68, md: 76 },
|
||||
paddingX: { xs: 1.5, sm: 2.5, md: 3 },
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Abrir menu">
|
||||
<IconButton
|
||||
onClick={onMenuClick}
|
||||
edge="start"
|
||||
aria-label="Abrir menu"
|
||||
sx={{
|
||||
display: { xs: 'inline-flex', md: 'none' },
|
||||
marginRight: 1.25,
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 2.5,
|
||||
color: 'text.primary',
|
||||
backgroundColor: 'rgba(15,23,42,0.04)',
|
||||
border: '1px solid rgba(15,23,42,0.06)',
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Painel financeiro
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(59,130,246,0.08)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuRoundedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
display: 'block',
|
||||
color: 'text.secondary',
|
||||
fontWeight: 750,
|
||||
letterSpacing: '0.04em',
|
||||
textTransform: 'uppercase',
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{routeInfo.eyebrow}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" fontWeight={900}>
|
||||
Controle de movimentos
|
||||
<Typography
|
||||
variant="h6"
|
||||
fontWeight={950}
|
||||
noWrap
|
||||
sx={{
|
||||
marginTop: 0.25,
|
||||
fontSize: { xs: '1.02rem', sm: '1.18rem' },
|
||||
letterSpacing: '-0.02em',
|
||||
}}
|
||||
>
|
||||
{routeInfo.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||
<Avatar
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={{ xs: 0.75, sm: 1.25 }}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1.15}
|
||||
sx={{
|
||||
width: 38,
|
||||
height: 38,
|
||||
bgcolor: 'primary.main',
|
||||
fontWeight: 800,
|
||||
paddingY: 0.6,
|
||||
paddingLeft: 0.65,
|
||||
paddingRight: { xs: 0.65, sm: 1.4 },
|
||||
borderRadius: 3,
|
||||
border: '1px solid rgba(15,23,42,0.07)',
|
||||
backgroundColor: 'rgba(248,250,252,0.88)',
|
||||
}}
|
||||
>
|
||||
{user?.nome?.charAt(0)?.toUpperCase() || 'U'}
|
||||
</Avatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 900,
|
||||
color: '#FFFFFF',
|
||||
background:
|
||||
'linear-gradient(135deg, #3B82F6 0%, #2563EB 100%)',
|
||||
boxShadow: '0 8px 18px rgba(37,99,235,0.24)',
|
||||
}}
|
||||
>
|
||||
{iniciais}
|
||||
</Avatar>
|
||||
|
||||
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
|
||||
<Typography variant="body2" fontWeight={800}>
|
||||
{user?.nome || 'Usuário'}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Logado
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: { xs: 'none', sm: 'block' },
|
||||
minWidth: 0,
|
||||
maxWidth: 180,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={850}
|
||||
noWrap
|
||||
title={user?.nome || 'Usuário'}
|
||||
>
|
||||
{user?.nome || 'Usuário'}
|
||||
</Typography>
|
||||
|
||||
<IconButton onClick={handleLogout} title="Sair">
|
||||
<LogoutIcon />
|
||||
</IconButton>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
sx={{ display: 'block' }}
|
||||
>
|
||||
Sessão ativa
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Chip
|
||||
label="Online"
|
||||
size="small"
|
||||
color="success"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
display: { xs: 'none', lg: 'inline-flex' },
|
||||
height: 24,
|
||||
fontWeight: 750,
|
||||
fontSize: '0.68rem',
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Tooltip title="Sair da conta">
|
||||
<IconButton
|
||||
onClick={handleLogout}
|
||||
aria-label="Sair da conta"
|
||||
sx={{
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 2.5,
|
||||
color: 'text.secondary',
|
||||
border: '1px solid rgba(15,23,42,0.07)',
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
|
||||
'&:hover': {
|
||||
color: 'error.main',
|
||||
backgroundColor: 'rgba(239,68,68,0.07)',
|
||||
borderColor: 'rgba(239,68,68,0.18)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LogoutRoundedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Alert,
|
||||
|
|
@ -9,30 +10,42 @@ import {
|
|||
Chip,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Stack,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
|
||||
import AutoGraphRoundedIcon from '@mui/icons-material/AutoGraphRounded';
|
||||
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
|
||||
import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined';
|
||||
import TrendingUpRoundedIcon from '@mui/icons-material/TrendingUpRounded';
|
||||
import VisibilityOffRoundedIcon from '@mui/icons-material/VisibilityOffRounded';
|
||||
import VisibilityRoundedIcon from '@mui/icons-material/VisibilityRounded';
|
||||
import { loginRequest } from '../services/authService';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
|
||||
const APP_NAME = 'Zendion Finance';
|
||||
const APP_VERSION = '1.0.0';
|
||||
const LOGO_SRC = '/icon.png';
|
||||
|
||||
export function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const setAuth = useAuthStore((state) => state.setAuth);
|
||||
|
||||
const [usuario, setUsuario] = useState('');
|
||||
const [senha, setSenha] = useState('');
|
||||
const [mostrarSenha, setMostrarSenha] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [erro, setErro] = useState('');
|
||||
|
||||
async function handleLogin(event: React.FormEvent<HTMLFormElement>) {
|
||||
const formularioValido = Boolean(usuario.trim() && senha);
|
||||
|
||||
async function handleLogin(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!usuario.trim() || !senha.trim()) {
|
||||
if (!usuario.trim() || !senha) {
|
||||
setErro('Informe usuário e senha.');
|
||||
return;
|
||||
}
|
||||
|
|
@ -47,11 +60,11 @@ export function LoginPage() {
|
|||
});
|
||||
|
||||
setAuth(result.user, result.token);
|
||||
navigate('/dashboard');
|
||||
navigate('/dashboard', { replace: true });
|
||||
} catch (error: any) {
|
||||
const message =
|
||||
error?.response?.data?.message ||
|
||||
'Não foi possível realizar o login.';
|
||||
'Não foi possível realizar o login. Verifique seus dados e tente novamente.';
|
||||
|
||||
setErro(message);
|
||||
} finally {
|
||||
|
|
@ -65,217 +78,442 @@ export function LoginPage() {
|
|||
display="grid"
|
||||
gridTemplateColumns={{
|
||||
xs: '1fr',
|
||||
md: '1.1fr 0.9fr',
|
||||
md: 'minmax(480px, 1.08fr) minmax(420px, 0.92fr)',
|
||||
}}
|
||||
sx={{
|
||||
background:
|
||||
'radial-gradient(circle at top left, rgba(37,99,235,0.18), transparent 34%), linear-gradient(135deg, #F8FAFC 0%, #EEF2F7 48%, #E5E7EB 100%)',
|
||||
'radial-gradient(circle at top right, rgba(59,130,246,0.12), transparent 32%), linear-gradient(135deg, #F8FAFC 0%, #EEF2F7 48%, #E5E7EB 100%)',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="section"
|
||||
sx={{
|
||||
display: { xs: 'none', md: 'flex' },
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 6,
|
||||
background:
|
||||
'linear-gradient(160deg, #0F172A 0%, #111827 45%, #1E3A8A 100%)',
|
||||
color: '#fff',
|
||||
padding: { md: 5, lg: 7 },
|
||||
color: '#FFFFFF',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
'linear-gradient(155deg, #0F172A 0%, #111827 48%, #172554 100%)',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: 360,
|
||||
height: 360,
|
||||
width: 430,
|
||||
height: 430,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(96,165,250,0.18)',
|
||||
top: -120,
|
||||
right: -100,
|
||||
filter: 'blur(4px)',
|
||||
top: -170,
|
||||
right: -120,
|
||||
background:
|
||||
'radial-gradient(circle, rgba(59,130,246,0.30), rgba(59,130,246,0.02) 68%, transparent 72%)',
|
||||
filter: 'blur(2px)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: 260,
|
||||
height: 260,
|
||||
width: 310,
|
||||
height: 310,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(34,197,94,0.10)',
|
||||
bottom: -80,
|
||||
left: -70,
|
||||
filter: 'blur(4px)',
|
||||
bottom: -120,
|
||||
left: -90,
|
||||
background:
|
||||
'radial-gradient(circle, rgba(14,165,233,0.18), rgba(14,165,233,0.02) 65%, transparent 70%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box maxWidth={560} position="relative">
|
||||
<Stack direction="row" spacing={1.5} alignItems="center" marginBottom={5}>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
opacity: 0.28,
|
||||
backgroundImage:
|
||||
'linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px)',
|
||||
backgroundSize: '48px 48px',
|
||||
maskImage:
|
||||
'linear-gradient(to bottom, rgba(0,0,0,0.8), transparent 92%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box maxWidth={570} position="relative" zIndex={1}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1.5}
|
||||
alignItems="center"
|
||||
marginBottom={5.5}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 3,
|
||||
width: 58,
|
||||
height: 58,
|
||||
flexShrink: 0,
|
||||
borderRadius: 3.5,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
backgroundColor: 'rgba(255,255,255,0.10)',
|
||||
border: '1px solid rgba(255,255,255,0.14)',
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(59,130,246,0.30), rgba(37,99,235,0.12))',
|
||||
border: '1px solid rgba(147,197,253,0.24)',
|
||||
boxShadow: '0 16px 36px rgba(37,99,235,0.24)',
|
||||
}}
|
||||
>
|
||||
<AccountBalanceWalletIcon />
|
||||
<Box
|
||||
component="img"
|
||||
src={LOGO_SRC}
|
||||
alt="Zendion Finance"
|
||||
sx={{
|
||||
width: 46,
|
||||
height: 46,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="h5" fontWeight={900}>
|
||||
Zendion
|
||||
<Typography
|
||||
variant="h5"
|
||||
fontWeight={950}
|
||||
lineHeight={1.05}
|
||||
letterSpacing="-0.02em"
|
||||
>
|
||||
{APP_NAME}
|
||||
</Typography>
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.68)' }}>
|
||||
Financeiro
|
||||
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.62)' }}>
|
||||
Gestão financeira
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Chip
|
||||
label="Controle financeiro web"
|
||||
sx={{
|
||||
color: '#BFDBFE',
|
||||
borderColor: 'rgba(191,219,254,0.28)',
|
||||
backgroundColor: 'rgba(37,99,235,0.16)',
|
||||
marginBottom: 2,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
label="Controle financeiro inteligente"
|
||||
icon={<AutoGraphRoundedIcon />}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
marginBottom: 2.5,
|
||||
color: '#BFDBFE',
|
||||
borderColor: 'rgba(191,219,254,0.24)',
|
||||
backgroundColor: 'rgba(37,99,235,0.12)',
|
||||
fontWeight: 750,
|
||||
|
||||
'& .MuiChip-icon': {
|
||||
color: '#93C5FD',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography variant="h3" fontWeight={950} lineHeight={1.08} marginBottom={2}>
|
||||
Movimentos, carteiras e saldos em um painel limpo.
|
||||
<Typography
|
||||
variant="h3"
|
||||
fontWeight={950}
|
||||
lineHeight={1.06}
|
||||
letterSpacing="-0.035em"
|
||||
marginBottom={2.5}
|
||||
>
|
||||
Clareza para decidir.
|
||||
<br />
|
||||
Controle para evoluir.
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
color: 'rgba(255,255,255,0.72)',
|
||||
fontWeight: 400,
|
||||
maxWidth: 520,
|
||||
maxWidth: 540,
|
||||
marginBottom: 5,
|
||||
color: 'rgba(255,255,255,0.68)',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.55,
|
||||
}}
|
||||
>
|
||||
Registre entradas, saídas, sangrias e estornos direto do celular ou desktop, com segurança e praticidade.
|
||||
Acompanhe movimentos, carteiras, cartões, investimentos e
|
||||
simulações em um único ambiente.
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<ShieldOutlinedIcon sx={{ color: '#93C5FD' }} />
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.78)' }}>
|
||||
Acesso protegido por autenticação e token.
|
||||
</Typography>
|
||||
<Stack spacing={2.25}>
|
||||
<Stack direction="row" spacing={1.75} alignItems="center">
|
||||
<Box
|
||||
sx={{
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 2.5,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
backgroundColor: 'rgba(59,130,246,0.12)',
|
||||
border: '1px solid rgba(147,197,253,0.14)',
|
||||
}}
|
||||
>
|
||||
<ShieldOutlinedIcon
|
||||
sx={{ color: '#93C5FD', fontSize: 21 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography fontWeight={800}>
|
||||
Ambiente protegido
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: 'rgba(255,255,255,0.56)' }}
|
||||
>
|
||||
Acesso autenticado e dados separados por usuário.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<TrendingUpIcon sx={{ color: '#86EFAC' }} />
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.78)' }}>
|
||||
Base pronta para relatórios, dashboards e controle por carteiras.
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1.75} alignItems="center">
|
||||
<Box
|
||||
sx={{
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 2.5,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
backgroundColor: 'rgba(34,197,94,0.10)',
|
||||
border: '1px solid rgba(134,239,172,0.12)',
|
||||
}}
|
||||
>
|
||||
<TrendingUpRoundedIcon
|
||||
sx={{ color: '#86EFAC', fontSize: 21 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography fontWeight={800}>
|
||||
Visão financeira completa
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: 'rgba(255,255,255,0.56)' }}
|
||||
>
|
||||
Dashboards, relatórios e planejamento mensal.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
component="main"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
padding={{ xs: 2, sm: 3, md: 6 }}
|
||||
padding={{ xs: 2, sm: 3, md: 5, lg: 7 }}
|
||||
position="relative"
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: 440,
|
||||
boxShadow: '0 22px 70px rgba(15,23,42,0.12)',
|
||||
}}
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={450}
|
||||
spacing={2}
|
||||
>
|
||||
<CardContent sx={{ padding: { xs: 3, sm: 4 } }}>
|
||||
<Stack spacing={1} alignItems="center" textAlign="center" marginBottom={3}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 58,
|
||||
height: 58,
|
||||
borderRadius: 4,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
backgroundColor: 'rgba(21,101,192,0.10)',
|
||||
color: 'primary.main',
|
||||
marginBottom: 1,
|
||||
}}
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1.25}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
sx={{
|
||||
display: { xs: 'flex', md: 'none' },
|
||||
marginBottom: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={LOGO_SRC}
|
||||
alt="Zendion Finance"
|
||||
sx={{
|
||||
width: 46,
|
||||
height: 46,
|
||||
objectFit: 'contain',
|
||||
borderRadius: 2.5,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Typography fontWeight={950} lineHeight={1.05}>
|
||||
{APP_NAME}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Gestão financeira
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Card
|
||||
sx={{
|
||||
width: '100%',
|
||||
borderRadius: 5,
|
||||
border: '1px solid rgba(15,23,42,0.06)',
|
||||
boxShadow: '0 24px 70px rgba(15,23,42,0.12)',
|
||||
overflow: 'visible',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ padding: { xs: 3, sm: 4.5 } }}>
|
||||
<Stack
|
||||
spacing={1}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
marginBottom={3.5}
|
||||
>
|
||||
<LockOutlinedIcon />
|
||||
<Box
|
||||
sx={{
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 4,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
marginBottom: 1,
|
||||
color: 'primary.main',
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(59,130,246,0.14), rgba(37,99,235,0.05))',
|
||||
border: '1px solid rgba(59,130,246,0.10)',
|
||||
}}
|
||||
>
|
||||
<LockOutlinedIcon />
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
fontWeight={950}
|
||||
letterSpacing="-0.025em"
|
||||
>
|
||||
Bem-vindo
|
||||
</Typography>
|
||||
|
||||
<Typography color="text.secondary">
|
||||
Entre com suas credenciais para continuar.
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{erro && (
|
||||
<Alert
|
||||
severity="error"
|
||||
onClose={() => setErro('')}
|
||||
sx={{ marginBottom: 2.5, borderRadius: 2.5 }}
|
||||
>
|
||||
{erro}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleLogin}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
gap={2.25}
|
||||
>
|
||||
<TextField
|
||||
label="Usuário"
|
||||
value={usuario}
|
||||
onChange={(event) => {
|
||||
setUsuario(event.target.value);
|
||||
if (erro) setErro('');
|
||||
}}
|
||||
fullWidth
|
||||
autoFocus
|
||||
autoComplete="username"
|
||||
disabled={loading}
|
||||
inputProps={{
|
||||
maxLength: 100,
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Senha"
|
||||
type={mostrarSenha ? 'text' : 'password'}
|
||||
value={senha}
|
||||
onChange={(event) => {
|
||||
setSenha(event.target.value);
|
||||
if (erro) setErro('');
|
||||
}}
|
||||
fullWidth
|
||||
autoComplete="current-password"
|
||||
disabled={loading}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Tooltip
|
||||
title={mostrarSenha ? 'Ocultar senha' : 'Mostrar senha'}
|
||||
>
|
||||
<IconButton
|
||||
type="button"
|
||||
edge="end"
|
||||
onClick={() => setMostrarSenha((valor) => !valor)}
|
||||
aria-label={
|
||||
mostrarSenha
|
||||
? 'Ocultar senha'
|
||||
: 'Mostrar senha'
|
||||
}
|
||||
disabled={loading}
|
||||
>
|
||||
{mostrarSenha ? (
|
||||
<VisibilityOffRoundedIcon />
|
||||
) : (
|
||||
<VisibilityRoundedIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="large"
|
||||
fullWidth
|
||||
disabled={loading || !formularioValido}
|
||||
startIcon={
|
||||
loading
|
||||
? <CircularProgress size={18} color="inherit" />
|
||||
: <LockOutlinedIcon fontSize="small" />
|
||||
}
|
||||
sx={{
|
||||
minHeight: 52,
|
||||
marginTop: 0.5,
|
||||
borderRadius: 2.75,
|
||||
fontWeight: 850,
|
||||
boxShadow: '0 12px 28px rgba(37,99,235,0.24)',
|
||||
}}
|
||||
>
|
||||
{loading ? 'Entrando...' : 'Entrar no sistema'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h4" fontWeight={950}>
|
||||
Entrar
|
||||
</Typography>
|
||||
<Divider sx={{ marginY: 3 }} />
|
||||
|
||||
<Typography color="text.secondary">
|
||||
Acesse o Zendion Financeiro para continuar.
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{erro && (
|
||||
<Alert severity="error" sx={{ marginBottom: 2 }}>
|
||||
{erro}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleLogin}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
gap={2}
|
||||
>
|
||||
<TextField
|
||||
label="Usuário"
|
||||
value={usuario}
|
||||
onChange={(event) => setUsuario(event.target.value)}
|
||||
fullWidth
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Senha"
|
||||
type="password"
|
||||
value={senha}
|
||||
onChange={(event) => setSenha(event.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="large"
|
||||
fullWidth
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={18} color="inherit" /> : null}
|
||||
sx={{
|
||||
minHeight: 50,
|
||||
marginTop: 1,
|
||||
boxShadow: 3,
|
||||
}}
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
spacing={0.5}
|
||||
>
|
||||
{loading ? 'Entrando...' : 'Entrar no sistema'}
|
||||
</Button>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Sistema privado da Zendion INC.
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ marginY: 3 }} />
|
||||
<Typography variant="caption" color="text.disabled">
|
||||
Versão {APP_VERSION}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" textAlign="center">
|
||||
Sistema financeiro privado da Zendion INC.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.disabled"
|
||||
textAlign="center"
|
||||
>
|
||||
Use apenas credenciais autorizadas.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,18 @@ import {
|
|||
Chip,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
FormControlLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Stack,
|
||||
Switch,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import SavingsIcon from '@mui/icons-material/Savings';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
atualizarCentroCusto,
|
||||
|
|
@ -26,6 +29,7 @@ import {
|
|||
import type {
|
||||
CentroCusto,
|
||||
CriarCentroCustoRequest,
|
||||
FlagNumerica,
|
||||
} from '../types/centroCustoTypes';
|
||||
|
||||
type CentroCustoFormProps = {
|
||||
|
|
@ -40,7 +44,11 @@ type FormSectionProps = {
|
|||
};
|
||||
|
||||
function formatarValorResumo(value: string) {
|
||||
const numero = Number(value || 0);
|
||||
const numero = Number(value);
|
||||
|
||||
if (!value || !Number.isFinite(numero)) {
|
||||
return 'Não informado';
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
|
|
@ -101,7 +109,6 @@ const fieldSx = {
|
|||
|
||||
export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isEdit = mode === 'edit';
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -109,44 +116,67 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
|||
const [sucesso, setSucesso] = useState('');
|
||||
|
||||
const [descricao, setDescricao] = useState('');
|
||||
const [limite, setLimite] = useState('0');
|
||||
const [simular, setSimular] = useState('0');
|
||||
const [investimento, setInvestimento] = useState('0');
|
||||
const [habilitado, setHabilitado] = useState('1');
|
||||
const [investimento, setInvestimento] = useState<FlagNumerica>(0);
|
||||
const [habilitado, setHabilitado] = useState<FlagNumerica>(1);
|
||||
|
||||
const [simular, setSimular] = useState<FlagNumerica>(0);
|
||||
const [limite, setLimite] = useState('');
|
||||
|
||||
const titulo = isEdit ? 'Editar centro de custo' : 'Novo centro de custo';
|
||||
|
||||
const subtitulo = isEdit
|
||||
? 'Atualize os dados do centro de custo selecionado.'
|
||||
: 'Cadastre categorias financeiras para classificar os movimentos.';
|
||||
? 'Atualize os dados globais e a configuração da sua simulação.'
|
||||
: 'Cadastre uma categoria financeira e, se desejar, inclua-a na sua simulação.';
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialData) return;
|
||||
|
||||
setDescricao(initialData.descricao || '');
|
||||
setLimite(String(initialData.limite ?? 0));
|
||||
setSimular(String(initialData.simular ?? 0));
|
||||
setInvestimento(String(initialData.investimento ?? 0));
|
||||
setHabilitado(String(initialData.habilitado ?? 1));
|
||||
setInvestimento(Number(initialData.investimento) === 1 ? 1 : 0);
|
||||
setHabilitado(Number(initialData.habilitado) === 1 ? 1 : 0);
|
||||
setSimular(Number(initialData.simular) === 1 ? 1 : 0);
|
||||
setLimite(
|
||||
initialData.limite === null || initialData.limite === undefined
|
||||
? ''
|
||||
: String(initialData.limite)
|
||||
);
|
||||
}, [initialData]);
|
||||
|
||||
function limparFormulario() {
|
||||
setDescricao('');
|
||||
setLimite('0');
|
||||
setSimular('0');
|
||||
setInvestimento('0');
|
||||
setHabilitado('1');
|
||||
setInvestimento(0);
|
||||
setHabilitado(1);
|
||||
setSimular(0);
|
||||
setLimite('');
|
||||
}
|
||||
|
||||
function validarFormulario() {
|
||||
if (!descricao.trim()) {
|
||||
return 'Informe a descrição.';
|
||||
}
|
||||
|
||||
if (simular === 1) {
|
||||
if (limite === '') {
|
||||
return 'Informe o limite da simulação.';
|
||||
}
|
||||
|
||||
const limiteNumero = Number(limite);
|
||||
|
||||
if (!Number.isFinite(limiteNumero) || limiteNumero < 0) {
|
||||
return 'Informe um limite válido para a simulação.';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!descricao.trim()) {
|
||||
setErro('Informe a descrição.');
|
||||
return;
|
||||
}
|
||||
const erroValidacao = validarFormulario();
|
||||
|
||||
if (limite === '' || !Number.isFinite(Number(limite))) {
|
||||
setErro('Informe um limite válido.');
|
||||
if (erroValidacao) {
|
||||
setErro(erroValidacao);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -157,10 +187,10 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
|||
|
||||
const payload: CriarCentroCustoRequest = {
|
||||
descricao: descricao.trim(),
|
||||
limite: Number(limite || 0),
|
||||
simular: Number(simular || 0),
|
||||
investimento: Number(investimento || 0),
|
||||
habilitado: Number(habilitado || 1),
|
||||
investimento,
|
||||
habilitado,
|
||||
simular,
|
||||
limite: simular === 1 ? Number(limite) : null,
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
|
|
@ -169,7 +199,11 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
|||
return;
|
||||
}
|
||||
|
||||
await atualizarCentroCusto(initialData.idcentrodecustos, payload);
|
||||
await atualizarCentroCusto(
|
||||
initialData.idcentrodecustos,
|
||||
payload
|
||||
);
|
||||
|
||||
setSucesso('Centro de custo atualizado com sucesso.');
|
||||
} else {
|
||||
await criarCentroCusto(payload);
|
||||
|
|
@ -256,7 +290,7 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
|||
>
|
||||
<FormSection
|
||||
title="Dados do centro"
|
||||
description="Defina descrição, limite e comportamento do centro de custo."
|
||||
description="Esses dados pertencem ao cadastro global do centro de custo."
|
||||
>
|
||||
<TextField
|
||||
label="Descrição"
|
||||
|
|
@ -264,65 +298,111 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
|||
onChange={(event) => setDescricao(event.target.value)}
|
||||
placeholder="Ex: Administrativo, Comercial, Impostos..."
|
||||
fullWidth
|
||||
required
|
||||
sx={fieldSx}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Limite"
|
||||
type="number"
|
||||
value={limite}
|
||||
onChange={(event) => setLimite(event.target.value)}
|
||||
inputProps={{
|
||||
step: '0.01',
|
||||
}}
|
||||
fullWidth
|
||||
sx={fieldSx}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
select
|
||||
label="Simular"
|
||||
value={simular}
|
||||
onChange={(event) => setSimular(event.target.value)}
|
||||
fullWidth
|
||||
sx={fieldSx}
|
||||
>
|
||||
<MenuItem value="1">Sim</MenuItem>
|
||||
<MenuItem value="0">Não</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
label="Investimento"
|
||||
value={investimento}
|
||||
onChange={(event) => setInvestimento(event.target.value)}
|
||||
onChange={(event) =>
|
||||
setInvestimento(Number(event.target.value) === 1 ? 1 : 0)
|
||||
}
|
||||
fullWidth
|
||||
sx={fieldSx}
|
||||
>
|
||||
<MenuItem value="1">Sim</MenuItem>
|
||||
<MenuItem value="0">Não</MenuItem>
|
||||
<MenuItem value={1}>Sim</MenuItem>
|
||||
<MenuItem value={0}>Não</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
label="Status"
|
||||
value={habilitado}
|
||||
onChange={(event) => setHabilitado(event.target.value)}
|
||||
onChange={(event) =>
|
||||
setHabilitado(Number(event.target.value) === 1 ? 1 : 0)
|
||||
}
|
||||
fullWidth
|
||||
sx={fieldSx}
|
||||
>
|
||||
<MenuItem value="1">Habilitado</MenuItem>
|
||||
<MenuItem value="0">Desabilitado</MenuItem>
|
||||
<MenuItem value={1}>Habilitado</MenuItem>
|
||||
<MenuItem value={0}>Desabilitado</MenuItem>
|
||||
</TextField>
|
||||
</FormSection>
|
||||
|
||||
<FormSection
|
||||
title="Simulação do usuário"
|
||||
description="Defina se este centro participa da sua simulação e qual será o limite previsto."
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
gridColumn: { md: '1 / -1' },
|
||||
padding: 2,
|
||||
borderRadius: 3,
|
||||
border: '1px solid',
|
||||
borderColor: simular === 1 ? 'success.light' : 'divider',
|
||||
backgroundColor:
|
||||
simular === 1
|
||||
? 'rgba(46, 125, 50, 0.05)'
|
||||
: 'rgba(15, 23, 42, 0.02)',
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={simular === 1}
|
||||
onChange={(event) =>
|
||||
setSimular(event.target.checked ? 1 : 0)
|
||||
}
|
||||
color="success"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box>
|
||||
<Typography fontWeight={850}>
|
||||
Incluir na minha simulação
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Ao desativar, o vínculo deste centro com a sua
|
||||
simulação será removido.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
sx={{
|
||||
margin: 0,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
label="Limite da simulação"
|
||||
type="number"
|
||||
value={limite}
|
||||
onChange={(event) => setLimite(event.target.value)}
|
||||
disabled={simular === 0}
|
||||
required={simular === 1}
|
||||
inputProps={{
|
||||
step: '0.01',
|
||||
min: '0',
|
||||
}}
|
||||
helperText={
|
||||
simular === 1
|
||||
? 'Valor previsto para este centro na sua simulação.'
|
||||
: 'Ative a simulação para informar um limite.'
|
||||
}
|
||||
fullWidth
|
||||
sx={fieldSx}
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
<Stack
|
||||
direction={{ xs: 'column-reverse', sm: 'row' }}
|
||||
justifyContent="flex-end"
|
||||
spacing={1.5}
|
||||
sx={{
|
||||
paddingTop: 1,
|
||||
}}
|
||||
sx={{ paddingTop: 1 }}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
|
|
@ -378,7 +458,7 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
|||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" marginBottom={2.5}>
|
||||
Prévia rápida do centro antes de salvar.
|
||||
Prévia rápida antes de salvar.
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ marginBottom: 2.5 }} />
|
||||
|
|
@ -388,6 +468,7 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
|||
<Typography variant="caption" color="text.secondary">
|
||||
Descrição
|
||||
</Typography>
|
||||
|
||||
<Typography fontWeight={800}>
|
||||
{descricao || 'Sem descrição'}
|
||||
</Typography>
|
||||
|
|
@ -395,28 +476,11 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
|||
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Limite
|
||||
Tipo
|
||||
</Typography>
|
||||
<Typography variant="h4" fontWeight={950}>
|
||||
{formatarValorResumo(limite)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Simular
|
||||
</Typography>
|
||||
<Typography fontWeight={800}>
|
||||
{Number(simular) === 1 ? 'Sim' : 'Não'}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Investimento
|
||||
</Typography>
|
||||
<Typography fontWeight={800}>
|
||||
{Number(investimento) === 1 ? 'Sim' : 'Não'}
|
||||
{investimento === 1 ? 'Investimento' : 'Operacional'}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
|
|
@ -424,16 +488,65 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
|||
<Typography variant="caption" color="text.secondary">
|
||||
Status
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
fontWeight={800}
|
||||
color={Number(habilitado) === 1 ? 'success.main' : 'text.secondary'}
|
||||
color={
|
||||
habilitado === 1
|
||||
? 'success.main'
|
||||
: 'text.secondary'
|
||||
}
|
||||
>
|
||||
{Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado'}
|
||||
{habilitado === 1 ? 'Habilitado' : 'Desabilitado'}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<SavingsIcon
|
||||
color={simular === 1 ? 'success' : 'disabled'}
|
||||
fontSize="small"
|
||||
/>
|
||||
|
||||
<Typography fontWeight={900}>
|
||||
Simulação
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Participação
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
fontWeight={800}
|
||||
color={
|
||||
simular === 1
|
||||
? 'success.main'
|
||||
: 'text.secondary'
|
||||
}
|
||||
>
|
||||
{simular === 1
|
||||
? 'Incluído na simulação'
|
||||
: 'Fora da simulação'}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{simular === 1 && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Limite previsto
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h4" fontWeight={950}>
|
||||
{formatarValorResumo(limite)}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,11 +48,24 @@ import type {
|
|||
|
||||
const LIMITE_PADRAO = 20;
|
||||
|
||||
function formatarValor(valor: number) {
|
||||
type FiltrosCentrosCusto = {
|
||||
busca: string;
|
||||
simular: 0 | 1 | '';
|
||||
investimento: 0 | 1 | '';
|
||||
habilitado: 0 | 1 | '';
|
||||
orderBy: string;
|
||||
orderDirection: 'ASC' | 'DESC';
|
||||
};
|
||||
|
||||
function formatarValor(valor: number | null | undefined) {
|
||||
if (valor === null || valor === undefined) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL',
|
||||
}).format(Number(valor || 0));
|
||||
}).format(Number(valor));
|
||||
}
|
||||
|
||||
function formatarData(data: string | null) {
|
||||
|
|
@ -119,7 +132,10 @@ export function CentrosCustoPage() {
|
|||
return count;
|
||||
}, [busca, simular, investimento, habilitado]);
|
||||
|
||||
async function carregarCentrosCusto(pageToLoad = page) {
|
||||
async function carregarCentrosCusto(
|
||||
pageToLoad = page,
|
||||
filtros?: Partial<FiltrosCentrosCusto>
|
||||
) {
|
||||
try {
|
||||
setLoading(true);
|
||||
setErro('');
|
||||
|
|
@ -127,12 +143,30 @@ export function CentrosCustoPage() {
|
|||
const params: ListarCentrosCustoParams = {
|
||||
limite: LIMITE_PADRAO,
|
||||
page: pageToLoad,
|
||||
busca: busca.trim() || undefined,
|
||||
simular,
|
||||
investimento,
|
||||
habilitado,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
busca:
|
||||
filtros?.busca !== undefined
|
||||
? filtros.busca.trim() || undefined
|
||||
: busca.trim() || undefined,
|
||||
simular:
|
||||
filtros?.simular !== undefined
|
||||
? filtros.simular
|
||||
: simular,
|
||||
investimento:
|
||||
filtros?.investimento !== undefined
|
||||
? filtros.investimento
|
||||
: investimento,
|
||||
habilitado:
|
||||
filtros?.habilitado !== undefined
|
||||
? filtros.habilitado
|
||||
: habilitado,
|
||||
orderBy:
|
||||
filtros?.orderBy !== undefined
|
||||
? filtros.orderBy
|
||||
: orderBy,
|
||||
orderDirection:
|
||||
filtros?.orderDirection !== undefined
|
||||
? filtros.orderDirection
|
||||
: orderDirection,
|
||||
};
|
||||
|
||||
const response = await listarCentrosCusto(params);
|
||||
|
|
@ -158,17 +192,24 @@ export function CentrosCustoPage() {
|
|||
}
|
||||
|
||||
function limparFiltros() {
|
||||
setBusca('');
|
||||
setSimular('');
|
||||
setInvestimento('');
|
||||
setHabilitado('');
|
||||
setOrderBy('descricao');
|
||||
setOrderDirection('ASC');
|
||||
const filtrosLimpos = {
|
||||
busca: '',
|
||||
simular: '' as const,
|
||||
investimento: '' as const,
|
||||
habilitado: '' as const,
|
||||
orderBy: 'descricao',
|
||||
orderDirection: 'ASC' as const,
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
setPage(1);
|
||||
carregarCentrosCusto(1);
|
||||
}, 0);
|
||||
setBusca(filtrosLimpos.busca);
|
||||
setSimular(filtrosLimpos.simular);
|
||||
setInvestimento(filtrosLimpos.investimento);
|
||||
setHabilitado(filtrosLimpos.habilitado);
|
||||
setOrderBy(filtrosLimpos.orderBy);
|
||||
setOrderDirection(filtrosLimpos.orderDirection);
|
||||
setPage(1);
|
||||
|
||||
carregarCentrosCusto(1, filtrosLimpos);
|
||||
}
|
||||
|
||||
async function handleDeletarCentroCusto(item: CentroCusto) {
|
||||
|
|
@ -199,31 +240,26 @@ export function CentrosCustoPage() {
|
|||
}
|
||||
|
||||
async function handleAlterarStatusCentroCusto(item: CentroCusto) {
|
||||
const novoStatus = Number(item.habilitado) === 1 ? 0 : 1;
|
||||
const novoStatus: 0 | 1 =
|
||||
Number(item.habilitado) === 1 ? 0 : 1;
|
||||
|
||||
try {
|
||||
setAlterandoStatusId(item.idcentrodecustos);
|
||||
setErro('');
|
||||
setSucesso('');
|
||||
|
||||
const centroAtualizado = await alterarHabilitadoCentroCusto(
|
||||
await alterarHabilitadoCentroCusto(
|
||||
item.idcentrodecustos,
|
||||
novoStatus
|
||||
);
|
||||
|
||||
setCentros((listaAtual) =>
|
||||
listaAtual.map((centro) =>
|
||||
centro.idcentrodecustos === item.idcentrodecustos
|
||||
? centroAtualizado
|
||||
: centro
|
||||
)
|
||||
);
|
||||
|
||||
setSucesso(
|
||||
novoStatus === 1
|
||||
? 'Centro de custo habilitado com sucesso.'
|
||||
: 'Centro de custo desabilitado com sucesso.'
|
||||
);
|
||||
|
||||
await carregarCentrosCusto(page);
|
||||
} catch (error: any) {
|
||||
const message =
|
||||
error?.response?.data?.message ||
|
||||
|
|
@ -307,7 +343,7 @@ export function CentrosCustoPage() {
|
|||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Centros encontrados
|
||||
Centros de custo
|
||||
</Typography>
|
||||
<Typography variant="h5" fontWeight={950}>
|
||||
{summary.quantidade}
|
||||
|
|
@ -318,8 +354,9 @@ export function CentrosCustoPage() {
|
|||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Limite total
|
||||
Limite simulado
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" fontWeight={950}>
|
||||
{formatarValor(summary.limiteTotal)}
|
||||
</Typography>
|
||||
|
|
@ -340,7 +377,7 @@ export function CentrosCustoPage() {
|
|||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Simuláveis
|
||||
Na simulação
|
||||
</Typography>
|
||||
<Typography variant="h5" fontWeight={950} color="success.main">
|
||||
{summary.simulaveis}
|
||||
|
|
@ -388,10 +425,14 @@ export function CentrosCustoPage() {
|
|||
|
||||
<TextField
|
||||
select
|
||||
label="Simular"
|
||||
label="Participa da simulação"
|
||||
value={simular}
|
||||
onChange={(event) =>
|
||||
setSimular(event.target.value === '' ? '' : Number(event.target.value))
|
||||
setSimular(
|
||||
event.target.value === ''
|
||||
? ''
|
||||
: Number(event.target.value) as 0 | 1
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
|
|
@ -529,16 +570,30 @@ export function CentrosCustoPage() {
|
|||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography fontWeight={950} whiteSpace="nowrap">
|
||||
{formatarValor(item.limite)}
|
||||
</Typography>
|
||||
{Number(item.simular) === 1 ? (
|
||||
<Typography fontWeight={950} whiteSpace="nowrap">
|
||||
{formatarValor(item.limite)}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
Fora da simulação
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" flexWrap="wrap" spacing={1} useFlexGap>
|
||||
<Chip
|
||||
label={flagLabel(item.simular, 'Simular', 'Não simular')}
|
||||
label={
|
||||
Number(item.simular) === 1
|
||||
? 'Na simulação'
|
||||
: 'Fora da simulação'
|
||||
}
|
||||
size="small"
|
||||
color={flagColor(item.simular) as any}
|
||||
color={Number(item.simular) === 1 ? 'success' : 'default'}
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
|
|
@ -655,16 +710,22 @@ export function CentrosCustoPage() {
|
|||
</TableCell>
|
||||
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight={950}>
|
||||
{formatarValor(item.limite)}
|
||||
</Typography>
|
||||
{Number(item.simular) === 1 ? (
|
||||
<Typography fontWeight={950}>
|
||||
{formatarValor(item.limite)}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography color="text.secondary">
|
||||
-
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={Number(item.simular) === 1 ? 'Sim' : 'Não'}
|
||||
size="small"
|
||||
color={flagColor(item.simular) as any}
|
||||
color={Number(item.simular) === 1 ? 'success' : 'default'}
|
||||
variant="outlined"
|
||||
/>
|
||||
</TableCell>
|
||||
|
|
|
|||
|
|
@ -5,10 +5,20 @@ import type {
|
|||
CentroCusto,
|
||||
CentrosCustoListResponse,
|
||||
CriarCentroCustoRequest,
|
||||
FlagNumerica,
|
||||
} from '../types/centroCustoTypes';
|
||||
|
||||
export type OrderDirection = 'ASC' | 'DESC';
|
||||
|
||||
export type CentroCustoOrderBy =
|
||||
| 'descricao'
|
||||
| 'limite'
|
||||
| 'simular'
|
||||
| 'investimento'
|
||||
| 'habilitado'
|
||||
| 'insert_date'
|
||||
| 'update_date';
|
||||
|
||||
export type ListarCentrosCustoParams = {
|
||||
limite?: number;
|
||||
offset?: number;
|
||||
|
|
@ -17,29 +27,39 @@ export type ListarCentrosCustoParams = {
|
|||
simular?: number | '';
|
||||
investimento?: number | '';
|
||||
habilitado?: number | '';
|
||||
orderBy?: string;
|
||||
orderBy?: CentroCustoOrderBy;
|
||||
orderDirection?: OrderDirection;
|
||||
};
|
||||
|
||||
export async function listarCentrosCusto(
|
||||
params?: ListarCentrosCustoParams
|
||||
): Promise<CentrosCustoListResponse> {
|
||||
const response = await api.get<CentrosCustoListResponse>('/centros-custo', {
|
||||
params,
|
||||
});
|
||||
const response = await api.get<CentrosCustoListResponse>(
|
||||
'/centros-custo',
|
||||
{ params }
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function buscarCentroCustoPorId(id: number): Promise<CentroCusto> {
|
||||
const response = await api.get<ApiResponse<CentroCusto>>(`/centros-custo/${id}`);
|
||||
export async function buscarCentroCustoPorId(
|
||||
id: number
|
||||
): Promise<CentroCusto> {
|
||||
const response = await api.get<ApiResponse<CentroCusto>>(
|
||||
`/centros-custo/${id}`
|
||||
);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function criarCentroCusto(
|
||||
data: CriarCentroCustoRequest
|
||||
): Promise<CentroCusto> {
|
||||
const response = await api.post<ApiResponse<CentroCusto>>('/centros-custo', data);
|
||||
const response = await api.post<ApiResponse<CentroCusto>>(
|
||||
'/centros-custo',
|
||||
data
|
||||
);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
|
|
@ -47,13 +67,17 @@ export async function atualizarCentroCusto(
|
|||
id: number,
|
||||
data: AtualizarCentroCustoRequest
|
||||
): Promise<CentroCusto> {
|
||||
const response = await api.put<ApiResponse<CentroCusto>>(`/centros-custo/${id}`, data);
|
||||
const response = await api.put<ApiResponse<CentroCusto>>(
|
||||
`/centros-custo/${id}`,
|
||||
data
|
||||
);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function alterarHabilitadoCentroCusto(
|
||||
id: number,
|
||||
habilitado: number
|
||||
habilitado: FlagNumerica
|
||||
): Promise<CentroCusto> {
|
||||
const response = await api.patch<ApiResponse<CentroCusto>>(
|
||||
`/centros-custo/${id}/habilitado`,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,36 @@
|
|||
export type FlagNumerica = 0 | 1;
|
||||
|
||||
export type CentroCusto = {
|
||||
idcentrodecustos: number;
|
||||
descricao: string;
|
||||
limite: number;
|
||||
simular: number;
|
||||
investimento: number;
|
||||
habilitado: number;
|
||||
|
||||
/**
|
||||
* Dados globais do centro de custo.
|
||||
*/
|
||||
investimento: FlagNumerica;
|
||||
habilitado: FlagNumerica;
|
||||
insert_date: string | null;
|
||||
update_date: string | null;
|
||||
|
||||
/**
|
||||
* Dados da simulação do usuário logado.
|
||||
* Não são mais colunas diretas de centrodecustos.
|
||||
*/
|
||||
simular: FlagNumerica;
|
||||
limite: number | null;
|
||||
};
|
||||
|
||||
export type CriarCentroCustoRequest = {
|
||||
descricao: string;
|
||||
limite: number;
|
||||
simular: number;
|
||||
investimento: number;
|
||||
habilitado: number;
|
||||
investimento: FlagNumerica;
|
||||
habilitado: FlagNumerica;
|
||||
|
||||
/**
|
||||
* Quando simular = 1, limite deve ser informado.
|
||||
* Quando simular = 0, o vínculo será removido ou não será criado.
|
||||
*/
|
||||
simular: FlagNumerica;
|
||||
limite: number | null;
|
||||
};
|
||||
|
||||
export type AtualizarCentroCustoRequest = CriarCentroCustoRequest;
|
||||
|
|
@ -29,10 +45,19 @@ export type CentrosCustoPagination = {
|
|||
|
||||
export type CentrosCustoSummary = {
|
||||
quantidade: number;
|
||||
|
||||
/**
|
||||
* Soma dos limites configurados pelo usuário logado.
|
||||
*/
|
||||
limiteTotal: number;
|
||||
|
||||
habilitados: number;
|
||||
desabilitados: number;
|
||||
investimentos: number;
|
||||
|
||||
/**
|
||||
* Quantidade de centros vinculados à simulação do usuário.
|
||||
*/
|
||||
simulaveis: number;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ import type {
|
|||
MovimentosSummary,
|
||||
} from '../types/movimentoTypes';
|
||||
import {
|
||||
listarBancos,
|
||||
listarMeusBancos,
|
||||
listarCentrosCusto,
|
||||
listarClientes,
|
||||
} from '../../referencias/services/referenciasService';
|
||||
|
|
@ -559,7 +559,7 @@ export function MovimentosPage() {
|
|||
setLoadingRefs(true);
|
||||
|
||||
const [bancosData, centrosData, clientesData] = await Promise.all([
|
||||
listarBancos(),
|
||||
listarMeusBancos(),
|
||||
listarCentrosCusto(),
|
||||
listarClientes(),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ function origemLabel(origem: string) {
|
|||
movimento_fixo_previsto: 'Fixo previsto',
|
||||
saldo_banco: 'Saldo banco',
|
||||
cartao_credito_agrupado: 'Cartão',
|
||||
limite_centro_custo: 'Limite centro',
|
||||
limite_centro_custo: 'Previsão do centro',
|
||||
manual: 'Manual',
|
||||
};
|
||||
|
||||
|
|
@ -355,6 +355,31 @@ function SimulacaoLista({
|
|||
/>
|
||||
)}
|
||||
|
||||
{item.origem === 'limite_centro_custo' && item.detalhes && (
|
||||
<>
|
||||
<Chip
|
||||
size="small"
|
||||
label={`Limite ${formatarValor(item.detalhes.limite)}`}
|
||||
color="info"
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<Chip
|
||||
size="small"
|
||||
label={`Já lançado ${formatarValor(item.detalhes.gastoAtual)}`}
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<Chip
|
||||
size="small"
|
||||
label={`Restante ${formatarValor(item.detalhes.restante)}`}
|
||||
color="success"
|
||||
variant="outlined"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{item.cliente_nome && (
|
||||
<Chip
|
||||
size="small"
|
||||
|
|
@ -415,7 +440,7 @@ export function SimulacaoMensalPage() {
|
|||
setBase(data);
|
||||
setEntradas(data.entradas || []);
|
||||
setSaidas(data.saidas || []);
|
||||
setSucesso('Previsão preenchida com sucesso.');
|
||||
setSucesso('Simulação mensal montada com sucesso.');
|
||||
} catch (error: any) {
|
||||
const message =
|
||||
error?.response?.data?.message ||
|
||||
|
|
@ -719,7 +744,10 @@ export function SimulacaoMensalPage() {
|
|||
<Chip label={`${base.fontes.movimentosExistentes} movimentos existentes`} variant="outlined" />
|
||||
<Chip label={`${base.fontes.movimentosFixos} fixos analisados`} variant="outlined" />
|
||||
<Chip label={`${base.fontes.bancos} bancos`} variant="outlined" />
|
||||
<Chip label={`${base.fontes.centrosSimulaveis} centros simuláveis`} variant="outlined" />
|
||||
<Chip
|
||||
label={`${base.fontes.centrosSimulaveis} centros configurados na simulação`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { api } from '../../../services/api';
|
||||
import type {
|
||||
ApiResponse,
|
||||
BuscarBaseSimulacaoMensalParams,
|
||||
SimulacaoMensalBase,
|
||||
} from '../types/simulacaoMensalTypes';
|
||||
|
|
@ -7,14 +8,18 @@ import type {
|
|||
export async function buscarBaseSimulacaoMensal(
|
||||
params: BuscarBaseSimulacaoMensalParams
|
||||
): Promise<SimulacaoMensalBase> {
|
||||
const response = await api.get('/simulacao-mensal/base', {
|
||||
params: {
|
||||
ano: params.ano,
|
||||
mes: params.mes,
|
||||
incluirQuitadas: params.incluirQuitadas ? 1 : 0,
|
||||
incluirFixosVencidosDoMesAtual: params.incluirFixosVencidosDoMesAtual ? 1 : 0,
|
||||
},
|
||||
});
|
||||
const response = await api.get<ApiResponse<SimulacaoMensalBase>>(
|
||||
'/simulacao-mensal/base',
|
||||
{
|
||||
params: {
|
||||
ano: params.ano,
|
||||
mes: params.mes,
|
||||
incluirQuitadas: params.incluirQuitadas ? 1 : 0,
|
||||
incluirFixosVencidosDoMesAtual:
|
||||
params.incluirFixosVencidosDoMesAtual ? 1 : 0,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,34 @@ export type SimulacaoMensalOrigemItem =
|
|||
| 'limite_centro_custo'
|
||||
| 'manual';
|
||||
|
||||
export type SimulacaoMensalDetalhesLimiteCentro = {
|
||||
limite: number;
|
||||
gastoAtual: number;
|
||||
restante: number;
|
||||
};
|
||||
|
||||
export type SimulacaoMensalDetalhesCartao = {
|
||||
quantidade_movimentos: number;
|
||||
ids: number[];
|
||||
};
|
||||
|
||||
export type SimulacaoMensalDetalhesMovimento = {
|
||||
parcela?: number | null;
|
||||
parcelas?: number | null;
|
||||
origem_movimento?: string | null;
|
||||
};
|
||||
|
||||
export type SimulacaoMensalDetalhes =
|
||||
| SimulacaoMensalDetalhesLimiteCentro
|
||||
| SimulacaoMensalDetalhesCartao
|
||||
| SimulacaoMensalDetalhesMovimento
|
||||
| Record<string, unknown>
|
||||
| null;
|
||||
|
||||
export type SimulacaoMensalItem = {
|
||||
id: string;
|
||||
tipo: SimulacaoMensalTipoItem;
|
||||
origem: SimulacaoMensalOrigemItem | string;
|
||||
origem: SimulacaoMensalOrigemItem;
|
||||
|
||||
descricao: string;
|
||||
valor: number;
|
||||
|
|
@ -38,32 +62,37 @@ export type SimulacaoMensalItem = {
|
|||
editavel: boolean;
|
||||
editado: boolean;
|
||||
|
||||
detalhes?: any;
|
||||
detalhes?: SimulacaoMensalDetalhes;
|
||||
};
|
||||
|
||||
export type SimulacaoMensalResumo = {
|
||||
totalEntradas: number;
|
||||
totalSaidas: number;
|
||||
totalGastos?: number;
|
||||
totalInvestimentos?: number;
|
||||
totalGastos: number;
|
||||
totalInvestimentos: number;
|
||||
resultado: number;
|
||||
|
||||
percentualEntradas?: number | null;
|
||||
percentualSaidas?: number | null;
|
||||
percentualGastos?: number | null;
|
||||
percentualInvestimentos?: number | null;
|
||||
percentualResultado?: number | null;
|
||||
percentualEntradas: number | null;
|
||||
percentualSaidas: number | null;
|
||||
percentualGastos: number | null;
|
||||
percentualInvestimentos: number | null;
|
||||
percentualResultado: number | null;
|
||||
percentualComprometido: number | null;
|
||||
|
||||
quantidadeEntradas: number;
|
||||
quantidadeSaidas: number;
|
||||
quantidadeInvestimentos?: number;
|
||||
quantidadeInvestimentos: number;
|
||||
};
|
||||
|
||||
export type SimulacaoMensalAviso = {
|
||||
tipo: string;
|
||||
mensagem: string;
|
||||
[key: string]: any;
|
||||
idcentrodecustos?: number;
|
||||
centro_custo_descricao?: string;
|
||||
limite?: number;
|
||||
gastoAtual?: number;
|
||||
excesso?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type SimulacaoMensalBase = {
|
||||
|
|
@ -98,4 +127,10 @@ export type BuscarBaseSimulacaoMensalParams = {
|
|||
mes: number;
|
||||
incluirQuitadas?: boolean;
|
||||
incluirFixosVencidosDoMesAtual?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type ApiResponse<T> = {
|
||||
ok: boolean;
|
||||
message?: string;
|
||||
data: T;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue