diff --git a/financeiro-api/src/modules/centrosCusto/controllers/centrosCusto.controller.js b/financeiro-api/src/modules/centrosCusto/controllers/centrosCusto.controller.js
index 33c98c4..2ed2157 100644
--- a/financeiro-api/src/modules/centrosCusto/controllers/centrosCusto.controller.js
+++ b/financeiro-api/src/modules/centrosCusto/controllers/centrosCusto.controller.js
@@ -1,40 +1,72 @@
const centrosCustoService = require('../services/centrosCusto.service');
+function obterIdUsuario(req) {
+ return Number(
+ req.user?.idusuarios
+ ?? req.user?.idusuario
+ ?? req.user?.id
+ );
+}
+
+function normalizarFlagEntrada(valor) {
+ if (valor === undefined || valor === null || valor === '') {
+ return undefined;
+ }
+
+ if (valor === true || valor === 'true') {
+ return 1;
+ }
+
+ if (valor === false || valor === 'false') {
+ return 0;
+ }
+
+ return Number(valor);
+}
+
function validarDadosCentroCusto(dados) {
if (!dados.descricao || !String(dados.descricao).trim()) {
return 'Descrição é obrigatória.';
}
- if (dados.limite !== undefined && dados.limite !== null && dados.limite !== '') {
+ const simular = normalizarFlagEntrada(dados.simular);
+
+ if (simular !== undefined && ![0, 1].includes(simular)) {
+ return 'Campo simular inválido.';
+ }
+
+ if (simular === 1) {
+ if (dados.limite === undefined || dados.limite === null || dados.limite === '') {
+ return 'Informe o limite para incluir o centro de custo na simulação.';
+ }
+
const limite = Number(dados.limite);
- if (!Number.isFinite(limite)) {
+ if (!Number.isFinite(limite) || limite < 0) {
+ return 'Limite inválido.';
+ }
+ } else if (
+ dados.limite !== undefined
+ && dados.limite !== null
+ && dados.limite !== ''
+ ) {
+ const limite = Number(dados.limite);
+
+ if (!Number.isFinite(limite) || limite < 0) {
return 'Limite inválido.';
}
}
- if (dados.simular !== undefined && dados.simular !== null && dados.simular !== '') {
- const simular = Number(dados.simular);
+ const investimento = normalizarFlagEntrada(dados.investimento);
- if (![0, 1].includes(simular)) {
- return 'Campo simular inválido.';
- }
+ if (investimento !== undefined && ![0, 1].includes(investimento)) {
+ return 'Campo investimento inválido.';
}
- if (dados.investimento !== undefined && dados.investimento !== null && dados.investimento !== '') {
- const investimento = Number(dados.investimento);
+ const habilitado = normalizarFlagEntrada(dados.habilitado);
- if (![0, 1].includes(investimento)) {
- return 'Campo investimento inválido.';
- }
- }
-
- if (dados.habilitado !== undefined && dados.habilitado !== null && dados.habilitado !== '') {
- const habilitado = Number(dados.habilitado);
-
- if (![0, 1].includes(habilitado)) {
- return 'Status inválido.';
- }
+ if (habilitado !== undefined && ![0, 1].includes(habilitado)) {
+ return 'Status inválido.';
}
return null;
@@ -42,7 +74,17 @@ function validarDadosCentroCusto(dados) {
async function listar(req, res) {
try {
+ const idUsuario = obterIdUsuario(req);
+
+ if (!idUsuario) {
+ return res.status(401).json({
+ ok: false,
+ message: 'Usuário não identificado.',
+ });
+ }
+
const resultado = await centrosCustoService.listarCentrosCusto({
+ idUsuario,
limite: req.query.limite,
offset: req.query.offset,
page: req.query.page,
@@ -73,6 +115,7 @@ async function listar(req, res) {
async function buscarPorId(req, res) {
try {
const id = Number(req.params.id);
+ const idUsuario = obterIdUsuario(req);
if (!id) {
return res.status(400).json({
@@ -81,7 +124,17 @@ async function buscarPorId(req, res) {
});
}
- const centroCusto = await centrosCustoService.buscarCentroCustoPorId(id);
+ if (!idUsuario) {
+ return res.status(401).json({
+ ok: false,
+ message: 'Usuário não identificado.',
+ });
+ }
+
+ const centroCusto = await centrosCustoService.buscarCentroCustoPorId(
+ id,
+ idUsuario
+ );
if (!centroCusto) {
return res.status(404).json({
@@ -106,6 +159,15 @@ async function buscarPorId(req, res) {
async function criar(req, res) {
try {
+ const idUsuario = obterIdUsuario(req);
+
+ if (!idUsuario) {
+ return res.status(401).json({
+ ok: false,
+ message: 'Usuário não identificado.',
+ });
+ }
+
const erroValidacao = validarDadosCentroCusto(req.body);
if (erroValidacao) {
@@ -115,7 +177,10 @@ async function criar(req, res) {
});
}
- const novoCentroCusto = await centrosCustoService.criarCentroCusto(req.body);
+ const novoCentroCusto = await centrosCustoService.criarCentroCusto(
+ req.body,
+ idUsuario
+ );
return res.status(201).json({
ok: true,
@@ -135,6 +200,7 @@ async function criar(req, res) {
async function atualizar(req, res) {
try {
const id = Number(req.params.id);
+ const idUsuario = obterIdUsuario(req);
if (!id) {
return res.status(400).json({
@@ -143,6 +209,13 @@ async function atualizar(req, res) {
});
}
+ if (!idUsuario) {
+ return res.status(401).json({
+ ok: false,
+ message: 'Usuário não identificado.',
+ });
+ }
+
const erroValidacao = validarDadosCentroCusto(req.body);
if (erroValidacao) {
@@ -152,7 +225,11 @@ async function atualizar(req, res) {
});
}
- const centroAtualizado = await centrosCustoService.atualizarCentroCusto(id, req.body);
+ const centroAtualizado = await centrosCustoService.atualizarCentroCusto(
+ id,
+ req.body,
+ idUsuario
+ );
if (!centroAtualizado) {
return res.status(404).json({
@@ -179,6 +256,7 @@ async function atualizar(req, res) {
async function alterarHabilitado(req, res) {
try {
const id = Number(req.params.id);
+ const idUsuario = obterIdUsuario(req);
if (!id) {
return res.status(400).json({
@@ -187,7 +265,14 @@ async function alterarHabilitado(req, res) {
});
}
- const habilitado = Number(req.body.habilitado);
+ if (!idUsuario) {
+ return res.status(401).json({
+ ok: false,
+ message: 'Usuário não identificado.',
+ });
+ }
+
+ const habilitado = normalizarFlagEntrada(req.body.habilitado);
if (![0, 1].includes(habilitado)) {
return res.status(400).json({
@@ -196,7 +281,12 @@ async function alterarHabilitado(req, res) {
});
}
- const centroAtualizado = await centrosCustoService.alterarHabilitadoCentroCusto(id, habilitado);
+ const centroAtualizado =
+ await centrosCustoService.alterarHabilitadoCentroCusto(
+ id,
+ habilitado,
+ idUsuario
+ );
if (!centroAtualizado) {
return res.status(404).json({
@@ -270,4 +360,4 @@ module.exports = {
atualizar,
alterarHabilitado,
deletar,
-};
\ No newline at end of file
+};
diff --git a/financeiro-api/src/modules/centrosCusto/routes/centrosCusto.routes.js b/financeiro-api/src/modules/centrosCusto/routes/centrosCusto.routes.js
index a025212..e2fd2a6 100644
--- a/financeiro-api/src/modules/centrosCusto/routes/centrosCusto.routes.js
+++ b/financeiro-api/src/modules/centrosCusto/routes/centrosCusto.routes.js
@@ -13,4 +13,4 @@ router.put('/:id', centrosCustoController.atualizar);
router.patch('/:id/habilitado', centrosCustoController.alterarHabilitado);
router.delete('/:id', centrosCustoController.deletar);
-module.exports = router;
\ No newline at end of file
+module.exports = router;
diff --git a/financeiro-api/src/modules/centrosCusto/services/centrosCusto.service.js b/financeiro-api/src/modules/centrosCusto/services/centrosCusto.service.js
index 6b840bc..daa960a 100644
--- a/financeiro-api/src/modules/centrosCusto/services/centrosCusto.service.js
+++ b/financeiro-api/src/modules/centrosCusto/services/centrosCusto.service.js
@@ -1,14 +1,17 @@
const pool = require('../../../database/mysql');
const CAMPOS_CENTRO_CUSTO_SELECT = `
- idcentrodecustos,
- descricao,
- limite,
- simular,
- investimento,
- habilitado,
- insert_date,
- update_date
+ cc.idcentrodecustos,
+ cc.descricao,
+ cc.investimento,
+ cc.habilitado,
+ cc.insert_date,
+ cc.update_date,
+ CASE
+ WHEN ccs.idcentrodecustossimulacao IS NOT NULL THEN 1
+ ELSE 0
+ END AS simular,
+ ccs.limite
`;
function normalizarTextoOuNull(valor) {
@@ -38,9 +41,15 @@ function normalizarFlag(valor, padrao = 0) {
return padrao;
}
- const numero = Number(valor);
+ if (valor === true || valor === 'true') {
+ return 1;
+ }
- return numero === 1 ? 1 : 0;
+ if (valor === false || valor === 'false') {
+ return 0;
+ }
+
+ return Number(valor) === 1 ? 1 : 0;
}
function limitarNumero(valor, padrao, minimo, maximo) {
@@ -63,17 +72,17 @@ function limitarNumero(valor, padrao, minimo, maximo) {
function resolverOrdenacao(orderBy) {
const camposPermitidos = {
- idcentrodecustos: 'idcentrodecustos',
- descricao: 'descricao',
- limite: 'limite',
+ idcentrodecustos: 'cc.idcentrodecustos',
+ descricao: 'cc.descricao',
+ limite: 'ccs.limite',
simular: 'simular',
- investimento: 'investimento',
- habilitado: 'habilitado',
- insert_date: 'insert_date',
- update_date: 'update_date',
+ investimento: 'cc.investimento',
+ habilitado: 'cc.habilitado',
+ insert_date: 'cc.insert_date',
+ update_date: 'cc.update_date',
};
- return camposPermitidos[orderBy] || 'descricao';
+ return camposPermitidos[orderBy] || 'cc.descricao';
}
function resolverDirecao(orderDirection) {
@@ -84,41 +93,51 @@ function montarWhereCentrosCusto(filtros = {}) {
const where = [];
const params = [];
- if (filtros.simular !== undefined && filtros.simular !== null && filtros.simular !== '') {
- where.push('simular = ?');
- params.push(Number(filtros.simular));
+ if (
+ filtros.simular !== undefined
+ && filtros.simular !== null
+ && filtros.simular !== ''
+ ) {
+ const simular = normalizarFlag(filtros.simular, 0);
+
+ where.push(
+ simular === 1
+ ? 'ccs.idcentrodecustossimulacao IS NOT NULL'
+ : 'ccs.idcentrodecustossimulacao IS NULL'
+ );
}
- if (filtros.investimento !== undefined && filtros.investimento !== null && filtros.investimento !== '') {
- where.push('investimento = ?');
- params.push(Number(filtros.investimento));
+ if (
+ filtros.investimento !== undefined
+ && filtros.investimento !== null
+ && filtros.investimento !== ''
+ ) {
+ where.push('cc.investimento = ?');
+ params.push(normalizarFlag(filtros.investimento, 0));
}
- if (filtros.habilitado !== undefined && filtros.habilitado !== null && filtros.habilitado !== '') {
- where.push('habilitado = ?');
- params.push(Number(filtros.habilitado));
+ if (
+ filtros.habilitado !== undefined
+ && filtros.habilitado !== null
+ && filtros.habilitado !== ''
+ ) {
+ where.push('cc.habilitado = ?');
+ params.push(normalizarFlag(filtros.habilitado, 0));
}
if (filtros.busca) {
- where.push(`
- (
- descricao LIKE ?
- )
- `);
-
- const termo = `%${String(filtros.busca).trim()}%`;
- params.push(termo);
+ where.push('cc.descricao LIKE ?');
+ params.push(`%${String(filtros.busca).trim()}%`);
}
- const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
-
return {
- whereSql,
+ whereSql: where.length > 0 ? `WHERE ${where.join(' AND ')}` : '',
params,
};
}
async function listarCentrosCusto(filtros = {}) {
+ const idUsuario = Number(filtros.idUsuario);
const limite = limitarNumero(filtros.limite, 20, 1, 100);
const page = limitarNumero(filtros.page, 1, 1, 999999);
const offset = filtros.offset !== undefined
@@ -127,50 +146,69 @@ async function listarCentrosCusto(filtros = {}) {
const orderBy = resolverOrdenacao(filtros.orderBy);
const orderDirection = resolverDirecao(filtros.orderDirection);
-
const { whereSql, params } = montarWhereCentrosCusto(filtros);
+ const joinSql = `
+ FROM centrodecustos cc
+ LEFT JOIN centrodecustossimulacao ccs
+ ON ccs.idcentrodecustos = cc.idcentrodecustos
+ AND ccs.idusuarios = ?
+ `;
+
+ const queryParams = [idUsuario, ...params];
+
const [rows] = await pool.query(
`
SELECT
${CAMPOS_CENTRO_CUSTO_SELECT}
- FROM centrodecustos
+ ${joinSql}
${whereSql}
- ORDER BY ${orderBy} ${orderDirection}, idcentrodecustos ASC
+ ORDER BY ${orderBy} ${orderDirection}, cc.idcentrodecustos ASC
LIMIT ? OFFSET ?
`,
- [...params, limite, offset]
+ [...queryParams, limite, offset]
);
const [countRows] = await pool.query(
`
SELECT COUNT(*) AS total
- FROM centrodecustos
+ ${joinSql}
${whereSql}
`,
- params
+ queryParams
);
const [summaryRows] = await pool.query(
`
SELECT
COUNT(*) AS quantidade,
- COALESCE(SUM(limite), 0) AS limiteTotal,
- COALESCE(SUM(CASE WHEN habilitado = 1 THEN 1 ELSE 0 END), 0) AS habilitados,
- COALESCE(SUM(CASE WHEN habilitado = 0 THEN 1 ELSE 0 END), 0) AS desabilitados,
- COALESCE(SUM(CASE WHEN investimento = 1 THEN 1 ELSE 0 END), 0) AS investimentos,
- COALESCE(SUM(CASE WHEN simular = 1 THEN 1 ELSE 0 END), 0) AS simulaveis
- FROM centrodecustos
+ COALESCE(SUM(ccs.limite), 0) AS limiteTotal,
+ COALESCE(SUM(CASE WHEN cc.habilitado = 1 THEN 1 ELSE 0 END), 0) AS habilitados,
+ COALESCE(SUM(CASE WHEN cc.habilitado = 0 THEN 1 ELSE 0 END), 0) AS desabilitados,
+ COALESCE(SUM(CASE WHEN cc.investimento = 1 THEN 1 ELSE 0 END), 0) AS investimentos,
+ COALESCE(SUM(
+ CASE
+ WHEN ccs.idcentrodecustossimulacao IS NOT NULL THEN 1
+ ELSE 0
+ END
+ ), 0) AS simulaveis
+ ${joinSql}
${whereSql}
`,
- params
+ queryParams
);
const total = Number(countRows[0]?.total || 0);
const totalPages = Math.max(1, Math.ceil(total / limite));
return {
- data: rows,
+ data: rows.map((row) => ({
+ ...row,
+ limite: row.limite === null ? null : Number(row.limite),
+ simular: Number(row.simular),
+ investimento: Number(row.investimento),
+ habilitado: Number(row.habilitado),
+ })),
pagination: {
total,
limite,
@@ -189,102 +227,178 @@ async function listarCentrosCusto(filtros = {}) {
};
}
-async function buscarCentroCustoPorId(id) {
- const [rows] = await pool.query(
+async function buscarCentroCustoPorId(id, idUsuario, executor = pool) {
+ const [rows] = await executor.query(
`
SELECT
${CAMPOS_CENTRO_CUSTO_SELECT}
- FROM centrodecustos
- WHERE idcentrodecustos = ?
+ FROM centrodecustos cc
+ LEFT JOIN centrodecustossimulacao ccs
+ ON ccs.idcentrodecustos = cc.idcentrodecustos
+ AND ccs.idusuarios = ?
+ WHERE cc.idcentrodecustos = ?
LIMIT 1
`,
- [id]
+ [idUsuario, id]
);
- return rows[0] || null;
+ const centro = rows[0];
+
+ if (!centro) {
+ return null;
+ }
+
+ return {
+ ...centro,
+ limite: centro.limite === null ? null : Number(centro.limite),
+ simular: Number(centro.simular),
+ investimento: Number(centro.investimento),
+ habilitado: Number(centro.habilitado),
+ };
}
-async function criarCentroCusto(dados) {
- const {
- descricao,
- limite,
- simular,
- investimento,
- habilitado,
- } = dados;
+async function sincronizarSimulacao(
+ executor,
+ idCentroCusto,
+ idUsuario,
+ dados
+) {
+ const simular = normalizarFlag(dados.simular, 0);
+ if (simular === 1) {
+ await executor.query(
+ `
+ INSERT INTO centrodecustossimulacao (
+ idcentrodecustos,
+ idusuarios,
+ limite,
+ insert_date,
+ update_date
+ ) VALUES (?, ?, ?, NOW(), NOW())
+ ON DUPLICATE KEY UPDATE
+ limite = VALUES(limite),
+ update_date = NOW()
+ `,
+ [
+ idCentroCusto,
+ idUsuario,
+ normalizarNumero(dados.limite, 0),
+ ]
+ );
+
+ return;
+ }
+
+ await executor.query(
+ `
+ DELETE FROM centrodecustossimulacao
+ WHERE idcentrodecustos = ?
+ AND idusuarios = ?
+ `,
+ [idCentroCusto, idUsuario]
+ );
+}
+
+async function criarCentroCusto(dados, idUsuario) {
+ const connection = await pool.getConnection();
+
+ try {
+ await connection.beginTransaction();
+
+ const [result] = await connection.query(
+ `
+ INSERT INTO centrodecustos (
+ descricao,
+ investimento,
+ habilitado,
+ insert_date,
+ update_date
+ ) VALUES (?, ?, ?, NOW(), NOW())
+ `,
+ [
+ normalizarTextoOuNull(dados.descricao),
+ normalizarFlag(dados.investimento, 0),
+ normalizarFlag(dados.habilitado, 1),
+ ]
+ );
+
+ await sincronizarSimulacao(
+ connection,
+ result.insertId,
+ idUsuario,
+ dados
+ );
+
+ await connection.commit();
+
+ return buscarCentroCustoPorId(result.insertId, idUsuario);
+ } catch (error) {
+ await connection.rollback();
+ throw error;
+ } finally {
+ connection.release();
+ }
+}
+
+async function atualizarCentroCusto(id, dados, idUsuario) {
+ const connection = await pool.getConnection();
+
+ try {
+ await connection.beginTransaction();
+
+ const [rows] = await connection.query(
+ `
+ SELECT idcentrodecustos
+ FROM centrodecustos
+ WHERE idcentrodecustos = ?
+ LIMIT 1
+ `,
+ [id]
+ );
+
+ if (!rows[0]) {
+ await connection.rollback();
+ return null;
+ }
+
+ await connection.query(
+ `
+ UPDATE centrodecustos
+ SET
+ descricao = ?,
+ investimento = ?,
+ habilitado = ?,
+ update_date = NOW()
+ WHERE idcentrodecustos = ?
+ `,
+ [
+ normalizarTextoOuNull(dados.descricao),
+ normalizarFlag(dados.investimento, 0),
+ normalizarFlag(dados.habilitado, 1),
+ id,
+ ]
+ );
+
+ await sincronizarSimulacao(
+ connection,
+ id,
+ idUsuario,
+ dados
+ );
+
+ await connection.commit();
+
+ return buscarCentroCustoPorId(id, idUsuario);
+ } catch (error) {
+ await connection.rollback();
+ throw error;
+ } finally {
+ connection.release();
+ }
+}
+
+async function alterarHabilitadoCentroCusto(id, habilitado, idUsuario) {
const [result] = await pool.query(
- `
- INSERT INTO centrodecustos (
- descricao,
- limite,
- simular,
- investimento,
- habilitado,
- insert_date,
- update_date
- ) VALUES (?, ?, ?, ?, ?, NOW(), NOW())
- `,
- [
- normalizarTextoOuNull(descricao),
- normalizarNumero(limite, 0),
- normalizarFlag(simular, 0),
- normalizarFlag(investimento, 0),
- normalizarFlag(habilitado, 1),
- ]
- );
-
- return buscarCentroCustoPorId(result.insertId);
-}
-
-async function atualizarCentroCusto(id, dados) {
- const centroAtual = await buscarCentroCustoPorId(id);
-
- if (!centroAtual) {
- return null;
- }
-
- const {
- descricao,
- limite,
- simular,
- investimento,
- habilitado,
- } = dados;
-
- await pool.query(
- `
- UPDATE centrodecustos
- SET
- descricao = ?,
- limite = ?,
- simular = ?,
- investimento = ?,
- habilitado = ?,
- update_date = NOW()
- WHERE idcentrodecustos = ?
- `,
- [
- normalizarTextoOuNull(descricao),
- normalizarNumero(limite, 0),
- normalizarFlag(simular, 0),
- normalizarFlag(investimento, 0),
- normalizarFlag(habilitado, 1),
- id,
- ]
- );
-
- return buscarCentroCustoPorId(id);
-}
-
-async function alterarHabilitadoCentroCusto(id, habilitado) {
- const centroAtual = await buscarCentroCustoPorId(id);
-
- if (!centroAtual) {
- return null;
- }
-
- await pool.query(
`
UPDATE centrodecustos
SET
@@ -298,17 +412,15 @@ async function alterarHabilitadoCentroCusto(id, habilitado) {
]
);
- return buscarCentroCustoPorId(id);
+ if (result.affectedRows === 0) {
+ return null;
+ }
+
+ return buscarCentroCustoPorId(id, idUsuario);
}
async function deletarCentroCusto(id) {
- const centroAtual = await buscarCentroCustoPorId(id);
-
- if (!centroAtual) {
- return false;
- }
-
- await pool.query(
+ const [result] = await pool.query(
`
DELETE FROM centrodecustos
WHERE idcentrodecustos = ?
@@ -316,7 +428,7 @@ async function deletarCentroCusto(id) {
[id]
);
- return true;
+ return result.affectedRows > 0;
}
module.exports = {
@@ -326,4 +438,4 @@ module.exports = {
atualizarCentroCusto,
alterarHabilitadoCentroCusto,
deletarCentroCusto,
-};
\ No newline at end of file
+};
diff --git a/financeiro-api/src/modules/dashboard/controllers/dashboard.controller.js b/financeiro-api/src/modules/dashboard/controllers/dashboard.controller.js
index 9998fce..2f6fd96 100644
--- a/financeiro-api/src/modules/dashboard/controllers/dashboard.controller.js
+++ b/financeiro-api/src/modules/dashboard/controllers/dashboard.controller.js
@@ -1,7 +1,15 @@
const dashboardService = require('../services/dashboard.service');
function resolverIdUsuario(req) {
- return Number(req.user?.idusuarios || req.user?.id);
+ return Number(
+ req.usuario?.idusuarios
+ ?? req.user?.idusuarios
+ ?? req.usuario?.idusuario
+ ?? req.user?.idusuario
+ ?? req.usuario?.id
+ ?? req.user?.id
+ ?? 0
+ );
}
async function buscarResumo(req, res) {
@@ -27,9 +35,12 @@ async function buscarResumo(req, res) {
} catch (error) {
console.error('[dashboard.controller] buscarResumo:', error);
- return res.status(500).json({
+ return res.status(error.statusCode || 500).json({
ok: false,
- message: 'Não foi possível carregar o resumo da dashboard.',
+ message:
+ error.statusCode
+ ? error.message
+ : 'Não foi possível carregar o resumo da dashboard.',
});
}
}
diff --git a/financeiro-api/src/modules/dashboard/services/dashboard.service.js b/financeiro-api/src/modules/dashboard/services/dashboard.service.js
index 875cc17..01b100d 100644
--- a/financeiro-api/src/modules/dashboard/services/dashboard.service.js
+++ b/financeiro-api/src/modules/dashboard/services/dashboard.service.js
@@ -1,34 +1,49 @@
const pool = require('../../../database/mysql');
+function formatarDataLocal(data) {
+ const ano = data.getFullYear();
+ const mes = String(data.getMonth() + 1).padStart(2, '0');
+ const dia = String(data.getDate()).padStart(2, '0');
+
+ return `${ano}-${mes}-${dia}`;
+}
+
function hojeISO() {
- return new Date().toISOString().slice(0, 10);
+ return formatarDataLocal(new Date());
}
function inicioMesAtualISO() {
const hoje = new Date();
- return new Date(hoje.getFullYear(), hoje.getMonth(), 1)
- .toISOString()
- .slice(0, 10);
+ return formatarDataLocal(
+ new Date(hoje.getFullYear(), hoje.getMonth(), 1)
+ );
}
function fimMesAtualISO() {
const hoje = new Date();
- return new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0)
- .toISOString()
- .slice(0, 10);
+ return formatarDataLocal(
+ new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0)
+ );
}
function normalizarDataISO(valor, fallback) {
if (!valor) return fallback;
const texto = String(valor).slice(0, 10);
- const data = new Date(`${texto}T00:00:00`);
- if (Number.isNaN(data.getTime())) {
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(texto)) {
return fallback;
}
- return texto;
+ const [ano, mes, dia] = texto.split('-').map(Number);
+ const data = new Date(ano, mes - 1, dia);
+
+ const valida =
+ data.getFullYear() === ano
+ && data.getMonth() === mes - 1
+ && data.getDate() === dia;
+
+ return valida ? texto : fallback;
}
function normalizarNumero(valor, padrao = 0) {
@@ -84,10 +99,7 @@ async function buscarCardsBancos(idUsuario) {
COUNT(*) AS quantidadeCarteiras,
- SUM(CASE
- WHEN habilitado = 1 THEN 1
- ELSE 0
- END) AS carteirasHabilitadas,
+ COUNT(*) AS carteirasHabilitadas,
SUM(CASE
WHEN debito = 1
@@ -113,6 +125,7 @@ async function buscarCardsBancos(idUsuario) {
END) AS quantidadeCreditos
FROM bancos
WHERE idusuarios = ?
+ AND habilitado = 1
`,
[idUsuario]
);
@@ -149,16 +162,18 @@ async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) {
ELSE 0
END), 0) AS entradas,
- COALESCE(SUM(CASE
- WHEN cp.movimento = 'Saida' THEN cp.valor
- ELSE 0
+ COALESCE(SUM(CASE
+ WHEN cp.movimento IN ('Saida', 'Sangria')
+ AND COALESCE(cc.investimento, 0) = 0
+ THEN cp.valor
+ ELSE 0
END), 0) AS saidas,
- COALESCE(SUM(CASE
- WHEN cp.movimento = 'Sangria'
+ COALESCE(SUM(CASE
+ WHEN cp.movimento IN ('Saida', 'Sangria')
AND COALESCE(cc.investimento, 0) = 1
- THEN cp.valor
- ELSE 0
+ THEN cp.valor
+ ELSE 0
END), 0) AS investimentos,
COALESCE(SUM(CASE
@@ -175,34 +190,36 @@ async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) {
ELSE NULL
END) AS quantidadeAbertoEntradas,
- COALESCE(SUM(CASE
+ COALESCE(SUM(CASE
WHEN cp.status = 'A pagar'
- AND cp.movimento = 'Saida'
- THEN cp.valor
- ELSE 0
+ AND cp.movimento IN ('Saida', 'Sangria')
+ AND COALESCE(cc.investimento, 0) = 0
+ THEN cp.valor
+ ELSE 0
END), 0) AS abertoSaidas,
- COUNT(CASE
+ COUNT(CASE
WHEN cp.status = 'A pagar'
- AND cp.movimento = 'Saida'
- THEN 1
- ELSE NULL
+ AND cp.movimento IN ('Saida', 'Sangria')
+ AND COALESCE(cc.investimento, 0) = 0
+ THEN 1
+ ELSE NULL
END) AS quantidadeAbertoSaidas,
- COALESCE(SUM(CASE
+ COALESCE(SUM(CASE
WHEN cp.status = 'A pagar'
- AND cp.movimento = 'Sangria'
+ AND cp.movimento IN ('Saida', 'Sangria')
AND COALESCE(cc.investimento, 0) = 1
- THEN cp.valor
- ELSE 0
+ THEN cp.valor
+ ELSE 0
END), 0) AS abertoInvestimentos,
- COUNT(CASE
+ COUNT(CASE
WHEN cp.status = 'A pagar'
- AND cp.movimento = 'Sangria'
+ AND cp.movimento IN ('Saida', 'Sangria')
AND COALESCE(cc.investimento, 0) = 1
- THEN 1
- ELSE NULL
+ THEN 1
+ ELSE NULL
END) AS quantidadeAbertoInvestimentos,
COALESCE(SUM(CASE
@@ -212,7 +229,7 @@ async function buscarCardsMovimentos(idUsuario, dataInicio, dataFim) {
COUNT(*) AS quantidadeMovimentos
FROM contasapagar cp
- LEFT JOIN bancos b
+ INNER JOIN bancos b
ON b.idbancos = cp.idbancos
LEFT JOIN centrodecustos cc
ON cc.idcentrodecustos = cp.idcentrodecustos
@@ -320,8 +337,9 @@ async function buscarAlertasVencimento(idUsuario) {
COALESCE(SUM(CASE
WHEN DATE(cp.datavencimento) >= ?
AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
- AND cp.movimento = 'Saida'
+ AND cp.movimento IN ('Saida', 'Sangria')
AND cp.status = 'A pagar'
+ AND COALESCE(cc.investimento, 0) = 0
THEN cp.valor
ELSE 0
END), 0) AS venceEmBreveSaidas,
@@ -329,8 +347,9 @@ async function buscarAlertasVencimento(idUsuario) {
COUNT(CASE
WHEN DATE(cp.datavencimento) >= ?
AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
- AND cp.movimento = 'Saida'
+ AND cp.movimento IN ('Saida', 'Sangria')
AND cp.status = 'A pagar'
+ AND COALESCE(cc.investimento, 0) = 0
THEN 1
ELSE NULL
END) AS quantidadeVenceEmBreveSaidas,
@@ -338,7 +357,7 @@ async function buscarAlertasVencimento(idUsuario) {
COALESCE(SUM(CASE
WHEN DATE(cp.datavencimento) >= ?
AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
- AND cp.movimento = 'Sangria'
+ AND cp.movimento IN ('Saida', 'Sangria')
AND cp.status = 'A pagar'
AND COALESCE(cc.investimento, 0) = 1
THEN cp.valor
@@ -348,7 +367,7 @@ async function buscarAlertasVencimento(idUsuario) {
COUNT(CASE
WHEN DATE(cp.datavencimento) >= ?
AND DATE(cp.datavencimento) <= DATE_ADD(?, INTERVAL 7 DAY)
- AND cp.movimento = 'Sangria'
+ AND cp.movimento IN ('Saida', 'Sangria')
AND cp.status = 'A pagar'
AND COALESCE(cc.investimento, 0) = 1
THEN 1
@@ -430,7 +449,7 @@ async function buscarProximosVencimentos(idUsuario) {
cc.descricao AS centro_custo_descricao,
DATEDIFF(DATE(cp.datavencimento), CURDATE()) AS dias_para_vencer
FROM contasapagar cp
- LEFT JOIN bancos bc ON bc.idbancos = cp.idbancos
+ INNER JOIN bancos bc ON bc.idbancos = cp.idbancos
LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos
WHERE cp.deleted_at IS NULL
AND bc.idusuarios = ?
@@ -630,7 +649,7 @@ async function buscarUltimosMovimentos(idUsuario) {
cp.idcentrodecustos,
cc.descricao AS centro_custo_descricao
FROM contasapagar cp
- LEFT JOIN bancos bc ON bc.idbancos = cp.idbancos
+ INNER JOIN bancos bc ON bc.idbancos = cp.idbancos
LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos
WHERE cp.deleted_at IS NULL
AND bc.idusuarios = ?
@@ -655,7 +674,8 @@ async function buscarGraficos(idUsuario, dataInicio, dataFim) {
LEFT JOIN bancos b
ON b.idbancos = cp.idbancos
WHERE cp.deleted_at IS NULL
- AND cp.movimento IN ('Saida')
+ AND cp.movimento IN ('Saida', 'Sangria')
+ AND COALESCE(cc.investimento, 0) = 0
AND DATE(cp.datavencimento) BETWEEN ? AND ?
AND b.idusuarios = ?
GROUP BY
@@ -676,16 +696,18 @@ async function buscarGraficos(idUsuario, dataInicio, dataFim) {
ELSE 0
END), 0) AS entradas,
- COALESCE(SUM(CASE
- WHEN cp.movimento = 'Saida' THEN cp.valor
- ELSE 0
+ COALESCE(SUM(CASE
+ WHEN cp.movimento IN ('Saida', 'Sangria')
+ AND COALESCE(cc.investimento, 0) = 0
+ THEN cp.valor
+ ELSE 0
END), 0) AS saidas,
- COALESCE(SUM(CASE
- WHEN cp.movimento = 'Sangria'
- AND COALESCE(cc.investimento, 0) = 1
- THEN cp.valor
- ELSE 0
+ COALESCE(SUM(CASE
+ WHEN cp.movimento IN ('Saida', 'Sangria')
+ AND COALESCE(cc.investimento, 0) = 1
+ THEN cp.valor
+ ELSE 0
END), 0) AS investimentos
FROM contasapagar cp
LEFT JOIN centrodecustos cc
@@ -753,6 +775,12 @@ async function buscarResumoDashboard(idUsuario, filtros = {}) {
const dataInicio = normalizarDataISO(filtros.dataInicio, inicioMesAtualISO());
const dataFim = normalizarDataISO(filtros.dataFim, fimMesAtualISO());
+ if (dataInicio > dataFim) {
+ const error = new Error('A data inicial não pode ser maior que a data final.');
+ error.statusCode = 400;
+ throw error;
+ }
+
const [
cardsBancos,
cardsMovimentos,
diff --git a/financeiro-api/src/modules/simulacaoMensal/controllers/simulacaoMensal.controller.js b/financeiro-api/src/modules/simulacaoMensal/controllers/simulacaoMensal.controller.js
index e35baf1..2be6886 100644
--- a/financeiro-api/src/modules/simulacaoMensal/controllers/simulacaoMensal.controller.js
+++ b/financeiro-api/src/modules/simulacaoMensal/controllers/simulacaoMensal.controller.js
@@ -1,12 +1,14 @@
const simulacaoMensalService = require('../services/simulacaoMensal.service');
function extrairIdUsuarioLogado(req) {
- return (
- req.usuario?.idusuarios ||
- req.user?.idusuarios ||
- req.usuario?.id ||
- req.user?.id ||
- null
+ return Number(
+ req.usuario?.idusuarios
+ ?? req.user?.idusuarios
+ ?? req.usuario?.idusuario
+ ?? req.user?.idusuario
+ ?? req.usuario?.id
+ ?? req.user?.id
+ ?? 0
);
}
diff --git a/financeiro-api/src/modules/simulacaoMensal/services/simulacaoMensal.service.js b/financeiro-api/src/modules/simulacaoMensal/services/simulacaoMensal.service.js
index 1b7bc3e..7f0821e 100644
--- a/financeiro-api/src/modules/simulacaoMensal/services/simulacaoMensal.service.js
+++ b/financeiro-api/src/modules/simulacaoMensal/services/simulacaoMensal.service.js
@@ -310,23 +310,29 @@ async function buscarFixosUsuario(connection, idUsuario) {
return rows;
}
-async function buscarCentrosSimulaveis(connection) {
+async function buscarCentrosSimulaveis(connection, idUsuario) {
const [rows] = await connection.query(
`
SELECT
- idcentrodecustos,
- descricao,
- limite,
- simular,
- investimento
- FROM centrodecustos
- WHERE habilitado = 1
- AND simular = 1
- ORDER BY descricao ASC
- `
+ cc.idcentrodecustos,
+ cc.descricao,
+ ccs.limite,
+ cc.investimento
+ FROM centrodecustossimulacao ccs
+ INNER JOIN centrodecustos cc
+ ON cc.idcentrodecustos = ccs.idcentrodecustos
+ WHERE ccs.idusuarios = ?
+ AND cc.habilitado = 1
+ ORDER BY cc.descricao ASC
+ `,
+ [idUsuario]
);
- return rows;
+ return rows.map((row) => ({
+ ...row,
+ limite: Number(row.limite || 0),
+ investimento: Number(row.investimento || 0),
+ }));
}
function movimentoFixoJaExisteNoMes(fixo, movimentosDoMes) {
@@ -716,7 +722,7 @@ async function montarBaseSimulacaoMensal(opcoes = {}) {
dataFim,
}),
buscarFixosUsuario(connection, idUsuario),
- buscarCentrosSimulaveis(connection),
+ buscarCentrosSimulaveis(connection, idUsuario),
buscarMovimentosParaCentrosDoMes(connection, {
idUsuario,
dataInicio,
diff --git a/financeiro-web/index.html b/financeiro-web/index.html
index 3f2b345..d9fe18c 100644
--- a/financeiro-web/index.html
+++ b/financeiro-web/index.html
@@ -2,9 +2,16 @@
-
+
- financeiro-web
+ Zendion Finance
+
+
+
+
diff --git a/financeiro-web/public/icon.png b/financeiro-web/public/icon.png
new file mode 100644
index 0000000..d239b09
Binary files /dev/null and b/financeiro-web/public/icon.png differ
diff --git a/financeiro-web/src/components/layout/AppLayout.tsx b/financeiro-web/src/components/layout/AppLayout.tsx
index f14f800..04ab346 100644
--- a/financeiro-web/src/components/layout/AppLayout.tsx
+++ b/financeiro-web/src/components/layout/AppLayout.tsx
@@ -9,12 +9,27 @@ const SIDEBAR_WIDTH = 280;
export function AppLayout() {
const [mobileOpen, setMobileOpen] = useState(false);
+ function abrirMenuMobile() {
+ setMobileOpen(true);
+ }
+
+ function fecharMenuMobile() {
+ setMobileOpen(false);
+ }
+
return (
-
+
setMobileOpen(false)}
- ModalProps={{ keepMounted: true }}
+ onClose={fecharMenuMobile}
+ ModalProps={{
+ keepMounted: true,
+ }}
sx={{
display: { xs: 'block', md: 'none' },
+
'& .MuiDrawer-paper': {
width: SIDEBAR_WIDTH,
border: 0,
+ overflow: 'hidden',
},
}}
>
- setMobileOpen(false)} />
+
- setMobileOpen(true)} />
+
diff --git a/financeiro-web/src/components/layout/Sidebar.tsx b/financeiro-web/src/components/layout/Sidebar.tsx
index 77d508b..fd79c2c 100644
--- a/financeiro-web/src/components/layout/Sidebar.tsx
+++ b/financeiro-web/src/components/layout/Sidebar.tsx
@@ -1,3 +1,4 @@
+import type { ReactNode } from 'react';
import {
Box,
Divider,
@@ -5,170 +6,331 @@ import {
ListItemButton,
ListItemIcon,
ListItemText,
+ Stack,
Typography,
} from '@mui/material';
-import DashboardIcon from '@mui/icons-material/Dashboard';
-import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
-import AssessmentIcon from '@mui/icons-material/Assessment';
-import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
-import SavingsIcon from '@mui/icons-material/Savings';
-import SettingsIcon from '@mui/icons-material/Settings';
-import PeopleAltIcon from '@mui/icons-material/PeopleAlt';
-import AccountTreeIcon from '@mui/icons-material/AccountTree';
-import EventRepeatIcon from '@mui/icons-material/EventRepeat';
-import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
-import CreditCardIcon from '@mui/icons-material/CreditCard';
-import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
+import DashboardRoundedIcon from '@mui/icons-material/DashboardRounded';
+import SwapHorizRoundedIcon from '@mui/icons-material/SwapHorizRounded';
+import AssessmentRoundedIcon from '@mui/icons-material/AssessmentRounded';
+import AccountBalanceWalletRoundedIcon from '@mui/icons-material/AccountBalanceWalletRounded';
+import SavingsRoundedIcon from '@mui/icons-material/SavingsRounded';
+import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded';
+import PeopleAltRoundedIcon from '@mui/icons-material/PeopleAltRounded';
+import AccountTreeRoundedIcon from '@mui/icons-material/AccountTreeRounded';
+import EventRepeatRoundedIcon from '@mui/icons-material/EventRepeatRounded';
+import ManageAccountsRoundedIcon from '@mui/icons-material/ManageAccountsRounded';
+import CreditCardRoundedIcon from '@mui/icons-material/CreditCardRounded';
+import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
+import InsightsRoundedIcon from '@mui/icons-material/InsightsRounded';
import { NavLink } from 'react-router-dom';
type SidebarProps = {
onNavigate?: () => void;
};
-const menuItems = [
+type MenuItem = {
+ label: string;
+ path: string;
+ icon: ReactNode;
+};
+
+type MenuSection = {
+ title: string;
+ items: MenuItem[];
+};
+
+const menuSections: MenuSection[] = [
{
- label: 'Dashboard',
- path: '/dashboard',
- icon: ,
+ title: 'Visão geral',
+ items: [
+ {
+ label: 'Dashboard',
+ path: '/dashboard',
+ icon: ,
+ },
+ {
+ label: 'Simulação mensal',
+ path: '/simulacao-mensal',
+ icon: ,
+ },
+ ],
},
{
- label: 'Movimentos',
- path: '/movimentos',
- icon: ,
+ title: 'Financeiro',
+ items: [
+ {
+ label: 'Movimentos',
+ path: '/movimentos',
+ icon: ,
+ },
+ {
+ label: 'Movimentos fixos',
+ path: '/movimentos-fixos',
+ icon: ,
+ },
+ {
+ label: 'Quitar créditos',
+ path: '/quitacoes-credito',
+ icon: ,
+ },
+ {
+ label: 'Carteiras',
+ path: '/bancos',
+ icon: ,
+ },
+ {
+ label: 'Rendimentos',
+ path: '/rendimentos',
+ icon: ,
+ },
+ ],
},
{
- label: 'Quitar créditos',
- path: '/quitacoes-credito',
- icon: ,
+ title: 'Cadastros',
+ items: [
+ {
+ label: 'Clientes',
+ path: '/clientes',
+ icon: ,
+ },
+ {
+ label: 'Centros de custo',
+ path: '/centros-custo',
+ icon: ,
+ },
+ ],
},
{
- label: 'Clientes',
- path: '/clientes',
- icon: ,
+ title: 'Análise',
+ items: [
+ {
+ label: 'Relatórios',
+ path: '/relatorios',
+ icon: ,
+ },
+ ],
},
{
- label: 'Carteiras',
- path: '/bancos',
- icon: ,
- },
- {
- label: 'Rendimentos',
- path: '/rendimentos',
- icon: ,
- },
- {
- label: 'Centros de custo',
- path: '/centros-custo',
- icon: ,
- },
- {
- label: 'Movimentos fixos',
- path: '/movimentos-fixos',
- icon: ,
- },
- {
- label: 'Usuários',
- path: '/usuarios',
- icon: ,
- },
- {
- label: 'Relatórios',
- path: '/relatorios',
- icon: ,
- },
- {
- label: 'Simulação mensal',
- path: '/simulacao-mensal',
- icon: ,
- },
- {
- label: 'Configurações',
- path: '/configuracoes',
- icon: ,
+ title: 'Administração',
+ items: [
+ {
+ label: 'Usuários',
+ path: '/usuarios',
+ icon: ,
+ },
+ {
+ label: 'Configurações',
+ path: '/configuracoes',
+ icon: ,
+ },
+ ],
},
];
export function Sidebar({ onNavigate }: SidebarProps) {
return (
-
-
- Zendion
-
+
+
+
+
+
-
- Financeiro
-
+
+
+ Zendion Finance
+
+
+
+ Gestão financeira
+
+
+
-
- {menuItems.map((item) => (
-
- {item.icon}
+
+ {menuSections.map((section) => (
+
+
-
+ >
+ {section.title}
+
+
+ {section.items.map((item) => (
+
+ {item.icon}
+
+
+
+ ))}
+
))}
+
+
-
- MVP ativo
+
+ Zendion Finance
-
- Movimentos e carteiras em operação.
+
+ Versão 1.0.0
);
-}
\ No newline at end of file
+}
diff --git a/financeiro-web/src/components/layout/Topbar.tsx b/financeiro-web/src/components/layout/Topbar.tsx
index 64e198a..434e4b8 100644
--- a/financeiro-web/src/components/layout/Topbar.tsx
+++ b/financeiro-web/src/components/layout/Topbar.tsx
@@ -2,28 +2,146 @@ import {
AppBar,
Avatar,
Box,
+ Chip,
IconButton,
Stack,
Toolbar,
+ Tooltip,
Typography,
} from '@mui/material';
-import MenuIcon from '@mui/icons-material/Menu';
-import LogoutIcon from '@mui/icons-material/Logout';
-import { useNavigate } from 'react-router-dom';
+import MenuRoundedIcon from '@mui/icons-material/MenuRounded';
+import LogoutRoundedIcon from '@mui/icons-material/LogoutRounded';
+import { useLocation, useNavigate } from 'react-router-dom';
import { useAuthStore } from '../../features/auth/store/authStore';
type TopbarProps = {
onMenuClick: () => void;
};
+type RouteInfo = {
+ eyebrow: string;
+ title: string;
+};
+
+const routeInfoMap: Record = {
+ '/dashboard': {
+ eyebrow: 'Visão geral',
+ title: 'Dashboard',
+ },
+ '/movimentos': {
+ eyebrow: 'Financeiro',
+ title: 'Movimentos',
+ },
+ '/quitacoes-credito': {
+ eyebrow: 'Financeiro',
+ title: 'Quitar créditos',
+ },
+ '/clientes': {
+ eyebrow: 'Cadastros',
+ title: 'Clientes',
+ },
+ '/bancos': {
+ eyebrow: 'Financeiro',
+ title: 'Carteiras',
+ },
+ '/rendimentos': {
+ eyebrow: 'Financeiro',
+ title: 'Rendimentos',
+ },
+ '/centros-custo': {
+ eyebrow: 'Cadastros',
+ title: 'Centros de custo',
+ },
+ '/movimentos-fixos': {
+ eyebrow: 'Financeiro',
+ title: 'Movimentos fixos',
+ },
+ '/usuarios': {
+ eyebrow: 'Administração',
+ title: 'Usuários',
+ },
+ '/relatorios': {
+ eyebrow: 'Análise',
+ title: 'Relatórios',
+ },
+ '/simulacao-mensal': {
+ eyebrow: 'Visão geral',
+ title: 'Simulação mensal',
+ },
+ '/configuracoes': {
+ eyebrow: 'Administração',
+ title: 'Configurações',
+ },
+};
+
+function resolverInformacaoRota(pathname: string): RouteInfo {
+ const rotaExata = routeInfoMap[pathname];
+
+ if (rotaExata) {
+ return rotaExata;
+ }
+
+ const rotaBase = Object.keys(routeInfoMap)
+ .sort((a, b) => b.length - a.length)
+ .find((rota) => pathname.startsWith(`${rota}/`));
+
+ if (rotaBase) {
+ const base = routeInfoMap[rotaBase];
+
+ if (pathname.endsWith('/novo')) {
+ return {
+ eyebrow: base.eyebrow,
+ title: `Novo ${base.title.toLowerCase()}`,
+ };
+ }
+
+ if (pathname.endsWith('/editar')) {
+ return {
+ eyebrow: base.eyebrow,
+ title: `Editar ${base.title.toLowerCase()}`,
+ };
+ }
+
+ return base;
+ }
+
+ return {
+ eyebrow: 'Zendion Finance',
+ title: 'Gestão financeira',
+ };
+}
+
+function obterIniciais(nome?: string | null) {
+ const partes = String(nome || '')
+ .trim()
+ .split(/\s+/)
+ .filter(Boolean);
+
+ if (partes.length === 0) {
+ return 'U';
+ }
+
+ if (partes.length === 1) {
+ return partes[0].charAt(0).toUpperCase();
+ }
+
+ return `${partes[0].charAt(0)}${partes[partes.length - 1].charAt(0)}`
+ .toUpperCase();
+}
+
export function Topbar({ onMenuClick }: TopbarProps) {
const navigate = useNavigate();
+ const location = useLocation();
+
const user = useAuthStore((state) => state.user);
const logout = useAuthStore((state) => state.logout);
+ const routeInfo = resolverInformacaoRota(location.pathname);
+ const iniciais = obterIniciais(user?.nome);
+
function handleLogout() {
logout();
- navigate('/login');
+ navigate('/login', { replace: true });
}
return (
@@ -31,60 +149,171 @@ export function Topbar({ onMenuClick }: TopbarProps) {
position="sticky"
elevation={0}
sx={{
- backgroundColor: '#FFFFFF',
+ zIndex: (theme) => theme.zIndex.drawer - 1,
color: '#111827',
+ backgroundColor: 'rgba(255,255,255,0.92)',
+ backdropFilter: 'blur(14px)',
borderBottom: '1px solid',
- borderColor: 'divider',
+ borderColor: 'rgba(15,23,42,0.08)',
+ boxShadow: '0 8px 24px rgba(15,23,42,0.04)',
}}
>
-
-
-
-
+
+
+
-
- Painel financeiro
+ '&:hover': {
+ backgroundColor: 'rgba(59,130,246,0.08)',
+ },
+ }}
+ >
+
+
+
+
+
+
+ {routeInfo.eyebrow}
-
- Controle de movimentos
+
+ {routeInfo.title}
-
-
+
- {user?.nome?.charAt(0)?.toUpperCase() || 'U'}
-
+
+ {iniciais}
+
-
-
- {user?.nome || 'Usuário'}
-
-
- Logado
-
-
+
+
+ {user?.nome || 'Usuário'}
+
-
-
-
+
+ Sessão ativa
+
+
+
+
+
+
+
+
+
+
+
);
-}
\ No newline at end of file
+}
diff --git a/financeiro-web/src/features/auth/pages/LoginPage.tsx b/financeiro-web/src/features/auth/pages/LoginPage.tsx
index 5e7306e..7a65419 100644
--- a/financeiro-web/src/features/auth/pages/LoginPage.tsx
+++ b/financeiro-web/src/features/auth/pages/LoginPage.tsx
@@ -1,4 +1,5 @@
import { useState } from 'react';
+import type { FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Alert,
@@ -9,30 +10,42 @@ import {
Chip,
CircularProgress,
Divider,
+ IconButton,
+ InputAdornment,
Stack,
TextField,
+ Tooltip,
Typography,
} from '@mui/material';
-import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
+import AutoGraphRoundedIcon from '@mui/icons-material/AutoGraphRounded';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
-import TrendingUpIcon from '@mui/icons-material/TrendingUp';
import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined';
+import TrendingUpRoundedIcon from '@mui/icons-material/TrendingUpRounded';
+import VisibilityOffRoundedIcon from '@mui/icons-material/VisibilityOffRounded';
+import VisibilityRoundedIcon from '@mui/icons-material/VisibilityRounded';
import { loginRequest } from '../services/authService';
import { useAuthStore } from '../store/authStore';
+const APP_NAME = 'Zendion Finance';
+const APP_VERSION = '1.0.0';
+const LOGO_SRC = '/icon.png';
+
export function LoginPage() {
const navigate = useNavigate();
const setAuth = useAuthStore((state) => state.setAuth);
const [usuario, setUsuario] = useState('');
const [senha, setSenha] = useState('');
+ const [mostrarSenha, setMostrarSenha] = useState(false);
const [loading, setLoading] = useState(false);
const [erro, setErro] = useState('');
- async function handleLogin(event: React.FormEvent) {
+ const formularioValido = Boolean(usuario.trim() && senha);
+
+ async function handleLogin(event: FormEvent) {
event.preventDefault();
- if (!usuario.trim() || !senha.trim()) {
+ if (!usuario.trim() || !senha) {
setErro('Informe usuário e senha.');
return;
}
@@ -47,11 +60,11 @@ export function LoginPage() {
});
setAuth(result.user, result.token);
- navigate('/dashboard');
+ navigate('/dashboard', { replace: true });
} catch (error: any) {
const message =
error?.response?.data?.message ||
- 'Não foi possível realizar o login.';
+ 'Não foi possível realizar o login. Verifique seus dados e tente novamente.';
setErro(message);
} finally {
@@ -65,217 +78,442 @@ export function LoginPage() {
display="grid"
gridTemplateColumns={{
xs: '1fr',
- md: '1.1fr 0.9fr',
+ md: 'minmax(480px, 1.08fr) minmax(420px, 0.92fr)',
}}
sx={{
background:
- 'radial-gradient(circle at top left, rgba(37,99,235,0.18), transparent 34%), linear-gradient(135deg, #F8FAFC 0%, #EEF2F7 48%, #E5E7EB 100%)',
+ 'radial-gradient(circle at top right, rgba(59,130,246,0.12), transparent 32%), linear-gradient(135deg, #F8FAFC 0%, #EEF2F7 48%, #E5E7EB 100%)',
}}
>
-
-
+
+
+
+
-
+
-
- Zendion
+
+ {APP_NAME}
-
- Financeiro
+
+
+ Gestão financeira
}
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',
+ },
+ }}
/>
-
- Movimentos, carteiras e saldos em um painel limpo.
+
+ Clareza para decidir.
+
+ Controle para evoluir.
- 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.
-
-
-
-
- Acesso protegido por autenticação e token.
-
+
+
+
+
+
+
+
+
+ Ambiente protegido
+
+
+
+ Acesso autenticado e dados separados por usuário.
+
+
-
-
-
- Base pronta para relatórios, dashboards e controle por carteiras.
-
+
+
+
+
+
+
+
+ Visão financeira completa
+
+
+
+ Dashboards, relatórios e planejamento mensal.
+
+
-
-
-
-
+
+
+
+
+ {APP_NAME}
+
+
+
+ Gestão financeira
+
+
+
+
+
+
+
-
+
+
+
+
+
+ Bem-vindo
+
+
+
+ Entre com suas credenciais para continuar.
+
+
+
+ {erro && (
+ setErro('')}
+ sx={{ marginBottom: 2.5, borderRadius: 2.5 }}
+ >
+ {erro}
+
+ )}
+
+
+ {
+ setUsuario(event.target.value);
+ if (erro) setErro('');
+ }}
+ fullWidth
+ autoFocus
+ autoComplete="username"
+ disabled={loading}
+ inputProps={{
+ maxLength: 100,
+ }}
+ />
+
+ {
+ setSenha(event.target.value);
+ if (erro) setErro('');
+ }}
+ fullWidth
+ autoComplete="current-password"
+ disabled={loading}
+ InputProps={{
+ endAdornment: (
+
+
+ setMostrarSenha((valor) => !valor)}
+ aria-label={
+ mostrarSenha
+ ? 'Ocultar senha'
+ : 'Mostrar senha'
+ }
+ disabled={loading}
+ >
+ {mostrarSenha ? (
+
+ ) : (
+
+ )}
+
+
+
+ ),
+ }}
+ />
+
+
+ :
+ }
+ 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'}
+
-
- Entrar
-
+
-
- Acesse o Zendion Financeiro para continuar.
-
-
-
- {erro && (
-
- {erro}
-
- )}
-
-
- setUsuario(event.target.value)}
- fullWidth
- autoFocus
- />
-
- setSenha(event.target.value)}
- fullWidth
- />
-
- : null}
- sx={{
- minHeight: 50,
- marginTop: 1,
- boxShadow: 3,
- }}
+
- {loading ? 'Entrando...' : 'Entrar no sistema'}
-
-
+
+ Sistema privado da Zendion INC.
+
-
+
+ Versão {APP_VERSION}
+
+
+
+
-
- Sistema financeiro privado da Zendion INC.
-
-
-
+
+ Use apenas credenciais autorizadas.
+
+
);
-}
\ No newline at end of file
+}
diff --git a/financeiro-web/src/features/centrosCusto/components/CentroCustoForm.tsx b/financeiro-web/src/features/centrosCusto/components/CentroCustoForm.tsx
index c5786dd..4d23172 100644
--- a/financeiro-web/src/features/centrosCusto/components/CentroCustoForm.tsx
+++ b/financeiro-web/src/features/centrosCusto/components/CentroCustoForm.tsx
@@ -9,15 +9,18 @@ import {
Chip,
CircularProgress,
Divider,
+ FormControlLabel,
MenuItem,
Paper,
Stack,
+ Switch,
TextField,
Typography,
} from '@mui/material';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import SaveIcon from '@mui/icons-material/Save';
+import SavingsIcon from '@mui/icons-material/Savings';
import { useNavigate } from 'react-router-dom';
import {
atualizarCentroCusto,
@@ -26,6 +29,7 @@ import {
import type {
CentroCusto,
CriarCentroCustoRequest,
+ FlagNumerica,
} from '../types/centroCustoTypes';
type CentroCustoFormProps = {
@@ -40,7 +44,11 @@ type FormSectionProps = {
};
function formatarValorResumo(value: string) {
- const numero = Number(value || 0);
+ const numero = Number(value);
+
+ if (!value || !Number.isFinite(numero)) {
+ return 'Não informado';
+ }
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
@@ -101,7 +109,6 @@ const fieldSx = {
export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
const navigate = useNavigate();
-
const isEdit = mode === 'edit';
const [saving, setSaving] = useState(false);
@@ -109,44 +116,67 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
const [sucesso, setSucesso] = useState('');
const [descricao, setDescricao] = useState('');
- const [limite, setLimite] = useState('0');
- const [simular, setSimular] = useState('0');
- const [investimento, setInvestimento] = useState('0');
- const [habilitado, setHabilitado] = useState('1');
+ const [investimento, setInvestimento] = useState(0);
+ const [habilitado, setHabilitado] = useState(1);
+
+ const [simular, setSimular] = useState(0);
+ const [limite, setLimite] = useState('');
const titulo = isEdit ? 'Editar centro de custo' : 'Novo centro de custo';
+
const subtitulo = isEdit
- ? 'Atualize os dados do centro de custo selecionado.'
- : 'Cadastre categorias financeiras para classificar os movimentos.';
+ ? 'Atualize os dados globais e a configuração da sua simulação.'
+ : 'Cadastre uma categoria financeira e, se desejar, inclua-a na sua simulação.';
useEffect(() => {
if (!initialData) return;
setDescricao(initialData.descricao || '');
- setLimite(String(initialData.limite ?? 0));
- setSimular(String(initialData.simular ?? 0));
- setInvestimento(String(initialData.investimento ?? 0));
- setHabilitado(String(initialData.habilitado ?? 1));
+ setInvestimento(Number(initialData.investimento) === 1 ? 1 : 0);
+ setHabilitado(Number(initialData.habilitado) === 1 ? 1 : 0);
+ setSimular(Number(initialData.simular) === 1 ? 1 : 0);
+ setLimite(
+ initialData.limite === null || initialData.limite === undefined
+ ? ''
+ : String(initialData.limite)
+ );
}, [initialData]);
function limparFormulario() {
setDescricao('');
- setLimite('0');
- setSimular('0');
- setInvestimento('0');
- setHabilitado('1');
+ setInvestimento(0);
+ setHabilitado(1);
+ setSimular(0);
+ setLimite('');
+ }
+
+ function validarFormulario() {
+ if (!descricao.trim()) {
+ return 'Informe a descrição.';
+ }
+
+ if (simular === 1) {
+ if (limite === '') {
+ return 'Informe o limite da simulação.';
+ }
+
+ const limiteNumero = Number(limite);
+
+ if (!Number.isFinite(limiteNumero) || limiteNumero < 0) {
+ return 'Informe um limite válido para a simulação.';
+ }
+ }
+
+ return null;
}
async function handleSubmit(event: FormEvent) {
event.preventDefault();
- if (!descricao.trim()) {
- setErro('Informe a descrição.');
- return;
- }
+ const erroValidacao = validarFormulario();
- if (limite === '' || !Number.isFinite(Number(limite))) {
- setErro('Informe um limite válido.');
+ if (erroValidacao) {
+ setErro(erroValidacao);
return;
}
@@ -157,10 +187,10 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
const payload: CriarCentroCustoRequest = {
descricao: descricao.trim(),
- limite: Number(limite || 0),
- simular: Number(simular || 0),
- investimento: Number(investimento || 0),
- habilitado: Number(habilitado || 1),
+ investimento,
+ habilitado,
+ simular,
+ limite: simular === 1 ? Number(limite) : null,
};
if (isEdit) {
@@ -169,7 +199,11 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
return;
}
- await atualizarCentroCusto(initialData.idcentrodecustos, payload);
+ await atualizarCentroCusto(
+ initialData.idcentrodecustos,
+ payload
+ );
+
setSucesso('Centro de custo atualizado com sucesso.');
} else {
await criarCentroCusto(payload);
@@ -256,7 +290,7 @@ export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
>
setDescricao(event.target.value)}
placeholder="Ex: Administrativo, Comercial, Impostos..."
fullWidth
+ required
sx={fieldSx}
/>
- setLimite(event.target.value)}
- inputProps={{
- step: '0.01',
- }}
- fullWidth
- sx={fieldSx}
- />
-
- setSimular(event.target.value)}
- fullWidth
- sx={fieldSx}
- >
-
-
-
-
setInvestimento(event.target.value)}
+ onChange={(event) =>
+ setInvestimento(Number(event.target.value) === 1 ? 1 : 0)
+ }
fullWidth
sx={fieldSx}
>
-
-
+
+
setHabilitado(event.target.value)}
+ onChange={(event) =>
+ setHabilitado(Number(event.target.value) === 1 ? 1 : 0)
+ }
fullWidth
sx={fieldSx}
>
-
-
+
+
+
+
+
+ setSimular(event.target.checked ? 1 : 0)
+ }
+ color="success"
+ />
+ }
+ label={
+
+
+ Incluir na minha simulação
+
+
+
+ Ao desativar, o vínculo deste centro com a sua
+ simulação será removido.
+
+
+ }
+ sx={{
+ margin: 0,
+ alignItems: 'center',
+ }}
+ />
+
+
+ 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}
+ />
+
+
+
+
+
+
+
+
+
+ Simulação
+
+
+
+
+
+ Participação
+
+
+
+ {simular === 1
+ ? 'Incluído na simulação'
+ : 'Fora da simulação'}
+
+
+
+ {simular === 1 && (
+
+
+ Limite previsto
+
+
+
+ {formatarValorResumo(limite)}
+
+
+ )}
);
-}
\ No newline at end of file
+}
diff --git a/financeiro-web/src/features/centrosCusto/pages/CentrosCustoPage.tsx b/financeiro-web/src/features/centrosCusto/pages/CentrosCustoPage.tsx
index 7385c4d..c3a23ed 100644
--- a/financeiro-web/src/features/centrosCusto/pages/CentrosCustoPage.tsx
+++ b/financeiro-web/src/features/centrosCusto/pages/CentrosCustoPage.tsx
@@ -48,11 +48,24 @@ import type {
const LIMITE_PADRAO = 20;
-function formatarValor(valor: number) {
+type FiltrosCentrosCusto = {
+ busca: string;
+ simular: 0 | 1 | '';
+ investimento: 0 | 1 | '';
+ habilitado: 0 | 1 | '';
+ orderBy: string;
+ orderDirection: 'ASC' | 'DESC';
+};
+
+function formatarValor(valor: number | null | undefined) {
+ if (valor === null || valor === undefined) {
+ return '-';
+ }
+
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
- }).format(Number(valor || 0));
+ }).format(Number(valor));
}
function formatarData(data: string | null) {
@@ -119,7 +132,10 @@ export function CentrosCustoPage() {
return count;
}, [busca, simular, investimento, habilitado]);
- async function carregarCentrosCusto(pageToLoad = page) {
+ async function carregarCentrosCusto(
+ pageToLoad = page,
+ filtros?: Partial
+ ) {
try {
setLoading(true);
setErro('');
@@ -127,12 +143,30 @@ export function CentrosCustoPage() {
const params: ListarCentrosCustoParams = {
limite: LIMITE_PADRAO,
page: pageToLoad,
- busca: busca.trim() || undefined,
- simular,
- investimento,
- habilitado,
- orderBy,
- orderDirection,
+ busca:
+ filtros?.busca !== undefined
+ ? filtros.busca.trim() || undefined
+ : busca.trim() || undefined,
+ simular:
+ filtros?.simular !== undefined
+ ? filtros.simular
+ : simular,
+ investimento:
+ filtros?.investimento !== undefined
+ ? filtros.investimento
+ : investimento,
+ habilitado:
+ filtros?.habilitado !== undefined
+ ? filtros.habilitado
+ : habilitado,
+ orderBy:
+ filtros?.orderBy !== undefined
+ ? filtros.orderBy
+ : orderBy,
+ orderDirection:
+ filtros?.orderDirection !== undefined
+ ? filtros.orderDirection
+ : orderDirection,
};
const response = await listarCentrosCusto(params);
@@ -158,17 +192,24 @@ export function CentrosCustoPage() {
}
function limparFiltros() {
- setBusca('');
- setSimular('');
- setInvestimento('');
- setHabilitado('');
- setOrderBy('descricao');
- setOrderDirection('ASC');
+ const filtrosLimpos = {
+ busca: '',
+ simular: '' as const,
+ investimento: '' as const,
+ habilitado: '' as const,
+ orderBy: 'descricao',
+ orderDirection: 'ASC' as const,
+ };
- setTimeout(() => {
- setPage(1);
- carregarCentrosCusto(1);
- }, 0);
+ setBusca(filtrosLimpos.busca);
+ setSimular(filtrosLimpos.simular);
+ setInvestimento(filtrosLimpos.investimento);
+ setHabilitado(filtrosLimpos.habilitado);
+ setOrderBy(filtrosLimpos.orderBy);
+ setOrderDirection(filtrosLimpos.orderDirection);
+ setPage(1);
+
+ carregarCentrosCusto(1, filtrosLimpos);
}
async function handleDeletarCentroCusto(item: CentroCusto) {
@@ -199,31 +240,26 @@ export function CentrosCustoPage() {
}
async function handleAlterarStatusCentroCusto(item: CentroCusto) {
- const novoStatus = Number(item.habilitado) === 1 ? 0 : 1;
+ const novoStatus: 0 | 1 =
+ Number(item.habilitado) === 1 ? 0 : 1;
try {
setAlterandoStatusId(item.idcentrodecustos);
setErro('');
setSucesso('');
- const centroAtualizado = await alterarHabilitadoCentroCusto(
+ await alterarHabilitadoCentroCusto(
item.idcentrodecustos,
novoStatus
);
- setCentros((listaAtual) =>
- listaAtual.map((centro) =>
- centro.idcentrodecustos === item.idcentrodecustos
- ? centroAtualizado
- : centro
- )
- );
-
setSucesso(
novoStatus === 1
? 'Centro de custo habilitado com sucesso.'
: 'Centro de custo desabilitado com sucesso.'
);
+
+ await carregarCentrosCusto(page);
} catch (error: any) {
const message =
error?.response?.data?.message ||
@@ -307,7 +343,7 @@ export function CentrosCustoPage() {
- Centros encontrados
+ Centros de custo
{summary.quantidade}
@@ -318,8 +354,9 @@ export function CentrosCustoPage() {
- Limite total
+ Limite simulado
+
{formatarValor(summary.limiteTotal)}
@@ -340,7 +377,7 @@ export function CentrosCustoPage() {
- Simuláveis
+ Na simulação
{summary.simulaveis}
@@ -388,10 +425,14 @@ export function CentrosCustoPage() {
- setSimular(event.target.value === '' ? '' : Number(event.target.value))
+ setSimular(
+ event.target.value === ''
+ ? ''
+ : Number(event.target.value) as 0 | 1
+ )
}
fullWidth
>
@@ -529,16 +570,30 @@ export function CentrosCustoPage() {
-
- {formatarValor(item.limite)}
-
+ {Number(item.simular) === 1 ? (
+
+ {formatarValor(item.limite)}
+
+ ) : (
+
+ Fora da simulação
+
+ )}
@@ -655,16 +710,22 @@ export function CentrosCustoPage() {
-
- {formatarValor(item.limite)}
-
+ {Number(item.simular) === 1 ? (
+
+ {formatarValor(item.limite)}
+
+ ) : (
+
+ -
+
+ )}
diff --git a/financeiro-web/src/features/centrosCusto/services/centrosCustoService.ts b/financeiro-web/src/features/centrosCusto/services/centrosCustoService.ts
index 1643272..80ca196 100644
--- a/financeiro-web/src/features/centrosCusto/services/centrosCustoService.ts
+++ b/financeiro-web/src/features/centrosCusto/services/centrosCustoService.ts
@@ -5,10 +5,20 @@ import type {
CentroCusto,
CentrosCustoListResponse,
CriarCentroCustoRequest,
+ FlagNumerica,
} from '../types/centroCustoTypes';
export type OrderDirection = 'ASC' | 'DESC';
+export type CentroCustoOrderBy =
+ | 'descricao'
+ | 'limite'
+ | 'simular'
+ | 'investimento'
+ | 'habilitado'
+ | 'insert_date'
+ | 'update_date';
+
export type ListarCentrosCustoParams = {
limite?: number;
offset?: number;
@@ -17,29 +27,39 @@ export type ListarCentrosCustoParams = {
simular?: number | '';
investimento?: number | '';
habilitado?: number | '';
- orderBy?: string;
+ orderBy?: CentroCustoOrderBy;
orderDirection?: OrderDirection;
};
export async function listarCentrosCusto(
params?: ListarCentrosCustoParams
): Promise {
- const response = await api.get('/centros-custo', {
- params,
- });
+ const response = await api.get(
+ '/centros-custo',
+ { params }
+ );
return response.data;
}
-export async function buscarCentroCustoPorId(id: number): Promise {
- const response = await api.get>(`/centros-custo/${id}`);
+export async function buscarCentroCustoPorId(
+ id: number
+): Promise {
+ const response = await api.get>(
+ `/centros-custo/${id}`
+ );
+
return response.data.data;
}
export async function criarCentroCusto(
data: CriarCentroCustoRequest
): Promise {
- const response = await api.post>('/centros-custo', data);
+ const response = await api.post>(
+ '/centros-custo',
+ data
+ );
+
return response.data.data;
}
@@ -47,13 +67,17 @@ export async function atualizarCentroCusto(
id: number,
data: AtualizarCentroCustoRequest
): Promise {
- const response = await api.put>(`/centros-custo/${id}`, data);
+ const response = await api.put>(
+ `/centros-custo/${id}`,
+ data
+ );
+
return response.data.data;
}
export async function alterarHabilitadoCentroCusto(
id: number,
- habilitado: number
+ habilitado: FlagNumerica
): Promise {
const response = await api.patch>(
`/centros-custo/${id}/habilitado`,
diff --git a/financeiro-web/src/features/centrosCusto/types/centroCustoTypes.ts b/financeiro-web/src/features/centrosCusto/types/centroCustoTypes.ts
index e7df198..4797670 100644
--- a/financeiro-web/src/features/centrosCusto/types/centroCustoTypes.ts
+++ b/financeiro-web/src/features/centrosCusto/types/centroCustoTypes.ts
@@ -1,20 +1,36 @@
+export type FlagNumerica = 0 | 1;
+
export type CentroCusto = {
idcentrodecustos: number;
descricao: string;
- limite: number;
- simular: number;
- investimento: number;
- habilitado: number;
+
+ /**
+ * Dados globais do centro de custo.
+ */
+ investimento: FlagNumerica;
+ habilitado: FlagNumerica;
insert_date: string | null;
update_date: string | null;
+
+ /**
+ * Dados da simulação do usuário logado.
+ * Não são mais colunas diretas de centrodecustos.
+ */
+ simular: FlagNumerica;
+ limite: number | null;
};
export type CriarCentroCustoRequest = {
descricao: string;
- limite: number;
- simular: number;
- investimento: number;
- habilitado: number;
+ investimento: FlagNumerica;
+ habilitado: FlagNumerica;
+
+ /**
+ * Quando simular = 1, limite deve ser informado.
+ * Quando simular = 0, o vínculo será removido ou não será criado.
+ */
+ simular: FlagNumerica;
+ limite: number | null;
};
export type AtualizarCentroCustoRequest = CriarCentroCustoRequest;
@@ -29,10 +45,19 @@ export type CentrosCustoPagination = {
export type CentrosCustoSummary = {
quantidade: number;
+
+ /**
+ * Soma dos limites configurados pelo usuário logado.
+ */
limiteTotal: number;
+
habilitados: number;
desabilitados: number;
investimentos: number;
+
+ /**
+ * Quantidade de centros vinculados à simulação do usuário.
+ */
simulaveis: number;
};
diff --git a/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx b/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx
index 5c7f869..f3bbb5e 100644
--- a/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx
+++ b/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx
@@ -61,7 +61,7 @@ import type {
MovimentosSummary,
} from '../types/movimentoTypes';
import {
- listarBancos,
+ listarMeusBancos,
listarCentrosCusto,
listarClientes,
} from '../../referencias/services/referenciasService';
@@ -559,7 +559,7 @@ export function MovimentosPage() {
setLoadingRefs(true);
const [bancosData, centrosData, clientesData] = await Promise.all([
- listarBancos(),
+ listarMeusBancos(),
listarCentrosCusto(),
listarClientes(),
]);
diff --git a/financeiro-web/src/features/simulacaoMensal/pages/SimulacaoMensalPage.tsx b/financeiro-web/src/features/simulacaoMensal/pages/SimulacaoMensalPage.tsx
index a63b8ac..23cb0e9 100644
--- a/financeiro-web/src/features/simulacaoMensal/pages/SimulacaoMensalPage.tsx
+++ b/financeiro-web/src/features/simulacaoMensal/pages/SimulacaoMensalPage.tsx
@@ -127,7 +127,7 @@ function origemLabel(origem: string) {
movimento_fixo_previsto: 'Fixo previsto',
saldo_banco: 'Saldo banco',
cartao_credito_agrupado: 'Cartão',
- limite_centro_custo: 'Limite centro',
+ limite_centro_custo: 'Previsão do centro',
manual: 'Manual',
};
@@ -355,6 +355,31 @@ function SimulacaoLista({
/>
)}
+ {item.origem === 'limite_centro_custo' && item.detalhes && (
+ <>
+
+
+
+
+
+ >
+ )}
+
{item.cliente_nome && (
-
+
)}
diff --git a/financeiro-web/src/features/simulacaoMensal/services/simulacaoMensalService.ts b/financeiro-web/src/features/simulacaoMensal/services/simulacaoMensalService.ts
index 87cc888..f75caad 100644
--- a/financeiro-web/src/features/simulacaoMensal/services/simulacaoMensalService.ts
+++ b/financeiro-web/src/features/simulacaoMensal/services/simulacaoMensalService.ts
@@ -1,5 +1,6 @@
import { api } from '../../../services/api';
import type {
+ ApiResponse,
BuscarBaseSimulacaoMensalParams,
SimulacaoMensalBase,
} from '../types/simulacaoMensalTypes';
@@ -7,14 +8,18 @@ import type {
export async function buscarBaseSimulacaoMensal(
params: BuscarBaseSimulacaoMensalParams
): Promise {
- const response = await api.get('/simulacao-mensal/base', {
- params: {
- ano: params.ano,
- mes: params.mes,
- incluirQuitadas: params.incluirQuitadas ? 1 : 0,
- incluirFixosVencidosDoMesAtual: params.incluirFixosVencidosDoMesAtual ? 1 : 0,
- },
- });
+ const response = await api.get>(
+ '/simulacao-mensal/base',
+ {
+ params: {
+ ano: params.ano,
+ mes: params.mes,
+ incluirQuitadas: params.incluirQuitadas ? 1 : 0,
+ incluirFixosVencidosDoMesAtual:
+ params.incluirFixosVencidosDoMesAtual ? 1 : 0,
+ },
+ }
+ );
return response.data.data;
-}
\ No newline at end of file
+}
diff --git a/financeiro-web/src/features/simulacaoMensal/types/simulacaoMensalTypes.ts b/financeiro-web/src/features/simulacaoMensal/types/simulacaoMensalTypes.ts
index 5d88900..44bb61e 100644
--- a/financeiro-web/src/features/simulacaoMensal/types/simulacaoMensalTypes.ts
+++ b/financeiro-web/src/features/simulacaoMensal/types/simulacaoMensalTypes.ts
@@ -8,10 +8,34 @@ export type SimulacaoMensalOrigemItem =
| 'limite_centro_custo'
| 'manual';
+export type SimulacaoMensalDetalhesLimiteCentro = {
+ limite: number;
+ gastoAtual: number;
+ restante: number;
+};
+
+export type SimulacaoMensalDetalhesCartao = {
+ quantidade_movimentos: number;
+ ids: number[];
+};
+
+export type SimulacaoMensalDetalhesMovimento = {
+ parcela?: number | null;
+ parcelas?: number | null;
+ origem_movimento?: string | null;
+};
+
+export type SimulacaoMensalDetalhes =
+ | SimulacaoMensalDetalhesLimiteCentro
+ | SimulacaoMensalDetalhesCartao
+ | SimulacaoMensalDetalhesMovimento
+ | Record
+ | null;
+
export type SimulacaoMensalItem = {
id: string;
tipo: SimulacaoMensalTipoItem;
- origem: SimulacaoMensalOrigemItem | string;
+ origem: SimulacaoMensalOrigemItem;
descricao: string;
valor: number;
@@ -38,32 +62,37 @@ export type SimulacaoMensalItem = {
editavel: boolean;
editado: boolean;
- detalhes?: any;
+ detalhes?: SimulacaoMensalDetalhes;
};
export type SimulacaoMensalResumo = {
totalEntradas: number;
totalSaidas: number;
- totalGastos?: number;
- totalInvestimentos?: number;
+ totalGastos: number;
+ totalInvestimentos: number;
resultado: number;
- percentualEntradas?: number | null;
- percentualSaidas?: number | null;
- percentualGastos?: number | null;
- percentualInvestimentos?: number | null;
- percentualResultado?: number | null;
+ percentualEntradas: number | null;
+ percentualSaidas: number | null;
+ percentualGastos: number | null;
+ percentualInvestimentos: number | null;
+ percentualResultado: number | null;
percentualComprometido: number | null;
quantidadeEntradas: number;
quantidadeSaidas: number;
- quantidadeInvestimentos?: number;
+ quantidadeInvestimentos: number;
};
export type SimulacaoMensalAviso = {
tipo: string;
mensagem: string;
- [key: string]: any;
+ idcentrodecustos?: number;
+ centro_custo_descricao?: string;
+ limite?: number;
+ gastoAtual?: number;
+ excesso?: number;
+ [key: string]: unknown;
};
export type SimulacaoMensalBase = {
@@ -98,4 +127,10 @@ export type BuscarBaseSimulacaoMensalParams = {
mes: number;
incluirQuitadas?: boolean;
incluirFixosVencidosDoMesAtual?: boolean;
-};
\ No newline at end of file
+};
+
+export type ApiResponse = {
+ ok: boolean;
+ message?: string;
+ data: T;
+};