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 ? ( + + ) : ( + + )} + + + + ), + }} + /> + + - - Entrar - + - - Acesse o Zendion Financeiro para continuar. - -
- - {erro && ( - - {erro} - - )} - - - setUsuario(event.target.value)} - fullWidth - autoFocus - /> - - setSenha(event.target.value)} - fullWidth - /> - - - + + 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} - > - Sim - Não - - setInvestimento(event.target.value)} + onChange={(event) => + setInvestimento(Number(event.target.value) === 1 ? 1 : 0) + } fullWidth sx={fieldSx} > - Sim - Não + Sim + Não setHabilitado(event.target.value)} + onChange={(event) => + setHabilitado(Number(event.target.value) === 1 ? 1 : 0) + } fullWidth sx={fieldSx} > - Habilitado - Desabilitado + Habilitado + Desabilitado + + + + 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} + /> + +