ajustes em simulacoes por usuario
This commit is contained in:
parent
09ef64f491
commit
180d756195
|
|
@ -1,40 +1,72 @@
|
||||||
const centrosCustoService = require('../services/centrosCusto.service');
|
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) {
|
function validarDadosCentroCusto(dados) {
|
||||||
if (!dados.descricao || !String(dados.descricao).trim()) {
|
if (!dados.descricao || !String(dados.descricao).trim()) {
|
||||||
return 'Descrição é obrigatória.';
|
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);
|
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.';
|
return 'Limite inválido.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dados.simular !== undefined && dados.simular !== null && dados.simular !== '') {
|
const investimento = normalizarFlagEntrada(dados.investimento);
|
||||||
const simular = Number(dados.simular);
|
|
||||||
|
|
||||||
if (![0, 1].includes(simular)) {
|
if (investimento !== undefined && ![0, 1].includes(investimento)) {
|
||||||
return 'Campo simular inválido.';
|
return 'Campo investimento inválido.';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dados.investimento !== undefined && dados.investimento !== null && dados.investimento !== '') {
|
const habilitado = normalizarFlagEntrada(dados.habilitado);
|
||||||
const investimento = Number(dados.investimento);
|
|
||||||
|
|
||||||
if (![0, 1].includes(investimento)) {
|
if (habilitado !== undefined && ![0, 1].includes(habilitado)) {
|
||||||
return 'Campo investimento inválido.';
|
return 'Status 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.';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -42,7 +74,17 @@ function validarDadosCentroCusto(dados) {
|
||||||
|
|
||||||
async function listar(req, res) {
|
async function listar(req, res) {
|
||||||
try {
|
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({
|
const resultado = await centrosCustoService.listarCentrosCusto({
|
||||||
|
idUsuario,
|
||||||
limite: req.query.limite,
|
limite: req.query.limite,
|
||||||
offset: req.query.offset,
|
offset: req.query.offset,
|
||||||
page: req.query.page,
|
page: req.query.page,
|
||||||
|
|
@ -73,6 +115,7 @@ async function listar(req, res) {
|
||||||
async function buscarPorId(req, res) {
|
async function buscarPorId(req, res) {
|
||||||
try {
|
try {
|
||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
|
const idUsuario = obterIdUsuario(req);
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return res.status(400).json({
|
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) {
|
if (!centroCusto) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
@ -106,6 +159,15 @@ async function buscarPorId(req, res) {
|
||||||
|
|
||||||
async function criar(req, res) {
|
async function criar(req, res) {
|
||||||
try {
|
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);
|
const erroValidacao = validarDadosCentroCusto(req.body);
|
||||||
|
|
||||||
if (erroValidacao) {
|
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({
|
return res.status(201).json({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
@ -135,6 +200,7 @@ async function criar(req, res) {
|
||||||
async function atualizar(req, res) {
|
async function atualizar(req, res) {
|
||||||
try {
|
try {
|
||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
|
const idUsuario = obterIdUsuario(req);
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return res.status(400).json({
|
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);
|
const erroValidacao = validarDadosCentroCusto(req.body);
|
||||||
|
|
||||||
if (erroValidacao) {
|
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) {
|
if (!centroAtualizado) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
@ -179,6 +256,7 @@ async function atualizar(req, res) {
|
||||||
async function alterarHabilitado(req, res) {
|
async function alterarHabilitado(req, res) {
|
||||||
try {
|
try {
|
||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
|
const idUsuario = obterIdUsuario(req);
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return res.status(400).json({
|
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)) {
|
if (![0, 1].includes(habilitado)) {
|
||||||
return res.status(400).json({
|
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) {
|
if (!centroAtualizado) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
@ -270,4 +360,4 @@ module.exports = {
|
||||||
atualizar,
|
atualizar,
|
||||||
alterarHabilitado,
|
alterarHabilitado,
|
||||||
deletar,
|
deletar,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,4 @@ router.put('/:id', centrosCustoController.atualizar);
|
||||||
router.patch('/:id/habilitado', centrosCustoController.alterarHabilitado);
|
router.patch('/:id/habilitado', centrosCustoController.alterarHabilitado);
|
||||||
router.delete('/:id', centrosCustoController.deletar);
|
router.delete('/:id', centrosCustoController.deletar);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
const pool = require('../../../database/mysql');
|
const pool = require('../../../database/mysql');
|
||||||
|
|
||||||
const CAMPOS_CENTRO_CUSTO_SELECT = `
|
const CAMPOS_CENTRO_CUSTO_SELECT = `
|
||||||
idcentrodecustos,
|
cc.idcentrodecustos,
|
||||||
descricao,
|
cc.descricao,
|
||||||
limite,
|
cc.investimento,
|
||||||
simular,
|
cc.habilitado,
|
||||||
investimento,
|
cc.insert_date,
|
||||||
habilitado,
|
cc.update_date,
|
||||||
insert_date,
|
CASE
|
||||||
update_date
|
WHEN ccs.idcentrodecustossimulacao IS NOT NULL THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS simular,
|
||||||
|
ccs.limite
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function normalizarTextoOuNull(valor) {
|
function normalizarTextoOuNull(valor) {
|
||||||
|
|
@ -38,9 +41,15 @@ function normalizarFlag(valor, padrao = 0) {
|
||||||
return padrao;
|
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) {
|
function limitarNumero(valor, padrao, minimo, maximo) {
|
||||||
|
|
@ -63,17 +72,17 @@ function limitarNumero(valor, padrao, minimo, maximo) {
|
||||||
|
|
||||||
function resolverOrdenacao(orderBy) {
|
function resolverOrdenacao(orderBy) {
|
||||||
const camposPermitidos = {
|
const camposPermitidos = {
|
||||||
idcentrodecustos: 'idcentrodecustos',
|
idcentrodecustos: 'cc.idcentrodecustos',
|
||||||
descricao: 'descricao',
|
descricao: 'cc.descricao',
|
||||||
limite: 'limite',
|
limite: 'ccs.limite',
|
||||||
simular: 'simular',
|
simular: 'simular',
|
||||||
investimento: 'investimento',
|
investimento: 'cc.investimento',
|
||||||
habilitado: 'habilitado',
|
habilitado: 'cc.habilitado',
|
||||||
insert_date: 'insert_date',
|
insert_date: 'cc.insert_date',
|
||||||
update_date: 'update_date',
|
update_date: 'cc.update_date',
|
||||||
};
|
};
|
||||||
|
|
||||||
return camposPermitidos[orderBy] || 'descricao';
|
return camposPermitidos[orderBy] || 'cc.descricao';
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolverDirecao(orderDirection) {
|
function resolverDirecao(orderDirection) {
|
||||||
|
|
@ -84,41 +93,51 @@ function montarWhereCentrosCusto(filtros = {}) {
|
||||||
const where = [];
|
const where = [];
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (filtros.simular !== undefined && filtros.simular !== null && filtros.simular !== '') {
|
if (
|
||||||
where.push('simular = ?');
|
filtros.simular !== undefined
|
||||||
params.push(Number(filtros.simular));
|
&& 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 !== '') {
|
if (
|
||||||
where.push('investimento = ?');
|
filtros.investimento !== undefined
|
||||||
params.push(Number(filtros.investimento));
|
&& filtros.investimento !== null
|
||||||
|
&& filtros.investimento !== ''
|
||||||
|
) {
|
||||||
|
where.push('cc.investimento = ?');
|
||||||
|
params.push(normalizarFlag(filtros.investimento, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filtros.habilitado !== undefined && filtros.habilitado !== null && filtros.habilitado !== '') {
|
if (
|
||||||
where.push('habilitado = ?');
|
filtros.habilitado !== undefined
|
||||||
params.push(Number(filtros.habilitado));
|
&& filtros.habilitado !== null
|
||||||
|
&& filtros.habilitado !== ''
|
||||||
|
) {
|
||||||
|
where.push('cc.habilitado = ?');
|
||||||
|
params.push(normalizarFlag(filtros.habilitado, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filtros.busca) {
|
if (filtros.busca) {
|
||||||
where.push(`
|
where.push('cc.descricao LIKE ?');
|
||||||
(
|
params.push(`%${String(filtros.busca).trim()}%`);
|
||||||
descricao LIKE ?
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
const termo = `%${String(filtros.busca).trim()}%`;
|
|
||||||
params.push(termo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
whereSql,
|
whereSql: where.length > 0 ? `WHERE ${where.join(' AND ')}` : '',
|
||||||
params,
|
params,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listarCentrosCusto(filtros = {}) {
|
async function listarCentrosCusto(filtros = {}) {
|
||||||
|
const idUsuario = Number(filtros.idUsuario);
|
||||||
const limite = limitarNumero(filtros.limite, 20, 1, 100);
|
const limite = limitarNumero(filtros.limite, 20, 1, 100);
|
||||||
const page = limitarNumero(filtros.page, 1, 1, 999999);
|
const page = limitarNumero(filtros.page, 1, 1, 999999);
|
||||||
const offset = filtros.offset !== undefined
|
const offset = filtros.offset !== undefined
|
||||||
|
|
@ -127,50 +146,69 @@ async function listarCentrosCusto(filtros = {}) {
|
||||||
|
|
||||||
const orderBy = resolverOrdenacao(filtros.orderBy);
|
const orderBy = resolverOrdenacao(filtros.orderBy);
|
||||||
const orderDirection = resolverDirecao(filtros.orderDirection);
|
const orderDirection = resolverDirecao(filtros.orderDirection);
|
||||||
|
|
||||||
const { whereSql, params } = montarWhereCentrosCusto(filtros);
|
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(
|
const [rows] = await pool.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
${CAMPOS_CENTRO_CUSTO_SELECT}
|
${CAMPOS_CENTRO_CUSTO_SELECT}
|
||||||
FROM centrodecustos
|
${joinSql}
|
||||||
${whereSql}
|
${whereSql}
|
||||||
ORDER BY ${orderBy} ${orderDirection}, idcentrodecustos ASC
|
ORDER BY ${orderBy} ${orderDirection}, cc.idcentrodecustos ASC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
`,
|
`,
|
||||||
[...params, limite, offset]
|
[...queryParams, limite, offset]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [countRows] = await pool.query(
|
const [countRows] = await pool.query(
|
||||||
`
|
`
|
||||||
SELECT COUNT(*) AS total
|
SELECT COUNT(*) AS total
|
||||||
FROM centrodecustos
|
${joinSql}
|
||||||
${whereSql}
|
${whereSql}
|
||||||
`,
|
`,
|
||||||
params
|
queryParams
|
||||||
);
|
);
|
||||||
|
|
||||||
const [summaryRows] = await pool.query(
|
const [summaryRows] = await pool.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) AS quantidade,
|
COUNT(*) AS quantidade,
|
||||||
COALESCE(SUM(limite), 0) AS limiteTotal,
|
COALESCE(SUM(ccs.limite), 0) AS limiteTotal,
|
||||||
COALESCE(SUM(CASE WHEN habilitado = 1 THEN 1 ELSE 0 END), 0) AS habilitados,
|
COALESCE(SUM(CASE WHEN cc.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 cc.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 cc.investimento = 1 THEN 1 ELSE 0 END), 0) AS investimentos,
|
||||||
COALESCE(SUM(CASE WHEN simular = 1 THEN 1 ELSE 0 END), 0) AS simulaveis
|
COALESCE(SUM(
|
||||||
FROM centrodecustos
|
CASE
|
||||||
|
WHEN ccs.idcentrodecustossimulacao IS NOT NULL THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
), 0) AS simulaveis
|
||||||
|
${joinSql}
|
||||||
${whereSql}
|
${whereSql}
|
||||||
`,
|
`,
|
||||||
params
|
queryParams
|
||||||
);
|
);
|
||||||
|
|
||||||
const total = Number(countRows[0]?.total || 0);
|
const total = Number(countRows[0]?.total || 0);
|
||||||
const totalPages = Math.max(1, Math.ceil(total / limite));
|
const totalPages = Math.max(1, Math.ceil(total / limite));
|
||||||
|
|
||||||
return {
|
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: {
|
pagination: {
|
||||||
total,
|
total,
|
||||||
limite,
|
limite,
|
||||||
|
|
@ -189,102 +227,178 @@ async function listarCentrosCusto(filtros = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buscarCentroCustoPorId(id) {
|
async function buscarCentroCustoPorId(id, idUsuario, executor = pool) {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await executor.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
${CAMPOS_CENTRO_CUSTO_SELECT}
|
${CAMPOS_CENTRO_CUSTO_SELECT}
|
||||||
FROM centrodecustos
|
FROM centrodecustos cc
|
||||||
WHERE idcentrodecustos = ?
|
LEFT JOIN centrodecustossimulacao ccs
|
||||||
|
ON ccs.idcentrodecustos = cc.idcentrodecustos
|
||||||
|
AND ccs.idusuarios = ?
|
||||||
|
WHERE cc.idcentrodecustos = ?
|
||||||
LIMIT 1
|
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) {
|
async function sincronizarSimulacao(
|
||||||
const {
|
executor,
|
||||||
descricao,
|
idCentroCusto,
|
||||||
limite,
|
idUsuario,
|
||||||
simular,
|
dados
|
||||||
investimento,
|
) {
|
||||||
habilitado,
|
const simular = normalizarFlag(dados.simular, 0);
|
||||||
} = dados;
|
|
||||||
|
|
||||||
|
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(
|
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
|
UPDATE centrodecustos
|
||||||
SET
|
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) {
|
async function deletarCentroCusto(id) {
|
||||||
const centroAtual = await buscarCentroCustoPorId(id);
|
const [result] = await pool.query(
|
||||||
|
|
||||||
if (!centroAtual) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await pool.query(
|
|
||||||
`
|
`
|
||||||
DELETE FROM centrodecustos
|
DELETE FROM centrodecustos
|
||||||
WHERE idcentrodecustos = ?
|
WHERE idcentrodecustos = ?
|
||||||
|
|
@ -316,7 +428,7 @@ async function deletarCentroCusto(id) {
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return result.affectedRows > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
@ -326,4 +438,4 @@ module.exports = {
|
||||||
atualizarCentroCusto,
|
atualizarCentroCusto,
|
||||||
alterarHabilitadoCentroCusto,
|
alterarHabilitadoCentroCusto,
|
||||||
deletarCentroCusto,
|
deletarCentroCusto,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
const dashboardService = require('../services/dashboard.service');
|
const dashboardService = require('../services/dashboard.service');
|
||||||
|
|
||||||
function resolverIdUsuario(req) {
|
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) {
|
async function buscarResumo(req, res) {
|
||||||
|
|
@ -27,9 +35,12 @@ async function buscarResumo(req, res) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[dashboard.controller] buscarResumo:', error);
|
console.error('[dashboard.controller] buscarResumo:', error);
|
||||||
|
|
||||||
return res.status(500).json({
|
return res.status(error.statusCode || 500).json({
|
||||||
ok: false,
|
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');
|
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() {
|
function hojeISO() {
|
||||||
return new Date().toISOString().slice(0, 10);
|
return formatarDataLocal(new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
function inicioMesAtualISO() {
|
function inicioMesAtualISO() {
|
||||||
const hoje = new Date();
|
const hoje = new Date();
|
||||||
return new Date(hoje.getFullYear(), hoje.getMonth(), 1)
|
return formatarDataLocal(
|
||||||
.toISOString()
|
new Date(hoje.getFullYear(), hoje.getMonth(), 1)
|
||||||
.slice(0, 10);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fimMesAtualISO() {
|
function fimMesAtualISO() {
|
||||||
const hoje = new Date();
|
const hoje = new Date();
|
||||||
return new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0)
|
return formatarDataLocal(
|
||||||
.toISOString()
|
new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0)
|
||||||
.slice(0, 10);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizarDataISO(valor, fallback) {
|
function normalizarDataISO(valor, fallback) {
|
||||||
if (!valor) return fallback;
|
if (!valor) return fallback;
|
||||||
|
|
||||||
const texto = String(valor).slice(0, 10);
|
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 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) {
|
function normalizarNumero(valor, padrao = 0) {
|
||||||
|
|
@ -84,10 +99,7 @@ async function buscarCardsBancos(idUsuario) {
|
||||||
|
|
||||||
COUNT(*) AS quantidadeCarteiras,
|
COUNT(*) AS quantidadeCarteiras,
|
||||||
|
|
||||||
SUM(CASE
|
COUNT(*) AS carteirasHabilitadas,
|
||||||
WHEN habilitado = 1 THEN 1
|
|
||||||
ELSE 0
|
|
||||||
END) AS carteirasHabilitadas,
|
|
||||||
|
|
||||||
SUM(CASE
|
SUM(CASE
|
||||||
WHEN debito = 1
|
WHEN debito = 1
|
||||||
|
|
@ -113,6 +125,7 @@ async function buscarCardsBancos(idUsuario) {
|
||||||
END) AS quantidadeCreditos
|
END) AS quantidadeCreditos
|
||||||
FROM bancos
|
FROM bancos
|
||||||
WHERE idusuarios = ?
|
WHERE idusuarios = ?
|
||||||
|
AND habilitado = 1
|
||||||
`,
|
`,
|
||||||
[idUsuario]
|
[idUsuario]
|
||||||
);
|
);
|
||||||
|
|
@ -149,16 +162,18 @@ async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) {
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END), 0) AS entradas,
|
END), 0) AS entradas,
|
||||||
|
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN cp.movimento = 'Saida' THEN cp.valor
|
WHEN cp.movimento IN ('Saida', 'Sangria')
|
||||||
ELSE 0
|
AND COALESCE(cc.investimento, 0) = 0
|
||||||
|
THEN cp.valor
|
||||||
|
ELSE 0
|
||||||
END), 0) AS saidas,
|
END), 0) AS saidas,
|
||||||
|
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN cp.movimento = 'Sangria'
|
WHEN cp.movimento IN ('Saida', 'Sangria')
|
||||||
AND COALESCE(cc.investimento, 0) = 1
|
AND COALESCE(cc.investimento, 0) = 1
|
||||||
THEN cp.valor
|
THEN cp.valor
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END), 0) AS investimentos,
|
END), 0) AS investimentos,
|
||||||
|
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
|
|
@ -175,34 +190,36 @@ async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) {
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END) AS quantidadeAbertoEntradas,
|
END) AS quantidadeAbertoEntradas,
|
||||||
|
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN cp.status = 'A pagar'
|
WHEN cp.status = 'A pagar'
|
||||||
AND cp.movimento = 'Saida'
|
AND cp.movimento IN ('Saida', 'Sangria')
|
||||||
THEN cp.valor
|
AND COALESCE(cc.investimento, 0) = 0
|
||||||
ELSE 0
|
THEN cp.valor
|
||||||
|
ELSE 0
|
||||||
END), 0) AS abertoSaidas,
|
END), 0) AS abertoSaidas,
|
||||||
|
|
||||||
COUNT(CASE
|
COUNT(CASE
|
||||||
WHEN cp.status = 'A pagar'
|
WHEN cp.status = 'A pagar'
|
||||||
AND cp.movimento = 'Saida'
|
AND cp.movimento IN ('Saida', 'Sangria')
|
||||||
THEN 1
|
AND COALESCE(cc.investimento, 0) = 0
|
||||||
ELSE NULL
|
THEN 1
|
||||||
|
ELSE NULL
|
||||||
END) AS quantidadeAbertoSaidas,
|
END) AS quantidadeAbertoSaidas,
|
||||||
|
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN cp.status = 'A pagar'
|
WHEN cp.status = 'A pagar'
|
||||||
AND cp.movimento = 'Sangria'
|
AND cp.movimento IN ('Saida', 'Sangria')
|
||||||
AND COALESCE(cc.investimento, 0) = 1
|
AND COALESCE(cc.investimento, 0) = 1
|
||||||
THEN cp.valor
|
THEN cp.valor
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END), 0) AS abertoInvestimentos,
|
END), 0) AS abertoInvestimentos,
|
||||||
|
|
||||||
COUNT(CASE
|
COUNT(CASE
|
||||||
WHEN cp.status = 'A pagar'
|
WHEN cp.status = 'A pagar'
|
||||||
AND cp.movimento = 'Sangria'
|
AND cp.movimento IN ('Saida', 'Sangria')
|
||||||
AND COALESCE(cc.investimento, 0) = 1
|
AND COALESCE(cc.investimento, 0) = 1
|
||||||
THEN 1
|
THEN 1
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END) AS quantidadeAbertoInvestimentos,
|
END) AS quantidadeAbertoInvestimentos,
|
||||||
|
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
|
|
@ -212,7 +229,7 @@ async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) {
|
||||||
|
|
||||||
COUNT(*) AS quantidadeMovimentos
|
COUNT(*) AS quantidadeMovimentos
|
||||||
FROM contasapagar cp
|
FROM contasapagar cp
|
||||||
LEFT JOIN bancos b
|
INNER JOIN bancos b
|
||||||
ON b.idbancos = cp.idbancos
|
ON b.idbancos = cp.idbancos
|
||||||
LEFT JOIN centrodecustos cc
|
LEFT JOIN centrodecustos cc
|
||||||
ON cc.idcentrodecustos = cp.idcentrodecustos
|
ON cc.idcentrodecustos = cp.idcentrodecustos
|
||||||
|
|
@ -320,8 +337,9 @@ async function buscarAlertasVencimento(idUsuario) {
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN DATE(cp.datavencimento) >= ?
|
WHEN DATE(cp.datavencimento) >= ?
|
||||||
AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
|
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 cp.status = 'A pagar'
|
||||||
|
AND COALESCE(cc.investimento, 0) = 0
|
||||||
THEN cp.valor
|
THEN cp.valor
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END), 0) AS venceEmBreveSaidas,
|
END), 0) AS venceEmBreveSaidas,
|
||||||
|
|
@ -329,8 +347,9 @@ async function buscarAlertasVencimento(idUsuario) {
|
||||||
COUNT(CASE
|
COUNT(CASE
|
||||||
WHEN DATE(cp.datavencimento) >= ?
|
WHEN DATE(cp.datavencimento) >= ?
|
||||||
AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
|
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 cp.status = 'A pagar'
|
||||||
|
AND COALESCE(cc.investimento, 0) = 0
|
||||||
THEN 1
|
THEN 1
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END) AS quantidadeVenceEmBreveSaidas,
|
END) AS quantidadeVenceEmBreveSaidas,
|
||||||
|
|
@ -338,7 +357,7 @@ async function buscarAlertasVencimento(idUsuario) {
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN DATE(cp.datavencimento) >= ?
|
WHEN DATE(cp.datavencimento) >= ?
|
||||||
AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
|
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 cp.status = 'A pagar'
|
||||||
AND COALESCE(cc.investimento, 0) = 1
|
AND COALESCE(cc.investimento, 0) = 1
|
||||||
THEN cp.valor
|
THEN cp.valor
|
||||||
|
|
@ -348,7 +367,7 @@ async function buscarAlertasVencimento(idUsuario) {
|
||||||
COUNT(CASE
|
COUNT(CASE
|
||||||
WHEN DATE(cp.datavencimento) >= ?
|
WHEN DATE(cp.datavencimento) >= ?
|
||||||
AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
|
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 cp.status = 'A pagar'
|
||||||
AND COALESCE(cc.investimento, 0) = 1
|
AND COALESCE(cc.investimento, 0) = 1
|
||||||
THEN 1
|
THEN 1
|
||||||
|
|
@ -430,7 +449,7 @@ async function buscarProximosVencimentos(idUsuario) {
|
||||||
cc.descricao AS centro_custo_descricao,
|
cc.descricao AS centro_custo_descricao,
|
||||||
DATEDIFF(DATE(cp.datavencimento), CURDATE()) AS dias_para_vencer
|
DATEDIFF(DATE(cp.datavencimento), CURDATE()) AS dias_para_vencer
|
||||||
FROM contasapagar cp
|
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
|
LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos
|
||||||
WHERE cp.deleted_at IS NULL
|
WHERE cp.deleted_at IS NULL
|
||||||
AND bc.idusuarios = ?
|
AND bc.idusuarios = ?
|
||||||
|
|
@ -630,7 +649,7 @@ async function buscarUltimosMovimentos(idUsuario) {
|
||||||
cp.idcentrodecustos,
|
cp.idcentrodecustos,
|
||||||
cc.descricao AS centro_custo_descricao
|
cc.descricao AS centro_custo_descricao
|
||||||
FROM contasapagar cp
|
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
|
LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos
|
||||||
WHERE cp.deleted_at IS NULL
|
WHERE cp.deleted_at IS NULL
|
||||||
AND bc.idusuarios = ?
|
AND bc.idusuarios = ?
|
||||||
|
|
@ -655,7 +674,8 @@ async function buscarGraficos(idUsuario, dataInicio, dataFim) {
|
||||||
LEFT JOIN bancos b
|
LEFT JOIN bancos b
|
||||||
ON b.idbancos = cp.idbancos
|
ON b.idbancos = cp.idbancos
|
||||||
WHERE cp.deleted_at IS NULL
|
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 DATE(cp.datavencimento) BETWEEN ? AND ?
|
||||||
AND b.idusuarios = ?
|
AND b.idusuarios = ?
|
||||||
GROUP BY
|
GROUP BY
|
||||||
|
|
@ -676,16 +696,18 @@ async function buscarGraficos(idUsuario, dataInicio, dataFim) {
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END), 0) AS entradas,
|
END), 0) AS entradas,
|
||||||
|
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN cp.movimento = 'Saida' THEN cp.valor
|
WHEN cp.movimento IN ('Saida', 'Sangria')
|
||||||
ELSE 0
|
AND COALESCE(cc.investimento, 0) = 0
|
||||||
|
THEN cp.valor
|
||||||
|
ELSE 0
|
||||||
END), 0) AS saidas,
|
END), 0) AS saidas,
|
||||||
|
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN cp.movimento = 'Sangria'
|
WHEN cp.movimento IN ('Saida', 'Sangria')
|
||||||
AND COALESCE(cc.investimento, 0) = 1
|
AND COALESCE(cc.investimento, 0) = 1
|
||||||
THEN cp.valor
|
THEN cp.valor
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END), 0) AS investimentos
|
END), 0) AS investimentos
|
||||||
FROM contasapagar cp
|
FROM contasapagar cp
|
||||||
LEFT JOIN centrodecustos cc
|
LEFT JOIN centrodecustos cc
|
||||||
|
|
@ -753,6 +775,12 @@ async function buscarResumoDashboard(idUsuario, filtros = {}) {
|
||||||
const dataInicio = normalizarDataISO(filtros.dataInicio, inicioMesAtualISO());
|
const dataInicio = normalizarDataISO(filtros.dataInicio, inicioMesAtualISO());
|
||||||
const dataFim = normalizarDataISO(filtros.dataFim, fimMesAtualISO());
|
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 [
|
const [
|
||||||
cardsBancos,
|
cardsBancos,
|
||||||
cardsMovimentos,
|
cardsMovimentos,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
const simulacaoMensalService = require('../services/simulacaoMensal.service');
|
const simulacaoMensalService = require('../services/simulacaoMensal.service');
|
||||||
|
|
||||||
function extrairIdUsuarioLogado(req) {
|
function extrairIdUsuarioLogado(req) {
|
||||||
return (
|
return Number(
|
||||||
req.usuario?.idusuarios ||
|
req.usuario?.idusuarios
|
||||||
req.user?.idusuarios ||
|
?? req.user?.idusuarios
|
||||||
req.usuario?.id ||
|
?? req.usuario?.idusuario
|
||||||
req.user?.id ||
|
?? req.user?.idusuario
|
||||||
null
|
?? req.usuario?.id
|
||||||
|
?? req.user?.id
|
||||||
|
?? 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -310,23 +310,29 @@ async function buscarFixosUsuario(connection, idUsuario) {
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buscarCentrosSimulaveis(connection) {
|
async function buscarCentrosSimulaveis(connection, idUsuario) {
|
||||||
const [rows] = await connection.query(
|
const [rows] = await connection.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
idcentrodecustos,
|
cc.idcentrodecustos,
|
||||||
descricao,
|
cc.descricao,
|
||||||
limite,
|
ccs.limite,
|
||||||
simular,
|
cc.investimento
|
||||||
investimento
|
FROM centrodecustossimulacao ccs
|
||||||
FROM centrodecustos
|
INNER JOIN centrodecustos cc
|
||||||
WHERE habilitado = 1
|
ON cc.idcentrodecustos = ccs.idcentrodecustos
|
||||||
AND simular = 1
|
WHERE ccs.idusuarios = ?
|
||||||
ORDER BY descricao ASC
|
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) {
|
function movimentoFixoJaExisteNoMes(fixo, movimentosDoMes) {
|
||||||
|
|
@ -716,7 +722,7 @@ async function montarBaseSimulacaoMensal(opcoes = {}) {
|
||||||
dataFim,
|
dataFim,
|
||||||
}),
|
}),
|
||||||
buscarFixosUsuario(connection, idUsuario),
|
buscarFixosUsuario(connection, idUsuario),
|
||||||
buscarCentrosSimulaveis(connection),
|
buscarCentrosSimulaveis(connection, idUsuario),
|
||||||
buscarMovimentosParaCentrosDoMes(connection, {
|
buscarMovimentosParaCentrosDoMes(connection, {
|
||||||
idUsuario,
|
idUsuario,
|
||||||
dataInicio,
|
dataInicio,
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,16 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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() {
|
export function AppLayout() {
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
|
||||||
|
function abrirMenuMobile() {
|
||||||
|
setMobileOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fecharMenuMobile() {
|
||||||
|
setMobileOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ minHeight: '100vh', backgroundColor: 'background.default' }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
backgroundColor: 'background.default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
component="aside"
|
component="aside"
|
||||||
sx={{
|
sx={{
|
||||||
width: SIDEBAR_WIDTH,
|
width: SIDEBAR_WIDTH,
|
||||||
|
height: '100vh',
|
||||||
display: { xs: 'none', md: 'block' },
|
display: { xs: 'none', md: 'block' },
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
inset: '0 auto 0 0',
|
inset: '0 auto 0 0',
|
||||||
|
|
@ -26,40 +41,49 @@ export function AppLayout() {
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
open={mobileOpen}
|
open={mobileOpen}
|
||||||
onClose={() => setMobileOpen(false)}
|
onClose={fecharMenuMobile}
|
||||||
ModalProps={{ keepMounted: true }}
|
ModalProps={{
|
||||||
|
keepMounted: true,
|
||||||
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
display: { xs: 'block', md: 'none' },
|
display: { xs: 'block', md: 'none' },
|
||||||
|
|
||||||
'& .MuiDrawer-paper': {
|
'& .MuiDrawer-paper': {
|
||||||
width: SIDEBAR_WIDTH,
|
width: SIDEBAR_WIDTH,
|
||||||
border: 0,
|
border: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Sidebar onNavigate={() => setMobileOpen(false)} />
|
<Sidebar onNavigate={fecharMenuMobile} />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
marginLeft: { xs: 0, md: `${SIDEBAR_WIDTH}px` },
|
marginLeft: {
|
||||||
|
xs: 0,
|
||||||
|
md: `${SIDEBAR_WIDTH}px`,
|
||||||
|
},
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
overflowX: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Topbar onMenuClick={() => setMobileOpen(true)} />
|
<Topbar onMenuClick={abrirMenuMobile} />
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
component="main"
|
component="main"
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: 1280,
|
maxWidth: 1440,
|
||||||
margin: '0 auto',
|
marginX: 'auto',
|
||||||
padding: {
|
padding: {
|
||||||
xs: 2,
|
xs: 2,
|
||||||
sm: 2.5,
|
sm: 2.5,
|
||||||
md: 4,
|
md: 3.5,
|
||||||
|
lg: 4,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Divider,
|
Divider,
|
||||||
|
|
@ -5,170 +6,331 @@ import {
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
|
Stack,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
import DashboardRoundedIcon from '@mui/icons-material/DashboardRounded';
|
||||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
import SwapHorizRoundedIcon from '@mui/icons-material/SwapHorizRounded';
|
||||||
import AssessmentIcon from '@mui/icons-material/Assessment';
|
import AssessmentRoundedIcon from '@mui/icons-material/AssessmentRounded';
|
||||||
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
|
import AccountBalanceWalletRoundedIcon from '@mui/icons-material/AccountBalanceWalletRounded';
|
||||||
import SavingsIcon from '@mui/icons-material/Savings';
|
import SavingsRoundedIcon from '@mui/icons-material/SavingsRounded';
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded';
|
||||||
import PeopleAltIcon from '@mui/icons-material/PeopleAlt';
|
import PeopleAltRoundedIcon from '@mui/icons-material/PeopleAltRounded';
|
||||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
import AccountTreeRoundedIcon from '@mui/icons-material/AccountTreeRounded';
|
||||||
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
|
import EventRepeatRoundedIcon from '@mui/icons-material/EventRepeatRounded';
|
||||||
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
|
import ManageAccountsRoundedIcon from '@mui/icons-material/ManageAccountsRounded';
|
||||||
import CreditCardIcon from '@mui/icons-material/CreditCard';
|
import CreditCardRoundedIcon from '@mui/icons-material/CreditCardRounded';
|
||||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
|
||||||
|
import InsightsRoundedIcon from '@mui/icons-material/InsightsRounded';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
type SidebarProps = {
|
type SidebarProps = {
|
||||||
onNavigate?: () => void;
|
onNavigate?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuItems = [
|
type MenuItem = {
|
||||||
|
label: string;
|
||||||
|
path: string;
|
||||||
|
icon: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MenuSection = {
|
||||||
|
title: string;
|
||||||
|
items: MenuItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuSections: MenuSection[] = [
|
||||||
{
|
{
|
||||||
label: 'Dashboard',
|
title: 'Visão geral',
|
||||||
path: '/dashboard',
|
items: [
|
||||||
icon: <DashboardIcon />,
|
{
|
||||||
|
label: 'Dashboard',
|
||||||
|
path: '/dashboard',
|
||||||
|
icon: <DashboardRoundedIcon />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Simulação mensal',
|
||||||
|
path: '/simulacao-mensal',
|
||||||
|
icon: <AutoAwesomeRoundedIcon />,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Movimentos',
|
title: 'Financeiro',
|
||||||
path: '/movimentos',
|
items: [
|
||||||
icon: <SwapHorizIcon />,
|
{
|
||||||
|
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',
|
title: 'Cadastros',
|
||||||
path: '/quitacoes-credito',
|
items: [
|
||||||
icon: <CreditCardIcon />,
|
{
|
||||||
|
label: 'Clientes',
|
||||||
|
path: '/clientes',
|
||||||
|
icon: <PeopleAltRoundedIcon />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Centros de custo',
|
||||||
|
path: '/centros-custo',
|
||||||
|
icon: <AccountTreeRoundedIcon />,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Clientes',
|
title: 'Análise',
|
||||||
path: '/clientes',
|
items: [
|
||||||
icon: <PeopleAltIcon />,
|
{
|
||||||
|
label: 'Relatórios',
|
||||||
|
path: '/relatorios',
|
||||||
|
icon: <AssessmentRoundedIcon />,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Carteiras',
|
title: 'Administração',
|
||||||
path: '/bancos',
|
items: [
|
||||||
icon: <AccountBalanceWalletIcon />,
|
{
|
||||||
},
|
label: 'Usuários',
|
||||||
{
|
path: '/usuarios',
|
||||||
label: 'Rendimentos',
|
icon: <ManageAccountsRoundedIcon />,
|
||||||
path: '/rendimentos',
|
},
|
||||||
icon: <SavingsIcon />,
|
{
|
||||||
},
|
label: 'Configurações',
|
||||||
{
|
path: '/configuracoes',
|
||||||
label: 'Centros de custo',
|
icon: <SettingsRoundedIcon />,
|
||||||
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 />,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Sidebar({ onNavigate }: SidebarProps) {
|
export function Sidebar({ onNavigate }: SidebarProps) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
component="aside"
|
||||||
sx={{
|
sx={{
|
||||||
width: 280,
|
width: 280,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
background: 'linear-gradient(180deg, #0F172A 0%, #111827 100%)',
|
color: '#FFFFFF',
|
||||||
color: '#fff',
|
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 }}>
|
<Box
|
||||||
<Typography variant="h6" fontWeight={900}>
|
sx={{
|
||||||
Zendion
|
padding: 3,
|
||||||
</Typography>
|
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)' }}>
|
<Box sx={{ minWidth: 0 }}>
|
||||||
Financeiro
|
<Typography
|
||||||
</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>
|
</Box>
|
||||||
|
|
||||||
<Divider sx={{ borderColor: 'rgba(255,255,255,0.08)' }} />
|
<Divider sx={{ borderColor: 'rgba(255,255,255,0.08)' }} />
|
||||||
|
|
||||||
<List sx={{ padding: 2, flex: 1 }}>
|
<List
|
||||||
{menuItems.map((item) => (
|
component="nav"
|
||||||
<ListItemButton
|
aria-label="Menu principal"
|
||||||
key={item.path}
|
sx={{
|
||||||
component={NavLink}
|
flex: 1,
|
||||||
to={item.path}
|
overflowY: 'auto',
|
||||||
onClick={onNavigate}
|
overflowX: 'hidden',
|
||||||
sx={{
|
padding: 2,
|
||||||
borderRadius: 2,
|
paddingTop: 2.25,
|
||||||
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>
|
|
||||||
|
|
||||||
<ListItemText
|
'&::-webkit-scrollbar': {
|
||||||
primary={item.label}
|
width: 6,
|
||||||
primaryTypographyProps={{
|
},
|
||||||
fontWeight: 700,
|
|
||||||
|
'&::-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>
|
</List>
|
||||||
|
|
||||||
|
<Divider sx={{ borderColor: 'rgba(255,255,255,0.08)' }} />
|
||||||
|
|
||||||
<Box sx={{ padding: 2 }}>
|
<Box sx={{ padding: 2 }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
padding: 2,
|
padding: 2,
|
||||||
borderRadius: 3,
|
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)',
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2" fontWeight={800}>
|
<Typography
|
||||||
MVP ativo
|
variant="body2"
|
||||||
|
fontWeight={850}
|
||||||
|
sx={{ color: 'rgba(255,255,255,0.88)' }}
|
||||||
|
>
|
||||||
|
Zendion Finance
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.62)' }}>
|
<Typography
|
||||||
Movimentos e carteiras em operação.
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
display: 'block',
|
||||||
|
marginTop: 0.25,
|
||||||
|
color: 'rgba(255,255,255,0.46)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Versão 1.0.0
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,146 @@ import {
|
||||||
AppBar,
|
AppBar,
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
|
Chip,
|
||||||
IconButton,
|
IconButton,
|
||||||
Stack,
|
Stack,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import MenuRoundedIcon from '@mui/icons-material/MenuRounded';
|
||||||
import LogoutIcon from '@mui/icons-material/Logout';
|
import LogoutRoundedIcon from '@mui/icons-material/LogoutRounded';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../../features/auth/store/authStore';
|
import { useAuthStore } from '../../features/auth/store/authStore';
|
||||||
|
|
||||||
type TopbarProps = {
|
type TopbarProps = {
|
||||||
onMenuClick: () => void;
|
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) {
|
export function Topbar({ onMenuClick }: TopbarProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const user = useAuthStore((state) => state.user);
|
const user = useAuthStore((state) => state.user);
|
||||||
const logout = useAuthStore((state) => state.logout);
|
const logout = useAuthStore((state) => state.logout);
|
||||||
|
|
||||||
|
const routeInfo = resolverInformacaoRota(location.pathname);
|
||||||
|
const iniciais = obterIniciais(user?.nome);
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
logout();
|
logout();
|
||||||
navigate('/login');
|
navigate('/login', { replace: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -31,60 +149,171 @@ export function Topbar({ onMenuClick }: TopbarProps) {
|
||||||
position="sticky"
|
position="sticky"
|
||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: '#FFFFFF',
|
zIndex: (theme) => theme.zIndex.drawer - 1,
|
||||||
color: '#111827',
|
color: '#111827',
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.92)',
|
||||||
|
backdropFilter: 'blur(14px)',
|
||||||
borderBottom: '1px solid',
|
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 }}>
|
<Toolbar
|
||||||
<IconButton
|
sx={{
|
||||||
onClick={onMenuClick}
|
minHeight: { xs: 68, md: 76 },
|
||||||
edge="start"
|
paddingX: { xs: 1.5, sm: 2.5, md: 3 },
|
||||||
sx={{
|
}}
|
||||||
display: { xs: 'inline-flex', md: 'none' },
|
>
|
||||||
marginRight: 1,
|
<Tooltip title="Abrir menu">
|
||||||
}}
|
<IconButton
|
||||||
>
|
onClick={onMenuClick}
|
||||||
<MenuIcon />
|
edge="start"
|
||||||
</IconButton>
|
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 }}>
|
'&:hover': {
|
||||||
<Typography variant="body2" color="text.secondary">
|
backgroundColor: 'rgba(59,130,246,0.08)',
|
||||||
Painel financeiro
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
<Typography variant="h6" fontWeight={900}>
|
<Typography
|
||||||
Controle de movimentos
|
variant="h6"
|
||||||
|
fontWeight={950}
|
||||||
|
noWrap
|
||||||
|
sx={{
|
||||||
|
marginTop: 0.25,
|
||||||
|
fontSize: { xs: '1.02rem', sm: '1.18rem' },
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{routeInfo.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
<Stack
|
||||||
<Avatar
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={{ xs: 0.75, sm: 1.25 }}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={1.15}
|
||||||
sx={{
|
sx={{
|
||||||
width: 38,
|
paddingY: 0.6,
|
||||||
height: 38,
|
paddingLeft: 0.65,
|
||||||
bgcolor: 'primary.main',
|
paddingRight: { xs: 0.65, sm: 1.4 },
|
||||||
fontWeight: 800,
|
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' } }}>
|
<Box
|
||||||
<Typography variant="body2" fontWeight={800}>
|
sx={{
|
||||||
{user?.nome || 'Usuário'}
|
display: { xs: 'none', sm: 'block' },
|
||||||
</Typography>
|
minWidth: 0,
|
||||||
<Typography variant="caption" color="text.secondary">
|
maxWidth: 180,
|
||||||
Logado
|
}}
|
||||||
</Typography>
|
>
|
||||||
</Box>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
fontWeight={850}
|
||||||
|
noWrap
|
||||||
|
title={user?.nome || 'Usuário'}
|
||||||
|
>
|
||||||
|
{user?.nome || 'Usuário'}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<IconButton onClick={handleLogout} title="Sair">
|
<Typography
|
||||||
<LogoutIcon />
|
variant="caption"
|
||||||
</IconButton>
|
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>
|
</Stack>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import type { FormEvent } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
|
@ -9,30 +10,42 @@ import {
|
||||||
Chip,
|
Chip,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Divider,
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
Stack,
|
Stack,
|
||||||
TextField,
|
TextField,
|
||||||
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} 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 LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||||
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
|
|
||||||
import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined';
|
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 { loginRequest } from '../services/authService';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
|
|
||||||
|
const APP_NAME = 'Zendion Finance';
|
||||||
|
const APP_VERSION = '1.0.0';
|
||||||
|
const LOGO_SRC = '/icon.png';
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const setAuth = useAuthStore((state) => state.setAuth);
|
const setAuth = useAuthStore((state) => state.setAuth);
|
||||||
|
|
||||||
const [usuario, setUsuario] = useState('');
|
const [usuario, setUsuario] = useState('');
|
||||||
const [senha, setSenha] = useState('');
|
const [senha, setSenha] = useState('');
|
||||||
|
const [mostrarSenha, setMostrarSenha] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [erro, setErro] = useState('');
|
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();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!usuario.trim() || !senha.trim()) {
|
if (!usuario.trim() || !senha) {
|
||||||
setErro('Informe usuário e senha.');
|
setErro('Informe usuário e senha.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -47,11 +60,11 @@ export function LoginPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
setAuth(result.user, result.token);
|
setAuth(result.user, result.token);
|
||||||
navigate('/dashboard');
|
navigate('/dashboard', { replace: true });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message =
|
const message =
|
||||||
error?.response?.data?.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);
|
setErro(message);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -65,217 +78,442 @@ export function LoginPage() {
|
||||||
display="grid"
|
display="grid"
|
||||||
gridTemplateColumns={{
|
gridTemplateColumns={{
|
||||||
xs: '1fr',
|
xs: '1fr',
|
||||||
md: '1.1fr 0.9fr',
|
md: 'minmax(480px, 1.08fr) minmax(420px, 0.92fr)',
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
background:
|
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
|
<Box
|
||||||
|
component="section"
|
||||||
sx={{
|
sx={{
|
||||||
display: { xs: 'none', md: 'flex' },
|
display: { xs: 'none', md: 'flex' },
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: 6,
|
padding: { md: 5, lg: 7 },
|
||||||
background:
|
color: '#FFFFFF',
|
||||||
'linear-gradient(160deg, #0F172A 0%, #111827 45%, #1E3A8A 100%)',
|
|
||||||
color: '#fff',
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
background:
|
||||||
|
'linear-gradient(155deg, #0F172A 0%, #111827 48%, #172554 100%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: 360,
|
width: 430,
|
||||||
height: 360,
|
height: 430,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: 'rgba(96,165,250,0.18)',
|
top: -170,
|
||||||
top: -120,
|
right: -120,
|
||||||
right: -100,
|
background:
|
||||||
filter: 'blur(4px)',
|
'radial-gradient(circle, rgba(59,130,246,0.30), rgba(59,130,246,0.02) 68%, transparent 72%)',
|
||||||
|
filter: 'blur(2px)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: 260,
|
width: 310,
|
||||||
height: 260,
|
height: 310,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: 'rgba(34,197,94,0.10)',
|
bottom: -120,
|
||||||
bottom: -80,
|
left: -90,
|
||||||
left: -70,
|
background:
|
||||||
filter: 'blur(4px)',
|
'radial-gradient(circle, rgba(14,165,233,0.18), rgba(14,165,233,0.02) 65%, transparent 70%)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box maxWidth={560} position="relative">
|
<Box
|
||||||
<Stack direction="row" spacing={1.5} alignItems="center" marginBottom={5}>
|
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
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: 52,
|
width: 58,
|
||||||
height: 52,
|
height: 58,
|
||||||
borderRadius: 3,
|
flexShrink: 0,
|
||||||
|
borderRadius: 3.5,
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
placeItems: 'center',
|
placeItems: 'center',
|
||||||
backgroundColor: 'rgba(255,255,255,0.10)',
|
overflow: 'hidden',
|
||||||
border: '1px solid rgba(255,255,255,0.14)',
|
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>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h5" fontWeight={900}>
|
<Typography
|
||||||
Zendion
|
variant="h5"
|
||||||
|
fontWeight={950}
|
||||||
|
lineHeight={1.05}
|
||||||
|
letterSpacing="-0.02em"
|
||||||
|
>
|
||||||
|
{APP_NAME}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography sx={{ color: 'rgba(255,255,255,0.68)' }}>
|
|
||||||
Financeiro
|
<Typography sx={{ color: 'rgba(255,255,255,0.62)' }}>
|
||||||
|
Gestão financeira
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Chip
|
<Chip
|
||||||
label="Controle financeiro web"
|
label="Controle financeiro inteligente"
|
||||||
sx={{
|
icon={<AutoGraphRoundedIcon />}
|
||||||
color: '#BFDBFE',
|
|
||||||
borderColor: 'rgba(191,219,254,0.28)',
|
|
||||||
backgroundColor: 'rgba(37,99,235,0.16)',
|
|
||||||
marginBottom: 2,
|
|
||||||
fontWeight: 700,
|
|
||||||
}}
|
|
||||||
variant="outlined"
|
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}>
|
<Typography
|
||||||
Movimentos, carteiras e saldos em um painel limpo.
|
variant="h3"
|
||||||
|
fontWeight={950}
|
||||||
|
lineHeight={1.06}
|
||||||
|
letterSpacing="-0.035em"
|
||||||
|
marginBottom={2.5}
|
||||||
|
>
|
||||||
|
Clareza para decidir.
|
||||||
|
<br />
|
||||||
|
Controle para evoluir.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
sx={{
|
sx={{
|
||||||
color: 'rgba(255,255,255,0.72)',
|
maxWidth: 540,
|
||||||
fontWeight: 400,
|
|
||||||
maxWidth: 520,
|
|
||||||
marginBottom: 5,
|
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>
|
</Typography>
|
||||||
|
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2.25}>
|
||||||
<Stack direction="row" spacing={2} alignItems="center">
|
<Stack direction="row" spacing={1.75} alignItems="center">
|
||||||
<ShieldOutlinedIcon sx={{ color: '#93C5FD' }} />
|
<Box
|
||||||
<Typography sx={{ color: 'rgba(255,255,255,0.78)' }}>
|
sx={{
|
||||||
Acesso protegido por autenticação e token.
|
width: 38,
|
||||||
</Typography>
|
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>
|
||||||
|
|
||||||
<Stack direction="row" spacing={2} alignItems="center">
|
<Stack direction="row" spacing={1.75} alignItems="center">
|
||||||
<TrendingUpIcon sx={{ color: '#86EFAC' }} />
|
<Box
|
||||||
<Typography sx={{ color: 'rgba(255,255,255,0.78)' }}>
|
sx={{
|
||||||
Base pronta para relatórios, dashboards e controle por carteiras.
|
width: 38,
|
||||||
</Typography>
|
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>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
|
component="main"
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
padding={{ xs: 2, sm: 3, md: 6 }}
|
padding={{ xs: 2, sm: 3, md: 5, lg: 7 }}
|
||||||
|
position="relative"
|
||||||
>
|
>
|
||||||
<Card
|
<Stack
|
||||||
sx={{
|
width="100%"
|
||||||
width: '100%',
|
maxWidth={450}
|
||||||
maxWidth: 440,
|
spacing={2}
|
||||||
boxShadow: '0 22px 70px rgba(15,23,42,0.12)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<CardContent sx={{ padding: { xs: 3, sm: 4 } }}>
|
<Stack
|
||||||
<Stack spacing={1} alignItems="center" textAlign="center" marginBottom={3}>
|
direction="row"
|
||||||
<Box
|
spacing={1.25}
|
||||||
sx={{
|
alignItems="center"
|
||||||
width: 58,
|
justifyContent="center"
|
||||||
height: 58,
|
sx={{
|
||||||
borderRadius: 4,
|
display: { xs: 'flex', md: 'none' },
|
||||||
display: 'grid',
|
marginBottom: 1,
|
||||||
placeItems: 'center',
|
}}
|
||||||
backgroundColor: 'rgba(21,101,192,0.10)',
|
>
|
||||||
color: 'primary.main',
|
<Box
|
||||||
marginBottom: 1,
|
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>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="h4" fontWeight={950}>
|
<Divider sx={{ marginY: 3 }} />
|
||||||
Entrar
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography color="text.secondary">
|
<Stack
|
||||||
Acesse o Zendion Financeiro para continuar.
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
</Typography>
|
alignItems="center"
|
||||||
</Stack>
|
justifyContent="space-between"
|
||||||
|
spacing={0.5}
|
||||||
{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,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{loading ? 'Entrando...' : 'Entrar no sistema'}
|
<Typography variant="caption" color="text.secondary">
|
||||||
</Button>
|
Sistema privado da Zendion INC.
|
||||||
</Box>
|
</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">
|
<Typography
|
||||||
Sistema financeiro privado da Zendion INC.
|
variant="caption"
|
||||||
</Typography>
|
color="text.disabled"
|
||||||
</CardContent>
|
textAlign="center"
|
||||||
</Card>
|
>
|
||||||
|
Use apenas credenciais autorizadas.
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,18 @@ import {
|
||||||
Chip,
|
Chip,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Divider,
|
Divider,
|
||||||
|
FormControlLabel,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
|
Switch,
|
||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
import SaveIcon from '@mui/icons-material/Save';
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
import SavingsIcon from '@mui/icons-material/Savings';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
atualizarCentroCusto,
|
atualizarCentroCusto,
|
||||||
|
|
@ -26,6 +29,7 @@ import {
|
||||||
import type {
|
import type {
|
||||||
CentroCusto,
|
CentroCusto,
|
||||||
CriarCentroCustoRequest,
|
CriarCentroCustoRequest,
|
||||||
|
FlagNumerica,
|
||||||
} from '../types/centroCustoTypes';
|
} from '../types/centroCustoTypes';
|
||||||
|
|
||||||
type CentroCustoFormProps = {
|
type CentroCustoFormProps = {
|
||||||
|
|
@ -40,7 +44,11 @@ type FormSectionProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatarValorResumo(value: string) {
|
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', {
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
|
|
@ -101,7 +109,6 @@ const fieldSx = {
|
||||||
|
|
||||||
export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const isEdit = mode === 'edit';
|
const isEdit = mode === 'edit';
|
||||||
|
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
@ -109,44 +116,67 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
||||||
const [sucesso, setSucesso] = useState('');
|
const [sucesso, setSucesso] = useState('');
|
||||||
|
|
||||||
const [descricao, setDescricao] = useState('');
|
const [descricao, setDescricao] = useState('');
|
||||||
const [limite, setLimite] = useState('0');
|
const [investimento, setInvestimento] = useState<FlagNumerica>(0);
|
||||||
const [simular, setSimular] = useState('0');
|
const [habilitado, setHabilitado] = useState<FlagNumerica>(1);
|
||||||
const [investimento, setInvestimento] = useState('0');
|
|
||||||
const [habilitado, setHabilitado] = useState('1');
|
const [simular, setSimular] = useState<FlagNumerica>(0);
|
||||||
|
const [limite, setLimite] = useState('');
|
||||||
|
|
||||||
const titulo = isEdit ? 'Editar centro de custo' : 'Novo centro de custo';
|
const titulo = isEdit ? 'Editar centro de custo' : 'Novo centro de custo';
|
||||||
|
|
||||||
const subtitulo = isEdit
|
const subtitulo = isEdit
|
||||||
? 'Atualize os dados do centro de custo selecionado.'
|
? 'Atualize os dados globais e a configuração da sua simulação.'
|
||||||
: 'Cadastre categorias financeiras para classificar os movimentos.';
|
: 'Cadastre uma categoria financeira e, se desejar, inclua-a na sua simulação.';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialData) return;
|
if (!initialData) return;
|
||||||
|
|
||||||
setDescricao(initialData.descricao || '');
|
setDescricao(initialData.descricao || '');
|
||||||
setLimite(String(initialData.limite ?? 0));
|
setInvestimento(Number(initialData.investimento) === 1 ? 1 : 0);
|
||||||
setSimular(String(initialData.simular ?? 0));
|
setHabilitado(Number(initialData.habilitado) === 1 ? 1 : 0);
|
||||||
setInvestimento(String(initialData.investimento ?? 0));
|
setSimular(Number(initialData.simular) === 1 ? 1 : 0);
|
||||||
setHabilitado(String(initialData.habilitado ?? 1));
|
setLimite(
|
||||||
|
initialData.limite === null || initialData.limite === undefined
|
||||||
|
? ''
|
||||||
|
: String(initialData.limite)
|
||||||
|
);
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
||||||
function limparFormulario() {
|
function limparFormulario() {
|
||||||
setDescricao('');
|
setDescricao('');
|
||||||
setLimite('0');
|
setInvestimento(0);
|
||||||
setSimular('0');
|
setHabilitado(1);
|
||||||
setInvestimento('0');
|
setSimular(0);
|
||||||
setHabilitado('1');
|
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>) {
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!descricao.trim()) {
|
const erroValidacao = validarFormulario();
|
||||||
setErro('Informe a descrição.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (limite === '' || !Number.isFinite(Number(limite))) {
|
if (erroValidacao) {
|
||||||
setErro('Informe um limite válido.');
|
setErro(erroValidacao);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,10 +187,10 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
||||||
|
|
||||||
const payload: CriarCentroCustoRequest = {
|
const payload: CriarCentroCustoRequest = {
|
||||||
descricao: descricao.trim(),
|
descricao: descricao.trim(),
|
||||||
limite: Number(limite || 0),
|
investimento,
|
||||||
simular: Number(simular || 0),
|
habilitado,
|
||||||
investimento: Number(investimento || 0),
|
simular,
|
||||||
habilitado: Number(habilitado || 1),
|
limite: simular === 1 ? Number(limite) : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
|
|
@ -169,7 +199,11 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await atualizarCentroCusto(initialData.idcentrodecustos, payload);
|
await atualizarCentroCusto(
|
||||||
|
initialData.idcentrodecustos,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
setSucesso('Centro de custo atualizado com sucesso.');
|
setSucesso('Centro de custo atualizado com sucesso.');
|
||||||
} else {
|
} else {
|
||||||
await criarCentroCusto(payload);
|
await criarCentroCusto(payload);
|
||||||
|
|
@ -256,7 +290,7 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
||||||
>
|
>
|
||||||
<FormSection
|
<FormSection
|
||||||
title="Dados do centro"
|
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
|
<TextField
|
||||||
label="Descrição"
|
label="Descrição"
|
||||||
|
|
@ -264,65 +298,111 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
||||||
onChange={(event) => setDescricao(event.target.value)}
|
onChange={(event) => setDescricao(event.target.value)}
|
||||||
placeholder="Ex: Administrativo, Comercial, Impostos..."
|
placeholder="Ex: Administrativo, Comercial, Impostos..."
|
||||||
fullWidth
|
fullWidth
|
||||||
|
required
|
||||||
sx={fieldSx}
|
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
|
<TextField
|
||||||
select
|
select
|
||||||
label="Investimento"
|
label="Investimento"
|
||||||
value={investimento}
|
value={investimento}
|
||||||
onChange={(event) => setInvestimento(event.target.value)}
|
onChange={(event) =>
|
||||||
|
setInvestimento(Number(event.target.value) === 1 ? 1 : 0)
|
||||||
|
}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={fieldSx}
|
sx={fieldSx}
|
||||||
>
|
>
|
||||||
<MenuItem value="1">Sim</MenuItem>
|
<MenuItem value={1}>Sim</MenuItem>
|
||||||
<MenuItem value="0">Não</MenuItem>
|
<MenuItem value={0}>Não</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
label="Status"
|
label="Status"
|
||||||
value={habilitado}
|
value={habilitado}
|
||||||
onChange={(event) => setHabilitado(event.target.value)}
|
onChange={(event) =>
|
||||||
|
setHabilitado(Number(event.target.value) === 1 ? 1 : 0)
|
||||||
|
}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={fieldSx}
|
sx={fieldSx}
|
||||||
>
|
>
|
||||||
<MenuItem value="1">Habilitado</MenuItem>
|
<MenuItem value={1}>Habilitado</MenuItem>
|
||||||
<MenuItem value="0">Desabilitado</MenuItem>
|
<MenuItem value={0}>Desabilitado</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
</FormSection>
|
</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
|
<Stack
|
||||||
direction={{ xs: 'column-reverse', sm: 'row' }}
|
direction={{ xs: 'column-reverse', sm: 'row' }}
|
||||||
justifyContent="flex-end"
|
justifyContent="flex-end"
|
||||||
spacing={1.5}
|
spacing={1.5}
|
||||||
sx={{
|
sx={{ paddingTop: 1 }}
|
||||||
paddingTop: 1,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|
@ -378,7 +458,7 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary" marginBottom={2.5}>
|
<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>
|
</Typography>
|
||||||
|
|
||||||
<Divider sx={{ marginBottom: 2.5 }} />
|
<Divider sx={{ marginBottom: 2.5 }} />
|
||||||
|
|
@ -388,6 +468,7 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
Descrição
|
Descrição
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography fontWeight={800}>
|
<Typography fontWeight={800}>
|
||||||
{descricao || 'Sem descrição'}
|
{descricao || 'Sem descrição'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
@ -395,28 +476,11 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
Limite
|
Tipo
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h4" fontWeight={950}>
|
|
||||||
{formatarValorResumo(limite)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Simular
|
|
||||||
</Typography>
|
|
||||||
<Typography fontWeight={800}>
|
<Typography fontWeight={800}>
|
||||||
{Number(simular) === 1 ? 'Sim' : 'Não'}
|
{investimento === 1 ? 'Investimento' : 'Operacional'}
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Investimento
|
|
||||||
</Typography>
|
|
||||||
<Typography fontWeight={800}>
|
|
||||||
{Number(investimento) === 1 ? 'Sim' : 'Não'}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
@ -424,16 +488,65 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
Status
|
Status
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography
|
<Typography
|
||||||
fontWeight={800}
|
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>
|
</Typography>
|
||||||
</Box>
|
</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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,24 @@ import type {
|
||||||
|
|
||||||
const LIMITE_PADRAO = 20;
|
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', {
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'BRL',
|
currency: 'BRL',
|
||||||
}).format(Number(valor || 0));
|
}).format(Number(valor));
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatarData(data: string | null) {
|
function formatarData(data: string | null) {
|
||||||
|
|
@ -119,7 +132,10 @@ export function CentrosCustoPage() {
|
||||||
return count;
|
return count;
|
||||||
}, [busca, simular, investimento, habilitado]);
|
}, [busca, simular, investimento, habilitado]);
|
||||||
|
|
||||||
async function carregarCentrosCusto(pageToLoad = page) {
|
async function carregarCentrosCusto(
|
||||||
|
pageToLoad = page,
|
||||||
|
filtros?: Partial<FiltrosCentrosCusto>
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErro('');
|
setErro('');
|
||||||
|
|
@ -127,12 +143,30 @@ export function CentrosCustoPage() {
|
||||||
const params: ListarCentrosCustoParams = {
|
const params: ListarCentrosCustoParams = {
|
||||||
limite: LIMITE_PADRAO,
|
limite: LIMITE_PADRAO,
|
||||||
page: pageToLoad,
|
page: pageToLoad,
|
||||||
busca: busca.trim() || undefined,
|
busca:
|
||||||
simular,
|
filtros?.busca !== undefined
|
||||||
investimento,
|
? filtros.busca.trim() || undefined
|
||||||
habilitado,
|
: busca.trim() || undefined,
|
||||||
orderBy,
|
simular:
|
||||||
orderDirection,
|
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);
|
const response = await listarCentrosCusto(params);
|
||||||
|
|
@ -158,17 +192,24 @@ export function CentrosCustoPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function limparFiltros() {
|
function limparFiltros() {
|
||||||
setBusca('');
|
const filtrosLimpos = {
|
||||||
setSimular('');
|
busca: '',
|
||||||
setInvestimento('');
|
simular: '' as const,
|
||||||
setHabilitado('');
|
investimento: '' as const,
|
||||||
setOrderBy('descricao');
|
habilitado: '' as const,
|
||||||
setOrderDirection('ASC');
|
orderBy: 'descricao',
|
||||||
|
orderDirection: 'ASC' as const,
|
||||||
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
setBusca(filtrosLimpos.busca);
|
||||||
setPage(1);
|
setSimular(filtrosLimpos.simular);
|
||||||
carregarCentrosCusto(1);
|
setInvestimento(filtrosLimpos.investimento);
|
||||||
}, 0);
|
setHabilitado(filtrosLimpos.habilitado);
|
||||||
|
setOrderBy(filtrosLimpos.orderBy);
|
||||||
|
setOrderDirection(filtrosLimpos.orderDirection);
|
||||||
|
setPage(1);
|
||||||
|
|
||||||
|
carregarCentrosCusto(1, filtrosLimpos);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeletarCentroCusto(item: CentroCusto) {
|
async function handleDeletarCentroCusto(item: CentroCusto) {
|
||||||
|
|
@ -199,31 +240,26 @@ export function CentrosCustoPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAlterarStatusCentroCusto(item: CentroCusto) {
|
async function handleAlterarStatusCentroCusto(item: CentroCusto) {
|
||||||
const novoStatus = Number(item.habilitado) === 1 ? 0 : 1;
|
const novoStatus: 0 | 1 =
|
||||||
|
Number(item.habilitado) === 1 ? 0 : 1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setAlterandoStatusId(item.idcentrodecustos);
|
setAlterandoStatusId(item.idcentrodecustos);
|
||||||
setErro('');
|
setErro('');
|
||||||
setSucesso('');
|
setSucesso('');
|
||||||
|
|
||||||
const centroAtualizado = await alterarHabilitadoCentroCusto(
|
await alterarHabilitadoCentroCusto(
|
||||||
item.idcentrodecustos,
|
item.idcentrodecustos,
|
||||||
novoStatus
|
novoStatus
|
||||||
);
|
);
|
||||||
|
|
||||||
setCentros((listaAtual) =>
|
|
||||||
listaAtual.map((centro) =>
|
|
||||||
centro.idcentrodecustos === item.idcentrodecustos
|
|
||||||
? centroAtualizado
|
|
||||||
: centro
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
setSucesso(
|
setSucesso(
|
||||||
novoStatus === 1
|
novoStatus === 1
|
||||||
? 'Centro de custo habilitado com sucesso.'
|
? 'Centro de custo habilitado com sucesso.'
|
||||||
: 'Centro de custo desabilitado com sucesso.'
|
: 'Centro de custo desabilitado com sucesso.'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await carregarCentrosCusto(page);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message =
|
const message =
|
||||||
error?.response?.data?.message ||
|
error?.response?.data?.message ||
|
||||||
|
|
@ -307,7 +343,7 @@ export function CentrosCustoPage() {
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Centros encontrados
|
Centros de custo
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h5" fontWeight={950}>
|
<Typography variant="h5" fontWeight={950}>
|
||||||
{summary.quantidade}
|
{summary.quantidade}
|
||||||
|
|
@ -318,8 +354,9 @@ export function CentrosCustoPage() {
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Limite total
|
Limite simulado
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="h5" fontWeight={950}>
|
<Typography variant="h5" fontWeight={950}>
|
||||||
{formatarValor(summary.limiteTotal)}
|
{formatarValor(summary.limiteTotal)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
@ -340,7 +377,7 @@ export function CentrosCustoPage() {
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Simuláveis
|
Na simulação
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h5" fontWeight={950} color="success.main">
|
<Typography variant="h5" fontWeight={950} color="success.main">
|
||||||
{summary.simulaveis}
|
{summary.simulaveis}
|
||||||
|
|
@ -388,10 +425,14 @@ export function CentrosCustoPage() {
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
label="Simular"
|
label="Participa da simulação"
|
||||||
value={simular}
|
value={simular}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setSimular(event.target.value === '' ? '' : Number(event.target.value))
|
setSimular(
|
||||||
|
event.target.value === ''
|
||||||
|
? ''
|
||||||
|
: Number(event.target.value) as 0 | 1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
|
|
@ -529,16 +570,30 @@ export function CentrosCustoPage() {
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography fontWeight={950} whiteSpace="nowrap">
|
{Number(item.simular) === 1 ? (
|
||||||
{formatarValor(item.limite)}
|
<Typography fontWeight={950} whiteSpace="nowrap">
|
||||||
</Typography>
|
{formatarValor(item.limite)}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
whiteSpace="nowrap"
|
||||||
|
>
|
||||||
|
Fora da simulação
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack direction="row" flexWrap="wrap" spacing={1} useFlexGap>
|
<Stack direction="row" flexWrap="wrap" spacing={1} useFlexGap>
|
||||||
<Chip
|
<Chip
|
||||||
label={flagLabel(item.simular, 'Simular', 'Não simular')}
|
label={
|
||||||
|
Number(item.simular) === 1
|
||||||
|
? 'Na simulação'
|
||||||
|
: 'Fora da simulação'
|
||||||
|
}
|
||||||
size="small"
|
size="small"
|
||||||
color={flagColor(item.simular) as any}
|
color={Number(item.simular) === 1 ? 'success' : 'default'}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -655,16 +710,22 @@ export function CentrosCustoPage() {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<Typography fontWeight={950}>
|
{Number(item.simular) === 1 ? (
|
||||||
{formatarValor(item.limite)}
|
<Typography fontWeight={950}>
|
||||||
</Typography>
|
{formatarValor(item.limite)}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
-
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Chip
|
<Chip
|
||||||
label={Number(item.simular) === 1 ? 'Sim' : 'Não'}
|
label={Number(item.simular) === 1 ? 'Sim' : 'Não'}
|
||||||
size="small"
|
size="small"
|
||||||
color={flagColor(item.simular) as any}
|
color={Number(item.simular) === 1 ? 'success' : 'default'}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,20 @@ import type {
|
||||||
CentroCusto,
|
CentroCusto,
|
||||||
CentrosCustoListResponse,
|
CentrosCustoListResponse,
|
||||||
CriarCentroCustoRequest,
|
CriarCentroCustoRequest,
|
||||||
|
FlagNumerica,
|
||||||
} from '../types/centroCustoTypes';
|
} from '../types/centroCustoTypes';
|
||||||
|
|
||||||
export type OrderDirection = 'ASC' | 'DESC';
|
export type OrderDirection = 'ASC' | 'DESC';
|
||||||
|
|
||||||
|
export type CentroCustoOrderBy =
|
||||||
|
| 'descricao'
|
||||||
|
| 'limite'
|
||||||
|
| 'simular'
|
||||||
|
| 'investimento'
|
||||||
|
| 'habilitado'
|
||||||
|
| 'insert_date'
|
||||||
|
| 'update_date';
|
||||||
|
|
||||||
export type ListarCentrosCustoParams = {
|
export type ListarCentrosCustoParams = {
|
||||||
limite?: number;
|
limite?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
|
@ -17,29 +27,39 @@ export type ListarCentrosCustoParams = {
|
||||||
simular?: number | '';
|
simular?: number | '';
|
||||||
investimento?: number | '';
|
investimento?: number | '';
|
||||||
habilitado?: number | '';
|
habilitado?: number | '';
|
||||||
orderBy?: string;
|
orderBy?: CentroCustoOrderBy;
|
||||||
orderDirection?: OrderDirection;
|
orderDirection?: OrderDirection;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function listarCentrosCusto(
|
export async function listarCentrosCusto(
|
||||||
params?: ListarCentrosCustoParams
|
params?: ListarCentrosCustoParams
|
||||||
): Promise<CentrosCustoListResponse> {
|
): Promise<CentrosCustoListResponse> {
|
||||||
const response = await api.get<CentrosCustoListResponse>('/centros-custo', {
|
const response = await api.get<CentrosCustoListResponse>(
|
||||||
params,
|
'/centros-custo',
|
||||||
});
|
{ params }
|
||||||
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buscarCentroCustoPorId(id: number): Promise<CentroCusto> {
|
export async function buscarCentroCustoPorId(
|
||||||
const response = await api.get<ApiResponse<CentroCusto>>(`/centros-custo/${id}`);
|
id: number
|
||||||
|
): Promise<CentroCusto> {
|
||||||
|
const response = await api.get<ApiResponse<CentroCusto>>(
|
||||||
|
`/centros-custo/${id}`
|
||||||
|
);
|
||||||
|
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function criarCentroCusto(
|
export async function criarCentroCusto(
|
||||||
data: CriarCentroCustoRequest
|
data: CriarCentroCustoRequest
|
||||||
): Promise<CentroCusto> {
|
): 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;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,13 +67,17 @@ export async function atualizarCentroCusto(
|
||||||
id: number,
|
id: number,
|
||||||
data: AtualizarCentroCustoRequest
|
data: AtualizarCentroCustoRequest
|
||||||
): Promise<CentroCusto> {
|
): 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;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function alterarHabilitadoCentroCusto(
|
export async function alterarHabilitadoCentroCusto(
|
||||||
id: number,
|
id: number,
|
||||||
habilitado: number
|
habilitado: FlagNumerica
|
||||||
): Promise<CentroCusto> {
|
): Promise<CentroCusto> {
|
||||||
const response = await api.patch<ApiResponse<CentroCusto>>(
|
const response = await api.patch<ApiResponse<CentroCusto>>(
|
||||||
`/centros-custo/${id}/habilitado`,
|
`/centros-custo/${id}/habilitado`,
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,36 @@
|
||||||
|
export type FlagNumerica = 0 | 1;
|
||||||
|
|
||||||
export type CentroCusto = {
|
export type CentroCusto = {
|
||||||
idcentrodecustos: number;
|
idcentrodecustos: number;
|
||||||
descricao: string;
|
descricao: string;
|
||||||
limite: number;
|
|
||||||
simular: number;
|
/**
|
||||||
investimento: number;
|
* Dados globais do centro de custo.
|
||||||
habilitado: number;
|
*/
|
||||||
|
investimento: FlagNumerica;
|
||||||
|
habilitado: FlagNumerica;
|
||||||
insert_date: string | null;
|
insert_date: string | null;
|
||||||
update_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 = {
|
export type CriarCentroCustoRequest = {
|
||||||
descricao: string;
|
descricao: string;
|
||||||
limite: number;
|
investimento: FlagNumerica;
|
||||||
simular: number;
|
habilitado: FlagNumerica;
|
||||||
investimento: number;
|
|
||||||
habilitado: number;
|
/**
|
||||||
|
* 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;
|
export type AtualizarCentroCustoRequest = CriarCentroCustoRequest;
|
||||||
|
|
@ -29,10 +45,19 @@ export type CentrosCustoPagination = {
|
||||||
|
|
||||||
export type CentrosCustoSummary = {
|
export type CentrosCustoSummary = {
|
||||||
quantidade: number;
|
quantidade: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soma dos limites configurados pelo usuário logado.
|
||||||
|
*/
|
||||||
limiteTotal: number;
|
limiteTotal: number;
|
||||||
|
|
||||||
habilitados: number;
|
habilitados: number;
|
||||||
desabilitados: number;
|
desabilitados: number;
|
||||||
investimentos: number;
|
investimentos: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quantidade de centros vinculados à simulação do usuário.
|
||||||
|
*/
|
||||||
simulaveis: number;
|
simulaveis: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ import type {
|
||||||
MovimentosSummary,
|
MovimentosSummary,
|
||||||
} from '../types/movimentoTypes';
|
} from '../types/movimentoTypes';
|
||||||
import {
|
import {
|
||||||
listarBancos,
|
listarMeusBancos,
|
||||||
listarCentrosCusto,
|
listarCentrosCusto,
|
||||||
listarClientes,
|
listarClientes,
|
||||||
} from '../../referencias/services/referenciasService';
|
} from '../../referencias/services/referenciasService';
|
||||||
|
|
@ -559,7 +559,7 @@ export function MovimentosPage() {
|
||||||
setLoadingRefs(true);
|
setLoadingRefs(true);
|
||||||
|
|
||||||
const [bancosData, centrosData, clientesData] = await Promise.all([
|
const [bancosData, centrosData, clientesData] = await Promise.all([
|
||||||
listarBancos(),
|
listarMeusBancos(),
|
||||||
listarCentrosCusto(),
|
listarCentrosCusto(),
|
||||||
listarClientes(),
|
listarClientes(),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ function origemLabel(origem: string) {
|
||||||
movimento_fixo_previsto: 'Fixo previsto',
|
movimento_fixo_previsto: 'Fixo previsto',
|
||||||
saldo_banco: 'Saldo banco',
|
saldo_banco: 'Saldo banco',
|
||||||
cartao_credito_agrupado: 'Cartão',
|
cartao_credito_agrupado: 'Cartão',
|
||||||
limite_centro_custo: 'Limite centro',
|
limite_centro_custo: 'Previsão do centro',
|
||||||
manual: 'Manual',
|
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 && (
|
{item.cliente_nome && (
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
|
|
@ -415,7 +440,7 @@ export function SimulacaoMensalPage() {
|
||||||
setBase(data);
|
setBase(data);
|
||||||
setEntradas(data.entradas || []);
|
setEntradas(data.entradas || []);
|
||||||
setSaidas(data.saidas || []);
|
setSaidas(data.saidas || []);
|
||||||
setSucesso('Previsão preenchida com sucesso.');
|
setSucesso('Simulação mensal montada com sucesso.');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message =
|
const message =
|
||||||
error?.response?.data?.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.movimentosExistentes} movimentos existentes`} variant="outlined" />
|
||||||
<Chip label={`${base.fontes.movimentosFixos} fixos analisados`} variant="outlined" />
|
<Chip label={`${base.fontes.movimentosFixos} fixos analisados`} variant="outlined" />
|
||||||
<Chip label={`${base.fontes.bancos} bancos`} 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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { api } from '../../../services/api';
|
import { api } from '../../../services/api';
|
||||||
import type {
|
import type {
|
||||||
|
ApiResponse,
|
||||||
BuscarBaseSimulacaoMensalParams,
|
BuscarBaseSimulacaoMensalParams,
|
||||||
SimulacaoMensalBase,
|
SimulacaoMensalBase,
|
||||||
} from '../types/simulacaoMensalTypes';
|
} from '../types/simulacaoMensalTypes';
|
||||||
|
|
@ -7,14 +8,18 @@ import type {
|
||||||
export async function buscarBaseSimulacaoMensal(
|
export async function buscarBaseSimulacaoMensal(
|
||||||
params: BuscarBaseSimulacaoMensalParams
|
params: BuscarBaseSimulacaoMensalParams
|
||||||
): Promise<SimulacaoMensalBase> {
|
): Promise<SimulacaoMensalBase> {
|
||||||
const response = await api.get('/simulacao-mensal/base', {
|
const response = await api.get<ApiResponse<SimulacaoMensalBase>>(
|
||||||
params: {
|
'/simulacao-mensal/base',
|
||||||
ano: params.ano,
|
{
|
||||||
mes: params.mes,
|
params: {
|
||||||
incluirQuitadas: params.incluirQuitadas ? 1 : 0,
|
ano: params.ano,
|
||||||
incluirFixosVencidosDoMesAtual: params.incluirFixosVencidosDoMesAtual ? 1 : 0,
|
mes: params.mes,
|
||||||
},
|
incluirQuitadas: params.incluirQuitadas ? 1 : 0,
|
||||||
});
|
incluirFixosVencidosDoMesAtual:
|
||||||
|
params.incluirFixosVencidosDoMesAtual ? 1 : 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,34 @@ export type SimulacaoMensalOrigemItem =
|
||||||
| 'limite_centro_custo'
|
| 'limite_centro_custo'
|
||||||
| 'manual';
|
| '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 = {
|
export type SimulacaoMensalItem = {
|
||||||
id: string;
|
id: string;
|
||||||
tipo: SimulacaoMensalTipoItem;
|
tipo: SimulacaoMensalTipoItem;
|
||||||
origem: SimulacaoMensalOrigemItem | string;
|
origem: SimulacaoMensalOrigemItem;
|
||||||
|
|
||||||
descricao: string;
|
descricao: string;
|
||||||
valor: number;
|
valor: number;
|
||||||
|
|
@ -38,32 +62,37 @@ export type SimulacaoMensalItem = {
|
||||||
editavel: boolean;
|
editavel: boolean;
|
||||||
editado: boolean;
|
editado: boolean;
|
||||||
|
|
||||||
detalhes?: any;
|
detalhes?: SimulacaoMensalDetalhes;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SimulacaoMensalResumo = {
|
export type SimulacaoMensalResumo = {
|
||||||
totalEntradas: number;
|
totalEntradas: number;
|
||||||
totalSaidas: number;
|
totalSaidas: number;
|
||||||
totalGastos?: number;
|
totalGastos: number;
|
||||||
totalInvestimentos?: number;
|
totalInvestimentos: number;
|
||||||
resultado: number;
|
resultado: number;
|
||||||
|
|
||||||
percentualEntradas?: number | null;
|
percentualEntradas: number | null;
|
||||||
percentualSaidas?: number | null;
|
percentualSaidas: number | null;
|
||||||
percentualGastos?: number | null;
|
percentualGastos: number | null;
|
||||||
percentualInvestimentos?: number | null;
|
percentualInvestimentos: number | null;
|
||||||
percentualResultado?: number | null;
|
percentualResultado: number | null;
|
||||||
percentualComprometido: number | null;
|
percentualComprometido: number | null;
|
||||||
|
|
||||||
quantidadeEntradas: number;
|
quantidadeEntradas: number;
|
||||||
quantidadeSaidas: number;
|
quantidadeSaidas: number;
|
||||||
quantidadeInvestimentos?: number;
|
quantidadeInvestimentos: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SimulacaoMensalAviso = {
|
export type SimulacaoMensalAviso = {
|
||||||
tipo: string;
|
tipo: string;
|
||||||
mensagem: string;
|
mensagem: string;
|
||||||
[key: string]: any;
|
idcentrodecustos?: number;
|
||||||
|
centro_custo_descricao?: string;
|
||||||
|
limite?: number;
|
||||||
|
gastoAtual?: number;
|
||||||
|
excesso?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SimulacaoMensalBase = {
|
export type SimulacaoMensalBase = {
|
||||||
|
|
@ -98,4 +127,10 @@ export type BuscarBaseSimulacaoMensalParams = {
|
||||||
mes: number;
|
mes: number;
|
||||||
incluirQuitadas?: boolean;
|
incluirQuitadas?: boolean;
|
||||||
incluirFixosVencidosDoMesAtual?: boolean;
|
incluirFixosVencidosDoMesAtual?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiResponse<T> = {
|
||||||
|
ok: boolean;
|
||||||
|
message?: string;
|
||||||
|
data: T;
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue