diff --git a/financeiro-api/src/app.js b/financeiro-api/src/app.js
index 6bed304..9d9fbd6 100644
--- a/financeiro-api/src/app.js
+++ b/financeiro-api/src/app.js
@@ -5,6 +5,11 @@ const authRoutes = require('./modules/auth/routes/auth.routes');
const movimentosRoutes = require('./modules/movimentos/routes/movimentos.routes');
const referenciasRoutes = require('./modules/referencias/routes/referencias.routes');
const relatoriosRoutes = require('./modules/relatorios/routes/relatorios.routes');
+const clientesRoutes = require('./modules/clientes/routes/clientes.routes');
+const bancosRoutes = require('./modules/bancos/routes/bancos.routes');
+const centrosCustoRoutes = require('./modules/centrosCusto/routes/centrosCusto.routes');
+const movimentosFixosRoutes = require('./modules/movimentosFixos/routes/movimentosFixos.routes');
+const usuariosRoutes = require('./modules/usuarios/routes/usuarios.routes');
const app = express();
@@ -32,6 +37,11 @@ app.use('/api/auth', authRoutes);
app.use('/api/movimentos', movimentosRoutes);
app.use('/api/referencias', referenciasRoutes);
app.use('/api/relatorios', relatoriosRoutes);
+app.use('/api/clientes', clientesRoutes);
+app.use('/api/bancos', bancosRoutes);
+app.use('/api/centros-custo', centrosCustoRoutes);
+app.use('/api/movimentos-fixos', movimentosFixosRoutes);
+app.use('/api/usuarios', usuariosRoutes);
app.use((req, res) => {
return res.status(404).json({
diff --git a/financeiro-api/src/modules/bancos/controllers/bancos.controller.js b/financeiro-api/src/modules/bancos/controllers/bancos.controller.js
new file mode 100644
index 0000000..0eff64b
--- /dev/null
+++ b/financeiro-api/src/modules/bancos/controllers/bancos.controller.js
@@ -0,0 +1,264 @@
+const bancosService = require('../services/bancos.service');
+
+function validarDadosBanco(dados) {
+ if (!dados.descricao || !String(dados.descricao).trim()) {
+ return 'Descrição é obrigatória.';
+ }
+
+ if (dados.saldo !== undefined && dados.saldo !== null && dados.saldo !== '') {
+ const saldo = Number(dados.saldo);
+
+ if (!Number.isFinite(saldo)) {
+ return 'Saldo inválido.';
+ }
+ }
+
+ if (dados.debito !== undefined && dados.debito !== null && dados.debito !== '') {
+ const debito = Number(dados.debito);
+
+ if (![0, 1].includes(debito)) {
+ return 'Tipo débito inválido.';
+ }
+ }
+
+ if (dados.habilitado !== undefined && dados.habilitado !== null && dados.habilitado !== '') {
+ const habilitado = Number(dados.habilitado);
+
+ if (![0, 1].includes(habilitado)) {
+ return 'Status inválido.';
+ }
+ }
+
+ return null;
+}
+
+async function listar(req, res) {
+ try {
+ const resultado = await bancosService.listarBancos({
+ limite: req.query.limite,
+ offset: req.query.offset,
+ page: req.query.page,
+ busca: req.query.busca,
+ debito: req.query.debito,
+ habilitado: req.query.habilitado,
+ orderBy: req.query.orderBy,
+ orderDirection: req.query.orderDirection,
+ });
+
+ return res.json({
+ ok: true,
+ data: resultado.data,
+ pagination: resultado.pagination,
+ summary: resultado.summary,
+ });
+ } catch (error) {
+ console.error('Erro ao listar bancos:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao listar bancos.',
+ });
+ }
+}
+
+async function buscarPorId(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const banco = await bancosService.buscarBancoPorId(id);
+
+ if (!banco) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Banco/carteira não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ data: banco,
+ });
+ } catch (error) {
+ console.error('Erro ao buscar banco:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao buscar banco.',
+ });
+ }
+}
+
+async function criar(req, res) {
+ try {
+ const erroValidacao = validarDadosBanco(req.body);
+
+ if (erroValidacao) {
+ return res.status(400).json({
+ ok: false,
+ message: erroValidacao,
+ });
+ }
+
+ const novoBanco = await bancosService.criarBanco(req.body);
+
+ return res.status(201).json({
+ ok: true,
+ message: 'Banco/carteira cadastrado com sucesso.',
+ data: novoBanco,
+ });
+ } catch (error) {
+ console.error('Erro ao criar banco:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao criar banco/carteira.',
+ });
+ }
+}
+
+async function atualizar(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const erroValidacao = validarDadosBanco(req.body);
+
+ if (erroValidacao) {
+ return res.status(400).json({
+ ok: false,
+ message: erroValidacao,
+ });
+ }
+
+ const bancoAtualizado = await bancosService.atualizarBanco(id, req.body);
+
+ if (!bancoAtualizado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Banco/carteira não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: 'Banco/carteira atualizado com sucesso.',
+ data: bancoAtualizado,
+ });
+ } catch (error) {
+ console.error('Erro ao atualizar banco:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao atualizar banco/carteira.',
+ });
+ }
+}
+
+async function alterarHabilitado(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const habilitado = Number(req.body.habilitado);
+
+ if (![0, 1].includes(habilitado)) {
+ return res.status(400).json({
+ ok: false,
+ message: 'Status inválido.',
+ });
+ }
+
+ const bancoAtualizado = await bancosService.alterarHabilitadoBanco(id, habilitado);
+
+ if (!bancoAtualizado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Banco/carteira não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: habilitado === 1
+ ? 'Banco/carteira habilitado com sucesso.'
+ : 'Banco/carteira desabilitado com sucesso.',
+ data: bancoAtualizado,
+ });
+ } catch (error) {
+ console.error('Erro ao alterar status do banco:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao alterar status do banco/carteira.',
+ });
+ }
+}
+
+async function deletar(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const deletado = await bancosService.deletarBanco(id);
+
+ if (!deletado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Banco/carteira não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: 'Banco/carteira excluído com sucesso.',
+ });
+ } catch (error) {
+ console.error('Erro ao excluir banco:', error);
+
+ if (error.code === 'ER_ROW_IS_REFERENCED_2') {
+ return res.status(400).json({
+ ok: false,
+ message: 'Este banco/carteira possui movimentos vinculados e não pode ser excluído.',
+ });
+ }
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao excluir banco/carteira.',
+ });
+ }
+}
+
+module.exports = {
+ listar,
+ buscarPorId,
+ criar,
+ atualizar,
+ alterarHabilitado,
+ deletar,
+};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/bancos/routes/bancos.routes.js b/financeiro-api/src/modules/bancos/routes/bancos.routes.js
new file mode 100644
index 0000000..44f7080
--- /dev/null
+++ b/financeiro-api/src/modules/bancos/routes/bancos.routes.js
@@ -0,0 +1,16 @@
+const express = require('express');
+const authMiddleware = require('../../../middlewares/auth.middleware');
+const bancosController = require('../controllers/bancos.controller');
+
+const router = express.Router();
+
+router.use(authMiddleware);
+
+router.get('/', bancosController.listar);
+router.get('/:id', bancosController.buscarPorId);
+router.post('/', bancosController.criar);
+router.put('/:id', bancosController.atualizar);
+router.patch('/:id/habilitado', bancosController.alterarHabilitado);
+router.delete('/:id', bancosController.deletar);
+
+module.exports = router;
\ No newline at end of file
diff --git a/financeiro-api/src/modules/bancos/services/bancos.service.js b/financeiro-api/src/modules/bancos/services/bancos.service.js
new file mode 100644
index 0000000..b2ce8e9
--- /dev/null
+++ b/financeiro-api/src/modules/bancos/services/bancos.service.js
@@ -0,0 +1,306 @@
+const pool = require('../../../database/mysql');
+
+const CAMPOS_BANCO_SELECT = `
+ idbancos,
+ descricao,
+ saldo,
+ debito,
+ habilitado,
+ insert_date,
+ update_date
+`;
+
+function normalizarTextoOuNull(valor) {
+ if (valor === undefined || valor === null || valor === '') {
+ return null;
+ }
+
+ return String(valor).trim();
+}
+
+function normalizarNumero(valor, padrao = 0) {
+ if (valor === undefined || valor === null || valor === '') {
+ return padrao;
+ }
+
+ const numero = Number(valor);
+
+ if (!Number.isFinite(numero)) {
+ return padrao;
+ }
+
+ return numero;
+}
+
+function limitarNumero(valor, padrao, minimo, maximo) {
+ const numero = Number(valor);
+
+ if (!Number.isFinite(numero)) {
+ return padrao;
+ }
+
+ if (numero < minimo) {
+ return minimo;
+ }
+
+ if (numero > maximo) {
+ return maximo;
+ }
+
+ return numero;
+}
+
+function resolverOrdenacao(orderBy) {
+ const camposPermitidos = {
+ idbancos: 'idbancos',
+ descricao: 'descricao',
+ saldo: 'saldo',
+ debito: 'debito',
+ habilitado: 'habilitado',
+ insert_date: 'insert_date',
+ update_date: 'update_date',
+ };
+
+ return camposPermitidos[orderBy] || 'descricao';
+}
+
+function resolverDirecao(orderDirection) {
+ return String(orderDirection || '').toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
+}
+
+function montarWhereBancos(filtros = {}) {
+ const where = [];
+ const params = [];
+
+ if (filtros.debito !== undefined && filtros.debito !== null && filtros.debito !== '') {
+ where.push('debito = ?');
+ params.push(Number(filtros.debito));
+ }
+
+ if (filtros.habilitado !== undefined && filtros.habilitado !== null && filtros.habilitado !== '') {
+ where.push('habilitado = ?');
+ params.push(Number(filtros.habilitado));
+ }
+
+ if (filtros.busca) {
+ where.push(`
+ (
+ descricao LIKE ?
+ )
+ `);
+
+ const termo = `%${String(filtros.busca).trim()}%`;
+ params.push(termo);
+ }
+
+ const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
+
+ return {
+ whereSql,
+ params,
+ };
+}
+
+async function listarBancos(filtros = {}) {
+ const limite = limitarNumero(filtros.limite, 20, 1, 100);
+ const page = limitarNumero(filtros.page, 1, 1, 999999);
+ const offset = filtros.offset !== undefined
+ ? limitarNumero(filtros.offset, 0, 0, 999999999)
+ : (page - 1) * limite;
+
+ const orderBy = resolverOrdenacao(filtros.orderBy);
+ const orderDirection = resolverDirecao(filtros.orderDirection);
+
+ const { whereSql, params } = montarWhereBancos(filtros);
+
+ const [rows] = await pool.query(
+ `
+ SELECT
+ ${CAMPOS_BANCO_SELECT}
+ FROM bancos
+ ${whereSql}
+ ORDER BY ${orderBy} ${orderDirection}, idbancos ASC
+ LIMIT ? OFFSET ?
+ `,
+ [...params, limite, offset]
+ );
+
+ const [countRows] = await pool.query(
+ `
+ SELECT COUNT(*) AS total
+ FROM bancos
+ ${whereSql}
+ `,
+ params
+ );
+
+ const [summaryRows] = await pool.query(
+ `
+ SELECT
+ COUNT(*) AS quantidade,
+ COALESCE(SUM(saldo), 0) AS saldoTotal,
+ COALESCE(SUM(CASE WHEN debito = 0 THEN saldo ELSE 0 END), 0) AS saldoCarteiras,
+ COALESCE(SUM(CASE WHEN debito = 1 THEN saldo ELSE 0 END), 0) AS saldoDebito
+ FROM bancos
+ ${whereSql}
+ `,
+ params
+ );
+
+ const total = Number(countRows[0]?.total || 0);
+ const totalPages = Math.max(1, Math.ceil(total / limite));
+
+ return {
+ data: rows,
+ pagination: {
+ total,
+ limite,
+ offset,
+ page: Math.floor(offset / limite) + 1,
+ totalPages,
+ },
+ summary: {
+ quantidade: Number(summaryRows[0]?.quantidade || 0),
+ saldoTotal: Number(summaryRows[0]?.saldoTotal || 0),
+ saldoCarteiras: Number(summaryRows[0]?.saldoCarteiras || 0),
+ saldoDebito: Number(summaryRows[0]?.saldoDebito || 0),
+ },
+ };
+}
+
+async function buscarBancoPorId(id) {
+ const [rows] = await pool.query(
+ `
+ SELECT
+ ${CAMPOS_BANCO_SELECT}
+ FROM bancos
+ WHERE idbancos = ?
+ LIMIT 1
+ `,
+ [id]
+ );
+
+ return rows[0] || null;
+}
+
+async function criarBanco(dados) {
+ const {
+ descricao,
+ saldo,
+ debito,
+ habilitado,
+ } = dados;
+
+ const [result] = await pool.query(
+ `
+ INSERT INTO bancos (
+ descricao,
+ saldo,
+ debito,
+ habilitado,
+ insert_date,
+ update_date
+ ) VALUES (?, ?, ?, ?, NOW(), NOW())
+ `,
+ [
+ normalizarTextoOuNull(descricao),
+ normalizarNumero(saldo, 0),
+ Number(debito || 0),
+ habilitado === undefined || habilitado === null || habilitado === ''
+ ? 1
+ : Number(habilitado),
+ ]
+ );
+
+ return buscarBancoPorId(result.insertId);
+}
+
+async function atualizarBanco(id, dados) {
+ const bancoAtual = await buscarBancoPorId(id);
+
+ if (!bancoAtual) {
+ return null;
+ }
+
+ const {
+ descricao,
+ saldo,
+ debito,
+ habilitado,
+ } = dados;
+
+ await pool.query(
+ `
+ UPDATE bancos
+ SET
+ descricao = ?,
+ saldo = ?,
+ debito = ?,
+ habilitado = ?,
+ update_date = NOW()
+ WHERE idbancos = ?
+ `,
+ [
+ normalizarTextoOuNull(descricao),
+ normalizarNumero(saldo, 0),
+ Number(debito || 0),
+ habilitado === undefined || habilitado === null || habilitado === ''
+ ? 1
+ : Number(habilitado),
+ id,
+ ]
+ );
+
+ return buscarBancoPorId(id);
+}
+
+async function alterarHabilitadoBanco(id, habilitado) {
+ const bancoAtual = await buscarBancoPorId(id);
+
+ if (!bancoAtual) {
+ return null;
+ }
+
+ await pool.query(
+ `
+ UPDATE bancos
+ SET
+ habilitado = ?,
+ update_date = NOW()
+ WHERE idbancos = ?
+ `,
+ [
+ Number(habilitado),
+ id,
+ ]
+ );
+
+ return buscarBancoPorId(id);
+}
+
+async function deletarBanco(id) {
+ const bancoAtual = await buscarBancoPorId(id);
+
+ if (!bancoAtual) {
+ return false;
+ }
+
+ await pool.query(
+ `
+ DELETE FROM bancos
+ WHERE idbancos = ?
+ `,
+ [id]
+ );
+
+ return true;
+}
+
+module.exports = {
+ listarBancos,
+ buscarBancoPorId,
+ criarBanco,
+ atualizarBanco,
+ alterarHabilitadoBanco,
+ deletarBanco,
+};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/centrosCusto/controllers/centrosCusto.controller.js b/financeiro-api/src/modules/centrosCusto/controllers/centrosCusto.controller.js
new file mode 100644
index 0000000..33c98c4
--- /dev/null
+++ b/financeiro-api/src/modules/centrosCusto/controllers/centrosCusto.controller.js
@@ -0,0 +1,273 @@
+const centrosCustoService = require('../services/centrosCusto.service');
+
+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 limite = Number(dados.limite);
+
+ if (!Number.isFinite(limite)) {
+ return 'Limite inválido.';
+ }
+ }
+
+ if (dados.simular !== undefined && dados.simular !== null && dados.simular !== '') {
+ const simular = Number(dados.simular);
+
+ if (![0, 1].includes(simular)) {
+ return 'Campo simular inválido.';
+ }
+ }
+
+ if (dados.investimento !== undefined && dados.investimento !== null && dados.investimento !== '') {
+ const investimento = Number(dados.investimento);
+
+ 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.';
+ }
+ }
+
+ return null;
+}
+
+async function listar(req, res) {
+ try {
+ const resultado = await centrosCustoService.listarCentrosCusto({
+ limite: req.query.limite,
+ offset: req.query.offset,
+ page: req.query.page,
+ busca: req.query.busca,
+ simular: req.query.simular,
+ investimento: req.query.investimento,
+ habilitado: req.query.habilitado,
+ orderBy: req.query.orderBy,
+ orderDirection: req.query.orderDirection,
+ });
+
+ return res.json({
+ ok: true,
+ data: resultado.data,
+ pagination: resultado.pagination,
+ summary: resultado.summary,
+ });
+ } catch (error) {
+ console.error('Erro ao listar centros de custo:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao listar centros de custo.',
+ });
+ }
+}
+
+async function buscarPorId(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const centroCusto = await centrosCustoService.buscarCentroCustoPorId(id);
+
+ if (!centroCusto) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Centro de custo não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ data: centroCusto,
+ });
+ } catch (error) {
+ console.error('Erro ao buscar centro de custo:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao buscar centro de custo.',
+ });
+ }
+}
+
+async function criar(req, res) {
+ try {
+ const erroValidacao = validarDadosCentroCusto(req.body);
+
+ if (erroValidacao) {
+ return res.status(400).json({
+ ok: false,
+ message: erroValidacao,
+ });
+ }
+
+ const novoCentroCusto = await centrosCustoService.criarCentroCusto(req.body);
+
+ return res.status(201).json({
+ ok: true,
+ message: 'Centro de custo cadastrado com sucesso.',
+ data: novoCentroCusto,
+ });
+ } catch (error) {
+ console.error('Erro ao criar centro de custo:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao criar centro de custo.',
+ });
+ }
+}
+
+async function atualizar(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const erroValidacao = validarDadosCentroCusto(req.body);
+
+ if (erroValidacao) {
+ return res.status(400).json({
+ ok: false,
+ message: erroValidacao,
+ });
+ }
+
+ const centroAtualizado = await centrosCustoService.atualizarCentroCusto(id, req.body);
+
+ if (!centroAtualizado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Centro de custo não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: 'Centro de custo atualizado com sucesso.',
+ data: centroAtualizado,
+ });
+ } catch (error) {
+ console.error('Erro ao atualizar centro de custo:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao atualizar centro de custo.',
+ });
+ }
+}
+
+async function alterarHabilitado(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const habilitado = Number(req.body.habilitado);
+
+ if (![0, 1].includes(habilitado)) {
+ return res.status(400).json({
+ ok: false,
+ message: 'Status inválido.',
+ });
+ }
+
+ const centroAtualizado = await centrosCustoService.alterarHabilitadoCentroCusto(id, habilitado);
+
+ if (!centroAtualizado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Centro de custo não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: habilitado === 1
+ ? 'Centro de custo habilitado com sucesso.'
+ : 'Centro de custo desabilitado com sucesso.',
+ data: centroAtualizado,
+ });
+ } catch (error) {
+ console.error('Erro ao alterar status do centro de custo:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao alterar status do centro de custo.',
+ });
+ }
+}
+
+async function deletar(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const deletado = await centrosCustoService.deletarCentroCusto(id);
+
+ if (!deletado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Centro de custo não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: 'Centro de custo excluído com sucesso.',
+ });
+ } catch (error) {
+ console.error('Erro ao excluir centro de custo:', error);
+
+ if (error.code === 'ER_ROW_IS_REFERENCED_2') {
+ return res.status(400).json({
+ ok: false,
+ message: 'Este centro de custo possui movimentos vinculados e não pode ser excluído.',
+ });
+ }
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao excluir centro de custo.',
+ });
+ }
+}
+
+module.exports = {
+ listar,
+ buscarPorId,
+ criar,
+ 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
new file mode 100644
index 0000000..a025212
--- /dev/null
+++ b/financeiro-api/src/modules/centrosCusto/routes/centrosCusto.routes.js
@@ -0,0 +1,16 @@
+const express = require('express');
+const authMiddleware = require('../../../middlewares/auth.middleware');
+const centrosCustoController = require('../controllers/centrosCusto.controller');
+
+const router = express.Router();
+
+router.use(authMiddleware);
+
+router.get('/', centrosCustoController.listar);
+router.get('/:id', centrosCustoController.buscarPorId);
+router.post('/', centrosCustoController.criar);
+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
diff --git a/financeiro-api/src/modules/centrosCusto/services/centrosCusto.service.js b/financeiro-api/src/modules/centrosCusto/services/centrosCusto.service.js
new file mode 100644
index 0000000..6b840bc
--- /dev/null
+++ b/financeiro-api/src/modules/centrosCusto/services/centrosCusto.service.js
@@ -0,0 +1,329 @@
+const pool = require('../../../database/mysql');
+
+const CAMPOS_CENTRO_CUSTO_SELECT = `
+ idcentrodecustos,
+ descricao,
+ limite,
+ simular,
+ investimento,
+ habilitado,
+ insert_date,
+ update_date
+`;
+
+function normalizarTextoOuNull(valor) {
+ if (valor === undefined || valor === null || valor === '') {
+ return null;
+ }
+
+ return String(valor).trim();
+}
+
+function normalizarNumero(valor, padrao = 0) {
+ if (valor === undefined || valor === null || valor === '') {
+ return padrao;
+ }
+
+ const numero = Number(valor);
+
+ if (!Number.isFinite(numero)) {
+ return padrao;
+ }
+
+ return numero;
+}
+
+function normalizarFlag(valor, padrao = 0) {
+ if (valor === undefined || valor === null || valor === '') {
+ return padrao;
+ }
+
+ const numero = Number(valor);
+
+ return numero === 1 ? 1 : 0;
+}
+
+function limitarNumero(valor, padrao, minimo, maximo) {
+ const numero = Number(valor);
+
+ if (!Number.isFinite(numero)) {
+ return padrao;
+ }
+
+ if (numero < minimo) {
+ return minimo;
+ }
+
+ if (numero > maximo) {
+ return maximo;
+ }
+
+ return numero;
+}
+
+function resolverOrdenacao(orderBy) {
+ const camposPermitidos = {
+ idcentrodecustos: 'idcentrodecustos',
+ descricao: 'descricao',
+ limite: 'limite',
+ simular: 'simular',
+ investimento: 'investimento',
+ habilitado: 'habilitado',
+ insert_date: 'insert_date',
+ update_date: 'update_date',
+ };
+
+ return camposPermitidos[orderBy] || 'descricao';
+}
+
+function resolverDirecao(orderDirection) {
+ return String(orderDirection || '').toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
+}
+
+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.investimento !== undefined && filtros.investimento !== null && filtros.investimento !== '') {
+ where.push('investimento = ?');
+ params.push(Number(filtros.investimento));
+ }
+
+ if (filtros.habilitado !== undefined && filtros.habilitado !== null && filtros.habilitado !== '') {
+ where.push('habilitado = ?');
+ params.push(Number(filtros.habilitado));
+ }
+
+ if (filtros.busca) {
+ where.push(`
+ (
+ descricao LIKE ?
+ )
+ `);
+
+ const termo = `%${String(filtros.busca).trim()}%`;
+ params.push(termo);
+ }
+
+ const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
+
+ return {
+ whereSql,
+ params,
+ };
+}
+
+async function listarCentrosCusto(filtros = {}) {
+ const limite = limitarNumero(filtros.limite, 20, 1, 100);
+ const page = limitarNumero(filtros.page, 1, 1, 999999);
+ const offset = filtros.offset !== undefined
+ ? limitarNumero(filtros.offset, 0, 0, 999999999)
+ : (page - 1) * limite;
+
+ const orderBy = resolverOrdenacao(filtros.orderBy);
+ const orderDirection = resolverDirecao(filtros.orderDirection);
+
+ const { whereSql, params } = montarWhereCentrosCusto(filtros);
+
+ const [rows] = await pool.query(
+ `
+ SELECT
+ ${CAMPOS_CENTRO_CUSTO_SELECT}
+ FROM centrodecustos
+ ${whereSql}
+ ORDER BY ${orderBy} ${orderDirection}, idcentrodecustos ASC
+ LIMIT ? OFFSET ?
+ `,
+ [...params, limite, offset]
+ );
+
+ const [countRows] = await pool.query(
+ `
+ SELECT COUNT(*) AS total
+ FROM centrodecustos
+ ${whereSql}
+ `,
+ params
+ );
+
+ 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
+ ${whereSql}
+ `,
+ params
+ );
+
+ const total = Number(countRows[0]?.total || 0);
+ const totalPages = Math.max(1, Math.ceil(total / limite));
+
+ return {
+ data: rows,
+ pagination: {
+ total,
+ limite,
+ offset,
+ page: Math.floor(offset / limite) + 1,
+ totalPages,
+ },
+ summary: {
+ quantidade: Number(summaryRows[0]?.quantidade || 0),
+ limiteTotal: Number(summaryRows[0]?.limiteTotal || 0),
+ habilitados: Number(summaryRows[0]?.habilitados || 0),
+ desabilitados: Number(summaryRows[0]?.desabilitados || 0),
+ investimentos: Number(summaryRows[0]?.investimentos || 0),
+ simulaveis: Number(summaryRows[0]?.simulaveis || 0),
+ },
+ };
+}
+
+async function buscarCentroCustoPorId(id) {
+ const [rows] = await pool.query(
+ `
+ SELECT
+ ${CAMPOS_CENTRO_CUSTO_SELECT}
+ FROM centrodecustos
+ WHERE idcentrodecustos = ?
+ LIMIT 1
+ `,
+ [id]
+ );
+
+ return rows[0] || null;
+}
+
+async function criarCentroCusto(dados) {
+ const {
+ descricao,
+ limite,
+ simular,
+ investimento,
+ habilitado,
+ } = dados;
+
+ 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
+ habilitado = ?,
+ update_date = NOW()
+ WHERE idcentrodecustos = ?
+ `,
+ [
+ normalizarFlag(habilitado, 1),
+ id,
+ ]
+ );
+
+ return buscarCentroCustoPorId(id);
+}
+
+async function deletarCentroCusto(id) {
+ const centroAtual = await buscarCentroCustoPorId(id);
+
+ if (!centroAtual) {
+ return false;
+ }
+
+ await pool.query(
+ `
+ DELETE FROM centrodecustos
+ WHERE idcentrodecustos = ?
+ `,
+ [id]
+ );
+
+ return true;
+}
+
+module.exports = {
+ listarCentrosCusto,
+ buscarCentroCustoPorId,
+ criarCentroCusto,
+ atualizarCentroCusto,
+ alterarHabilitadoCentroCusto,
+ deletarCentroCusto,
+};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/clientes/controllers/clientes.controller.js b/financeiro-api/src/modules/clientes/controllers/clientes.controller.js
new file mode 100644
index 0000000..08ed227
--- /dev/null
+++ b/financeiro-api/src/modules/clientes/controllers/clientes.controller.js
@@ -0,0 +1,272 @@
+const clientesService = require('../services/clientes.service');
+
+function validarDadosCliente(dados) {
+ if (!dados.cpf_cnpj || !String(dados.cpf_cnpj).trim()) {
+ return 'CPF/CNPJ é obrigatório.';
+ }
+
+ if (!dados.nome || !String(dados.nome).trim()) {
+ return 'Nome é obrigatório.';
+ }
+
+ if (dados.email && !String(dados.email).includes('@')) {
+ return 'E-mail inválido.';
+ }
+
+ if (dados.pessoafisica && !['F', 'J', 'S', 'N'].includes(String(dados.pessoafisica).toUpperCase())) {
+ return 'Tipo de pessoa inválido.';
+ }
+
+ if (dados.sexo && !['M', 'F', 'O'].includes(String(dados.sexo).toUpperCase())) {
+ return 'Sexo inválido.';
+ }
+
+ if (dados.habilitado !== undefined && dados.habilitado !== null && dados.habilitado !== '') {
+ const habilitado = Number(dados.habilitado);
+
+ if (![0, 1].includes(habilitado)) {
+ return 'Status inválido.';
+ }
+ }
+
+ return null;
+}
+
+async function listar(req, res) {
+ try {
+ const resultado = await clientesService.listarClientes({
+ limite: req.query.limite,
+ offset: req.query.offset,
+ page: req.query.page,
+ busca: req.query.busca,
+ pessoafisica: req.query.pessoafisica,
+ habilitado: req.query.habilitado,
+ cidade: req.query.cidade,
+ estado: req.query.estado,
+ orderBy: req.query.orderBy,
+ orderDirection: req.query.orderDirection,
+ });
+
+ return res.json({
+ ok: true,
+ data: resultado.data,
+ pagination: resultado.pagination,
+ });
+ } catch (error) {
+ console.error('Erro ao listar clientes:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao listar clientes.',
+ });
+ }
+}
+
+async function buscarPorId(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const cliente = await clientesService.buscarClientePorId(id);
+
+ if (!cliente) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Cliente não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ data: cliente,
+ });
+ } catch (error) {
+ console.error('Erro ao buscar cliente:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao buscar cliente.',
+ });
+ }
+}
+
+async function criar(req, res) {
+ try {
+ const erroValidacao = validarDadosCliente(req.body);
+
+ if (erroValidacao) {
+ return res.status(400).json({
+ ok: false,
+ message: erroValidacao,
+ });
+ }
+
+ const novoCliente = await clientesService.criarCliente(req.body);
+
+ return res.status(201).json({
+ ok: true,
+ message: 'Cliente cadastrado com sucesso.',
+ data: novoCliente,
+ });
+ } catch (error) {
+ console.error('Erro ao criar cliente:', error);
+
+ if (error.code === 'ER_DUP_ENTRY') {
+ return res.status(400).json({
+ ok: false,
+ message: 'Já existe um cliente cadastrado com esses dados.',
+ });
+ }
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao criar cliente.',
+ });
+ }
+}
+
+async function atualizar(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const erroValidacao = validarDadosCliente(req.body);
+
+ if (erroValidacao) {
+ return res.status(400).json({
+ ok: false,
+ message: erroValidacao,
+ });
+ }
+
+ const clienteAtualizado = await clientesService.atualizarCliente(id, req.body);
+
+ if (!clienteAtualizado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Cliente não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: 'Cliente atualizado com sucesso.',
+ data: clienteAtualizado,
+ });
+ } catch (error) {
+ console.error('Erro ao atualizar cliente:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao atualizar cliente.',
+ });
+ }
+}
+
+async function alterarHabilitado(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const habilitado = Number(req.body.habilitado);
+
+ if (![0, 1].includes(habilitado)) {
+ return res.status(400).json({
+ ok: false,
+ message: 'Status inválido.',
+ });
+ }
+
+ const clienteAtualizado = await clientesService.alterarHabilitadoCliente(id, habilitado);
+
+ if (!clienteAtualizado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Cliente não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: habilitado === 1
+ ? 'Cliente habilitado com sucesso.'
+ : 'Cliente desabilitado com sucesso.',
+ data: clienteAtualizado,
+ });
+ } catch (error) {
+ console.error('Erro ao alterar status do cliente:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao alterar status do cliente.',
+ });
+ }
+}
+
+async function deletar(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const deletado = await clientesService.deletarCliente(id);
+
+ if (!deletado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Cliente não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: 'Cliente excluído com sucesso.',
+ });
+ } catch (error) {
+ console.error('Erro ao excluir cliente:', error);
+
+ if (error.code === 'ER_ROW_IS_REFERENCED_2') {
+ return res.status(400).json({
+ ok: false,
+ message: 'Este cliente possui movimentos vinculados e não pode ser excluído.',
+ });
+ }
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao excluir cliente.',
+ });
+ }
+}
+
+module.exports = {
+ listar,
+ buscarPorId,
+ criar,
+ atualizar,
+ alterarHabilitado,
+ deletar,
+};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/clientes/routes/clientes.routes.js b/financeiro-api/src/modules/clientes/routes/clientes.routes.js
new file mode 100644
index 0000000..0215a89
--- /dev/null
+++ b/financeiro-api/src/modules/clientes/routes/clientes.routes.js
@@ -0,0 +1,16 @@
+const express = require('express');
+const authMiddleware = require('../../../middlewares/auth.middleware');
+const clientesController = require('../controllers/clientes.controller');
+
+const router = express.Router();
+
+router.use(authMiddleware);
+
+router.get('/', clientesController.listar);
+router.get('/:id', clientesController.buscarPorId);
+router.post('/', clientesController.criar);
+router.put('/:id', clientesController.atualizar);
+router.patch('/:id/habilitado', clientesController.alterarHabilitado);
+router.delete('/:id', clientesController.deletar);
+
+module.exports = router;
\ No newline at end of file
diff --git a/financeiro-api/src/modules/clientes/services/clientes.service.js b/financeiro-api/src/modules/clientes/services/clientes.service.js
new file mode 100644
index 0000000..b167cba
--- /dev/null
+++ b/financeiro-api/src/modules/clientes/services/clientes.service.js
@@ -0,0 +1,379 @@
+const pool = require('../../../database/mysql');
+
+const CAMPOS_CLIENTE_SELECT = `
+ idclientes,
+ cpf_cnpj,
+ nome,
+ rg,
+ celular,
+ email,
+ cep,
+ logradouro,
+ numero,
+ bairro,
+ cidade,
+ estado,
+ sexo,
+ pessoafisica,
+ habilitado,
+ insert_date,
+ update_date
+`;
+
+function normalizarTextoOuNull(valor) {
+ if (valor === undefined || valor === null || valor === '') {
+ return null;
+ }
+
+ return String(valor).trim();
+}
+
+function normalizarFlagOuNull(valor) {
+ if (valor === undefined || valor === null || valor === '') {
+ return null;
+ }
+
+ return String(valor).trim().toUpperCase().substring(0, 1);
+}
+
+function limitarNumero(valor, padrao, minimo, maximo) {
+ const numero = Number(valor);
+
+ if (!Number.isFinite(numero)) {
+ return padrao;
+ }
+
+ if (numero < minimo) {
+ return minimo;
+ }
+
+ if (numero > maximo) {
+ return maximo;
+ }
+
+ return numero;
+}
+
+function resolverOrdenacao(orderBy) {
+ const camposPermitidos = {
+ idclientes: 'idclientes',
+ nome: 'nome',
+ cpf_cnpj: 'cpf_cnpj',
+ email: 'email',
+ cidade: 'cidade',
+ estado: 'estado',
+ pessoafisica: 'pessoafisica',
+ habilitado: 'habilitado',
+ insert_date: 'insert_date',
+ update_date: 'update_date',
+ };
+
+ return camposPermitidos[orderBy] || 'nome';
+}
+
+function resolverDirecao(orderDirection) {
+ return String(orderDirection || '').toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
+}
+
+function montarWhereClientes(filtros = {}) {
+ const where = [];
+ const params = [];
+
+ if (filtros.pessoafisica) {
+ where.push('pessoafisica = ?');
+ params.push(String(filtros.pessoafisica).toUpperCase().substring(0, 1));
+ }
+
+ if (filtros.habilitado !== undefined && filtros.habilitado !== null && filtros.habilitado !== '') {
+ where.push('habilitado = ?');
+ params.push(Number(filtros.habilitado));
+ }
+
+ if (filtros.cidade) {
+ where.push('cidade = ?');
+ params.push(filtros.cidade);
+ }
+
+ if (filtros.estado) {
+ where.push('estado = ?');
+ params.push(filtros.estado);
+ }
+
+ if (filtros.busca) {
+ where.push(`
+ (
+ nome LIKE ?
+ OR cpf_cnpj LIKE ?
+ OR rg LIKE ?
+ OR celular LIKE ?
+ OR email LIKE ?
+ OR cidade LIKE ?
+ OR estado LIKE ?
+ )
+ `);
+
+ const termo = `%${String(filtros.busca).trim()}%`;
+
+ params.push(
+ termo,
+ termo,
+ termo,
+ termo,
+ termo,
+ termo,
+ termo
+ );
+ }
+
+ const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
+
+ return {
+ whereSql,
+ params,
+ };
+}
+
+async function listarClientes(filtros = {}) {
+ const limite = limitarNumero(filtros.limite, 20, 1, 100);
+ const page = limitarNumero(filtros.page, 1, 1, 999999);
+ const offset = filtros.offset !== undefined
+ ? limitarNumero(filtros.offset, 0, 0, 999999999)
+ : (page - 1) * limite;
+
+ const orderBy = resolverOrdenacao(filtros.orderBy);
+ const orderDirection = resolverDirecao(filtros.orderDirection);
+
+ const { whereSql, params } = montarWhereClientes(filtros);
+
+ const [rows] = await pool.query(
+ `
+ SELECT
+ ${CAMPOS_CLIENTE_SELECT}
+ FROM clientes
+ ${whereSql}
+ ORDER BY ${orderBy} ${orderDirection}, idclientes ASC
+ LIMIT ? OFFSET ?
+ `,
+ [...params, limite, offset]
+ );
+
+ const [countRows] = await pool.query(
+ `
+ SELECT COUNT(*) AS total
+ FROM clientes
+ ${whereSql}
+ `,
+ params
+ );
+
+ const total = Number(countRows[0]?.total || 0);
+ const totalPages = Math.max(1, Math.ceil(total / limite));
+
+ return {
+ data: rows,
+ pagination: {
+ total,
+ limite,
+ offset,
+ page: Math.floor(offset / limite) + 1,
+ totalPages,
+ },
+ };
+}
+
+async function buscarClientePorId(id) {
+ const [rows] = await pool.query(
+ `
+ SELECT
+ ${CAMPOS_CLIENTE_SELECT}
+ FROM clientes
+ WHERE idclientes = ?
+ LIMIT 1
+ `,
+ [id]
+ );
+
+ return rows[0] || null;
+}
+
+async function criarCliente(dados) {
+ const {
+ cpf_cnpj,
+ nome,
+ rg,
+ celular,
+ email,
+ cep,
+ logradouro,
+ numero,
+ bairro,
+ cidade,
+ estado,
+ sexo,
+ pessoafisica,
+ habilitado,
+ } = dados;
+
+ const [result] = await pool.query(
+ `
+ INSERT INTO clientes (
+ cpf_cnpj,
+ nome,
+ rg,
+ celular,
+ email,
+ cep,
+ logradouro,
+ numero,
+ bairro,
+ cidade,
+ estado,
+ sexo,
+ pessoafisica,
+ habilitado,
+ insert_date,
+ update_date
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
+ `,
+ [
+ normalizarTextoOuNull(cpf_cnpj),
+ normalizarTextoOuNull(nome),
+ normalizarTextoOuNull(rg),
+ normalizarTextoOuNull(celular),
+ normalizarTextoOuNull(email),
+ normalizarTextoOuNull(cep),
+ normalizarTextoOuNull(logradouro),
+ normalizarTextoOuNull(numero),
+ normalizarTextoOuNull(bairro),
+ normalizarTextoOuNull(cidade),
+ normalizarTextoOuNull(estado),
+ normalizarFlagOuNull(sexo),
+ normalizarFlagOuNull(pessoafisica),
+ habilitado === undefined || habilitado === null || habilitado === ''
+ ? 1
+ : Number(habilitado),
+ ]
+ );
+
+ return buscarClientePorId(result.insertId);
+}
+
+async function atualizarCliente(id, dados) {
+ const clienteAtual = await buscarClientePorId(id);
+
+ if (!clienteAtual) {
+ return null;
+ }
+
+ const {
+ cpf_cnpj,
+ nome,
+ rg,
+ celular,
+ email,
+ cep,
+ logradouro,
+ numero,
+ bairro,
+ cidade,
+ estado,
+ sexo,
+ pessoafisica,
+ habilitado,
+ } = dados;
+
+ await pool.query(
+ `
+ UPDATE clientes
+ SET
+ cpf_cnpj = ?,
+ nome = ?,
+ rg = ?,
+ celular = ?,
+ email = ?,
+ cep = ?,
+ logradouro = ?,
+ numero = ?,
+ bairro = ?,
+ cidade = ?,
+ estado = ?,
+ sexo = ?,
+ pessoafisica = ?,
+ habilitado = ?,
+ update_date = NOW()
+ WHERE idclientes = ?
+ `,
+ [
+ normalizarTextoOuNull(cpf_cnpj),
+ normalizarTextoOuNull(nome),
+ normalizarTextoOuNull(rg),
+ normalizarTextoOuNull(celular),
+ normalizarTextoOuNull(email),
+ normalizarTextoOuNull(cep),
+ normalizarTextoOuNull(logradouro),
+ normalizarTextoOuNull(numero),
+ normalizarTextoOuNull(bairro),
+ normalizarTextoOuNull(cidade),
+ normalizarTextoOuNull(estado),
+ normalizarFlagOuNull(sexo),
+ normalizarFlagOuNull(pessoafisica),
+ habilitado === undefined || habilitado === null || habilitado === ''
+ ? 1
+ : Number(habilitado),
+ id,
+ ]
+ );
+
+ return buscarClientePorId(id);
+}
+
+async function alterarHabilitadoCliente(id, habilitado) {
+ const clienteAtual = await buscarClientePorId(id);
+
+ if (!clienteAtual) {
+ return null;
+ }
+
+ await pool.query(
+ `
+ UPDATE clientes
+ SET
+ habilitado = ?,
+ update_date = NOW()
+ WHERE idclientes = ?
+ `,
+ [
+ Number(habilitado),
+ id,
+ ]
+ );
+
+ return buscarClientePorId(id);
+}
+
+async function deletarCliente(id) {
+ const clienteAtual = await buscarClientePorId(id);
+
+ if (!clienteAtual) {
+ return false;
+ }
+
+ await pool.query(
+ `
+ DELETE FROM clientes
+ WHERE idclientes = ?
+ `,
+ [id]
+ );
+
+ return true;
+}
+
+module.exports = {
+ listarClientes,
+ buscarClientePorId,
+ criarCliente,
+ atualizarCliente,
+ alterarHabilitadoCliente,
+ deletarCliente,
+};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js b/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js
index c6b8b04..e38b17d 100644
--- a/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js
+++ b/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js
@@ -1,19 +1,48 @@
const movimentosService = require('../services/movimentos.service');
+const MOVIMENTOS_VALIDOS = ['Entrada', 'Saida', 'Sangria', 'Estorno'];
+const STATUS_VALIDOS = ['Pago', 'A pagar', 'Recebido', 'A receber'];
+
+function validarStatusPorMovimento(movimento, status) {
+ if (movimento === 'Entrada' || movimento === 'Estorno') {
+ return ['A receber', 'Recebido'].includes(status);
+ }
+
+ if (movimento === 'Saida') {
+ return ['A pagar', 'Pago'].includes(status);
+ }
+
+ if (movimento === 'Sangria') {
+ return status === 'Pago';
+ }
+
+ return false;
+}
+
function validarDadosMovimento(dados) {
if (!dados.movimento) {
return 'Movimento é obrigatório.';
}
- if (!['Entrada', 'Saida', 'Sangria', 'Estorno'].includes(dados.movimento)) {
+ if (!MOVIMENTOS_VALIDOS.includes(dados.movimento)) {
return 'Movimento inválido.';
}
+ if (!dados.descricao || !String(dados.descricao).trim()) {
+ return 'Descrição é obrigatória.';
+ }
+
if (dados.valor === undefined || dados.valor === null || dados.valor === '') {
return 'Valor é obrigatório.';
}
- if (Number(dados.valor) <= 0) {
+ const valor = Number(dados.valor);
+
+ if (!Number.isFinite(valor)) {
+ return 'Valor inválido.';
+ }
+
+ if (valor <= 0) {
return 'Valor deve ser maior que zero.';
}
@@ -21,21 +50,74 @@ function validarDadosMovimento(dados) {
return 'Situação é obrigatória.';
}
- if (!['Pago', 'A pagar', 'Recebido', 'A receber'].includes(dados.status)) {
+ if (!STATUS_VALIDOS.includes(dados.status)) {
return 'Situação inválida.';
}
- if (!dados.descricao || !String(dados.descricao).trim()) {
- return 'Descrição é obrigatória.';
+ if (!validarStatusPorMovimento(dados.movimento, dados.status)) {
+ return 'Situação incompatível com o tipo de movimento.';
}
if (!dados.dataentrada) {
return 'Data de entrada é obrigatória.';
}
+ if (!dados.datavencimento) {
+ return 'Data de vencimento é obrigatória.';
+ }
+
+ const parcela = Number(dados.parcela || 1);
+ const parcelas = Number(dados.parcelas || 1);
+
+ if (!Number.isInteger(parcela) || parcela < 1) {
+ return 'Parcela inválida.';
+ }
+
+ if (!Number.isInteger(parcelas) || parcelas < 1) {
+ return 'Quantidade de parcelas inválida.';
+ }
+
+ if (parcela > parcelas) {
+ return 'Parcela atual não pode ser maior que o total de parcelas.';
+ }
+
+ if (dados.movimento === 'Sangria') {
+ if (!dados.idbancos) {
+ return 'Banco de origem é obrigatório para sangria.';
+ }
+
+ if (!dados.idbancos_p) {
+ return 'Banco de destino é obrigatório para sangria.';
+ }
+
+ if (Number(dados.idbancos) === Number(dados.idbancos_p)) {
+ return 'Banco de origem e destino não podem ser iguais.';
+ }
+ }
+
+ if (dados.modoParcelamento && !['valor_parcela', 'valor_total'].includes(dados.modoParcelamento)) {
+ return 'Modo de parcelamento inválido.';
+ }
+
return null;
}
+function tratarErro(error, res, mensagemPadrao) {
+ if (error?.code === 'DUPLICIDADE_MOVIMENTO') {
+ return res.status(409).json({
+ ok: false,
+ requiresConfirmation: true,
+ message: error.message,
+ data: error.data,
+ });
+ }
+
+ return res.status(500).json({
+ ok: false,
+ message: mensagemPadrao,
+ });
+}
+
async function listar(req, res) {
try {
const resultado = await movimentosService.listarMovimentos({
@@ -48,11 +130,17 @@ async function listar(req, res) {
idcentrodecustos: req.query.idcentrodecustos,
idclientes: req.query.idclientes,
idbancos_p: req.query.idbancos_p,
+ idmovimentosfixos: req.query.idmovimentosfixos,
+ competencia: req.query.competencia,
+ grupo_parcelamento: req.query.grupo_parcelamento,
+ origem: req.query.origem,
+ saldo_processado: req.query.saldo_processado,
referenciaTipo: req.query.referenciaTipo,
dataCampo: req.query.dataCampo,
dataInicio: req.query.dataInicio,
dataFim: req.query.dataFim,
busca: req.query.busca,
+ incluirExcluidos: req.query.incluirExcluidos,
orderBy: req.query.orderBy,
orderDirection: req.query.orderDirection,
});
@@ -107,6 +195,51 @@ async function buscarPorId(req, res) {
}
}
+async function verificarDuplicidade(req, res) {
+ try {
+ const duplicado = await movimentosService.verificarDuplicidade(req.body);
+
+ return res.json({
+ ok: true,
+ data: duplicado,
+ });
+ } catch (error) {
+ console.error('Erro ao verificar duplicidade:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao verificar duplicidade.',
+ });
+ }
+}
+
+async function preverImpactoSaldo(req, res) {
+ try {
+ const erroValidacao = validarDadosMovimento(req.body);
+
+ if (erroValidacao) {
+ return res.status(400).json({
+ ok: false,
+ message: erroValidacao,
+ });
+ }
+
+ const impacto = await movimentosService.preverImpactoSaldo(req.body, req.body.idcontasapagar);
+
+ return res.json({
+ ok: true,
+ data: impacto,
+ });
+ } catch (error) {
+ console.error('Erro ao prever impacto de saldo:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao prever impacto de saldo.',
+ });
+ }
+}
+
async function criar(req, res) {
try {
const erroValidacao = validarDadosMovimento(req.body);
@@ -118,20 +251,18 @@ async function criar(req, res) {
});
}
- const novoMovimento = await movimentosService.criarMovimento(req.user.id, req.body);
+ const resultado = await movimentosService.criarMovimento(req.user.id, req.body);
return res.status(201).json({
ok: true,
message: 'Movimento cadastrado com sucesso.',
- data: novoMovimento,
+ data: resultado.data,
+ parcelasGeradas: resultado.parcelasGeradas || [],
+ impactoSaldo: resultado.impactoSaldo || [],
});
} catch (error) {
console.error('Erro ao criar movimento:', error);
-
- return res.status(500).json({
- ok: false,
- message: 'Erro ao criar movimento.',
- });
+ return tratarErro(error, res, 'Erro ao criar movimento.');
}
}
@@ -155,9 +286,9 @@ async function atualizar(req, res) {
});
}
- const movimentoAtualizado = await movimentosService.atualizarMovimento(id, req.body);
+ const resultado = await movimentosService.atualizarMovimento(id, req.user.id, req.body);
- if (!movimentoAtualizado) {
+ if (!resultado?.data) {
return res.status(404).json({
ok: false,
message: 'Movimento não encontrado.',
@@ -167,14 +298,46 @@ async function atualizar(req, res) {
return res.json({
ok: true,
message: 'Movimento atualizado com sucesso.',
- data: movimentoAtualizado,
+ data: resultado.data,
+ impactoSaldo: resultado.impactoSaldo || [],
});
} catch (error) {
console.error('Erro ao atualizar movimento:', error);
+ return tratarErro(error, res, 'Erro ao atualizar movimento.');
+ }
+}
+
+async function deletar(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const resultado = await movimentosService.deletarMovimento(id, req.user.id);
+
+ if (!resultado?.ok) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Movimento não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: 'Movimento excluído com sucesso.',
+ impactoSaldo: resultado.impactoSaldo || [],
+ });
+ } catch (error) {
+ console.error('Erro ao excluir movimento:', error);
return res.status(500).json({
ok: false,
- message: 'Erro ao atualizar movimento.',
+ message: 'Erro ao excluir movimento.',
});
}
}
@@ -182,6 +345,9 @@ async function atualizar(req, res) {
module.exports = {
listar,
buscarPorId,
+ verificarDuplicidade,
+ preverImpactoSaldo,
criar,
atualizar,
+ deletar,
};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/movimentos/routes/movimentos.routes.js b/financeiro-api/src/modules/movimentos/routes/movimentos.routes.js
index a6b72b3..b1b285d 100644
--- a/financeiro-api/src/modules/movimentos/routes/movimentos.routes.js
+++ b/financeiro-api/src/modules/movimentos/routes/movimentos.routes.js
@@ -8,7 +8,12 @@ router.use(authMiddleware);
router.get('/', movimentosController.listar);
router.get('/:id', movimentosController.buscarPorId);
+
+router.post('/verificar-duplicidade', movimentosController.verificarDuplicidade);
+router.post('/prever-impacto-saldo', movimentosController.preverImpactoSaldo);
+
router.post('/', movimentosController.criar);
router.put('/:id', movimentosController.atualizar);
+router.delete('/:id', movimentosController.deletar);
module.exports = router;
\ No newline at end of file
diff --git a/financeiro-api/src/modules/movimentos/services/movimentos.service.js b/financeiro-api/src/modules/movimentos/services/movimentos.service.js
index 062a499..836abbe 100644
--- a/financeiro-api/src/modules/movimentos/services/movimentos.service.js
+++ b/financeiro-api/src/modules/movimentos/services/movimentos.service.js
@@ -1,4 +1,6 @@
+const crypto = require('crypto');
const pool = require('../../../database/mysql');
+const movimentosSaldoService = require('./movimentosSaldo.service');
const CAMPOS_MOVIMENTO_SELECT = `
cp.idcontasapagar,
@@ -18,13 +20,22 @@ const CAMPOS_MOVIMENTO_SELECT = `
cp.idclientes,
cp.idveiculosdetalhes,
cp.idbancos_p,
+ cp.idmovimentosfixos,
+ cp.competencia,
+ cp.grupo_parcelamento,
+ cp.origem,
+ cp.saldo_processado,
cp.observacao,
cp.insert_date,
cp.update_date,
+ cp.deleted_at,
b.descricao AS banco_descricao,
+ b.debito AS banco_debito,
cc.descricao AS centro_custo_descricao,
c.nome AS cliente_nome,
- bp.descricao AS banco_referencia_descricao
+ bp.descricao AS banco_referencia_descricao,
+ bp.debito AS banco_referencia_debito,
+ mf.descricao AS movimento_fixo_descricao
`;
const FROM_MOVIMENTO_JOIN = `
@@ -33,6 +44,7 @@ const FROM_MOVIMENTO_JOIN = `
LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = cp.idcentrodecustos
LEFT JOIN clientes c ON c.idclientes = cp.idclientes
LEFT JOIN bancos bp ON bp.idbancos = cp.idbancos_p
+ LEFT JOIN movimentosfixos mf ON mf.idmovimentosfixos = cp.idmovimentosfixos
`;
function normalizarNumeroOuNull(valor) {
@@ -40,7 +52,13 @@ function normalizarNumeroOuNull(valor) {
return null;
}
- return Number(valor);
+ const numero = Number(valor);
+
+ if (!Number.isFinite(numero)) {
+ return null;
+ }
+
+ return numero;
}
function normalizarTextoOuNull(valor) {
@@ -48,7 +66,9 @@ function normalizarTextoOuNull(valor) {
return null;
}
- return String(valor).trim();
+ const texto = String(valor).trim();
+
+ return texto || null;
}
function limitarNumero(valor, padrao, minimo, maximo) {
@@ -69,6 +89,75 @@ function limitarNumero(valor, padrao, minimo, maximo) {
return numero;
}
+function hojeDataString() {
+ return new Date().toISOString().slice(0, 10);
+}
+
+function extrairCompetencia(data) {
+ if (!data) return null;
+
+ return String(data).slice(0, 7);
+}
+
+function addMesesDataString(data, meses) {
+ if (!data) return null;
+
+ const partes = String(data).slice(0, 10).split('-');
+
+ if (partes.length !== 3) {
+ return data;
+ }
+
+ const ano = Number(partes[0]);
+ const mes = Number(partes[1]);
+ const dia = Number(partes[2]);
+
+ const dataBase = new Date(ano, mes - 1, dia);
+ dataBase.setMonth(dataBase.getMonth() + meses);
+
+ const y = dataBase.getFullYear();
+ const m = String(dataBase.getMonth() + 1).padStart(2, '0');
+ const d = String(dataBase.getDate()).padStart(2, '0');
+
+ return `${y}-${m}-${d}`;
+}
+
+function statusBaixado(status) {
+ return status === 'Pago' || status === 'Recebido';
+}
+
+function normalizarMovimentoParaSalvar(idUsuario, dados, extras = {}) {
+ const status = dados.status || null;
+ const baixado = statusBaixado(status);
+
+ return {
+ movimento: dados.movimento,
+ descricao: normalizarTextoOuNull(dados.descricao),
+ dataentrada: dados.dataentrada || null,
+ datavencimento: dados.datavencimento || null,
+ databaixa: baixado ? (dados.databaixa || hojeDataString()) : null,
+ valor: Number(dados.valor || 0),
+ parcela: Number(dados.parcela || 1),
+ parcelas: Number(dados.parcelas || 1),
+ status,
+ idcentrodecustos: normalizarNumeroOuNull(dados.idcentrodecustos),
+ idbancos: normalizarNumeroOuNull(dados.idbancos),
+ idusuarios_cad: extras.idusuarios_cad || dados.idusuarios_cad || idUsuario || null,
+ idusuarios_baixa: baixado
+ ? (normalizarNumeroOuNull(dados.idusuarios_baixa) || idUsuario || null)
+ : null,
+ idclientes: normalizarNumeroOuNull(dados.idclientes),
+ idveiculosdetalhes: normalizarNumeroOuNull(dados.idveiculosdetalhes),
+ idbancos_p: normalizarNumeroOuNull(dados.idbancos_p),
+ idmovimentosfixos: normalizarNumeroOuNull(dados.idmovimentosfixos),
+ competencia: normalizarTextoOuNull(dados.competencia) || extrairCompetencia(dados.datavencimento),
+ grupo_parcelamento: normalizarTextoOuNull(dados.grupo_parcelamento) || extras.grupo_parcelamento || null,
+ origem: normalizarTextoOuNull(dados.origem) || extras.origem || 'manual',
+ saldo_processado: Number(dados.saldo_processado || 0),
+ observacao: normalizarTextoOuNull(dados.observacao),
+ };
+}
+
function resolverCampoData(dataCampo) {
const camposPermitidos = {
dataentrada: 'cp.dataentrada',
@@ -76,6 +165,7 @@ function resolverCampoData(dataCampo) {
databaixa: 'cp.databaixa',
insert_date: 'cp.insert_date',
update_date: 'cp.update_date',
+ deleted_at: 'cp.deleted_at',
};
return camposPermitidos[dataCampo] || 'cp.datavencimento';
@@ -83,6 +173,7 @@ function resolverCampoData(dataCampo) {
function resolverOrdenacao(orderBy) {
const camposPermitidos = {
+ idcontasapagar: 'cp.idcontasapagar',
dataentrada: 'cp.dataentrada',
datavencimento: 'cp.datavencimento',
databaixa: 'cp.databaixa',
@@ -92,6 +183,12 @@ function resolverOrdenacao(orderBy) {
descricao: 'cp.descricao',
status: 'cp.status',
movimento: 'cp.movimento',
+ competencia: 'cp.competencia',
+ origem: 'cp.origem',
+ saldo_processado: 'cp.saldo_processado',
+ banco_descricao: 'b.descricao',
+ centro_custo_descricao: 'cc.descricao',
+ cliente_nome: 'c.nome',
};
return camposPermitidos[orderBy] || 'cp.datavencimento';
@@ -105,6 +202,10 @@ function montarWhereMovimentos(filtros = {}) {
const where = [];
const params = [];
+ if (String(filtros.incluirExcluidos || '') !== '1') {
+ where.push('cp.deleted_at IS NULL');
+ }
+
if (filtros.movimento) {
where.push('cp.movimento = ?');
params.push(filtros.movimento);
@@ -135,6 +236,31 @@ function montarWhereMovimentos(filtros = {}) {
params.push(Number(filtros.idbancos_p));
}
+ if (filtros.idmovimentosfixos) {
+ where.push('cp.idmovimentosfixos = ?');
+ params.push(Number(filtros.idmovimentosfixos));
+ }
+
+ if (filtros.competencia) {
+ where.push('cp.competencia = ?');
+ params.push(filtros.competencia);
+ }
+
+ if (filtros.grupo_parcelamento) {
+ where.push('cp.grupo_parcelamento = ?');
+ params.push(filtros.grupo_parcelamento);
+ }
+
+ if (filtros.origem) {
+ where.push('cp.origem = ?');
+ params.push(filtros.origem);
+ }
+
+ if (filtros.saldo_processado !== undefined && filtros.saldo_processado !== null && filtros.saldo_processado !== '') {
+ where.push('cp.saldo_processado = ?');
+ params.push(Number(filtros.saldo_processado));
+ }
+
if (filtros.referenciaTipo === 'cliente') {
where.push('cp.idclientes IS NOT NULL');
}
@@ -164,11 +290,14 @@ function montarWhereMovimentos(filtros = {}) {
(
cp.descricao LIKE ?
OR cp.observacao LIKE ?
+ OR cp.competencia LIKE ?
+ OR cp.origem LIKE ?
OR b.descricao LIKE ?
OR cc.descricao LIKE ?
OR c.nome LIKE ?
OR c.cpf_cnpj LIKE ?
OR bp.descricao LIKE ?
+ OR mf.descricao LIKE ?
)
`);
@@ -181,6 +310,9 @@ function montarWhereMovimentos(filtros = {}) {
termo,
termo,
termo,
+ termo,
+ termo,
+ termo,
termo
);
}
@@ -207,7 +339,7 @@ async function listarMovimentos(filtros = {}) {
const [rows] = await pool.query(
`
- SELECT
+ SELECT
${CAMPOS_MOVIMENTO_SELECT}
${FROM_MOVIMENTO_JOIN}
${whereSql}
@@ -234,8 +366,11 @@ async function listarMovimentos(filtros = {}) {
COALESCE(SUM(CASE WHEN cp.movimento = 'Saida' THEN cp.valor ELSE 0 END), 0) AS totalSaidas,
COALESCE(SUM(CASE WHEN cp.movimento = 'Sangria' THEN cp.valor ELSE 0 END), 0) AS totalSangrias,
COALESCE(SUM(CASE WHEN cp.movimento = 'Estorno' THEN cp.valor ELSE 0 END), 0) AS totalEstornos,
+ COALESCE(SUM(CASE WHEN cp.status IN ('Pago', 'Recebido') THEN cp.valor ELSE 0 END), 0) AS totalBaixado,
+ COALESCE(SUM(CASE WHEN cp.status IN ('A pagar', 'A receber') THEN cp.valor ELSE 0 END), 0) AS totalAberto,
+ COALESCE(SUM(CASE WHEN cp.saldo_processado = 1 THEN 1 ELSE 0 END), 0) AS saldoProcessado,
COALESCE(SUM(
- CASE
+ CASE
WHEN cp.movimento IN ('Entrada', 'Estorno') THEN cp.valor
WHEN cp.movimento IN ('Saida', 'Sangria') THEN -cp.valor
ELSE 0
@@ -265,18 +400,25 @@ async function listarMovimentos(filtros = {}) {
totalSaidas: Number(summaryRows[0]?.totalSaidas || 0),
totalSangrias: Number(summaryRows[0]?.totalSangrias || 0),
totalEstornos: Number(summaryRows[0]?.totalEstornos || 0),
+ totalBaixado: Number(summaryRows[0]?.totalBaixado || 0),
+ totalAberto: Number(summaryRows[0]?.totalAberto || 0),
+ saldoProcessado: Number(summaryRows[0]?.saldoProcessado || 0),
saldo: Number(summaryRows[0]?.saldo || 0),
},
};
}
-async function buscarMovimentoPorId(id) {
- const [rows] = await pool.query(
+async function buscarMovimentoPorId(id, opcoes = {}) {
+ const executor = opcoes.connection || pool;
+ const incluirExcluidos = opcoes.incluirExcluidos === true;
+
+ const [rows] = await executor.query(
`
- SELECT
+ SELECT
${CAMPOS_MOVIMENTO_SELECT}
${FROM_MOVIMENTO_JOIN}
WHERE cp.idcontasapagar = ?
+ ${incluirExcluidos ? '' : 'AND cp.deleted_at IS NULL'}
LIMIT 1
`,
[id]
@@ -285,27 +427,47 @@ async function buscarMovimentoPorId(id) {
return rows[0] || null;
}
-async function criarMovimento(idUsuario, dados) {
- const {
- movimento,
- descricao,
- dataentrada,
- datavencimento,
- databaixa,
- valor,
- parcela,
- parcelas,
- status,
- idcentrodecustos,
- idbancos,
- idusuarios_baixa,
- idclientes,
- idveiculosdetalhes,
- idbancos_p,
- observacao,
- } = dados;
+async function buscarMovimentoPorIdParaUpdate(connection, id) {
+ const [rows] = await connection.query(
+ `
+ SELECT
+ cp.*
+ FROM contasapagar cp
+ WHERE cp.idcontasapagar = ?
+ AND cp.deleted_at IS NULL
+ LIMIT 1
+ FOR UPDATE
+ `,
+ [id]
+ );
- const [result] = await pool.query(
+ return rows[0] || null;
+}
+
+async function inserirAuditoriaMovimento(connection, dados) {
+ await connection.query(
+ `
+ INSERT INTO contasapagar_auditoria (
+ idcontasapagar,
+ acao,
+ dados_antes,
+ dados_depois,
+ idusuarios,
+ insert_date
+ ) VALUES (?, ?, ?, ?, ?, NOW())
+ `,
+ [
+ dados.idcontasapagar || null,
+ dados.acao,
+ dados.dados_antes ? JSON.stringify(dados.dados_antes) : null,
+ dados.dados_depois ? JSON.stringify(dados.dados_depois) : null,
+ dados.idusuarios || null,
+ ]
+ );
+}
+
+async function inserirMovimento(connection, movimento) {
+ const [result] = await connection.query(
`
INSERT INTO contasapagar (
movimento,
@@ -324,62 +486,48 @@ async function criarMovimento(idUsuario, dados) {
idclientes,
idveiculosdetalhes,
idbancos_p,
+ idmovimentosfixos,
+ competencia,
+ grupo_parcelamento,
+ origem,
+ saldo_processado,
observacao,
insert_date,
- update_date
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
+ update_date,
+ deleted_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), NULL)
`,
[
- movimento,
- normalizarTextoOuNull(descricao),
- dataentrada || null,
- datavencimento || null,
- databaixa || null,
- Number(valor || 0),
- Number(parcela || 1),
- Number(parcelas || 1),
- status || null,
- normalizarNumeroOuNull(idcentrodecustos),
- normalizarNumeroOuNull(idbancos),
- idUsuario,
- normalizarNumeroOuNull(idusuarios_baixa),
- normalizarNumeroOuNull(idclientes),
- normalizarNumeroOuNull(idveiculosdetalhes),
- normalizarNumeroOuNull(idbancos_p),
- normalizarTextoOuNull(observacao),
+ movimento.movimento,
+ movimento.descricao,
+ movimento.dataentrada,
+ movimento.datavencimento,
+ movimento.databaixa,
+ movimento.valor,
+ movimento.parcela,
+ movimento.parcelas,
+ movimento.status,
+ movimento.idcentrodecustos,
+ movimento.idbancos,
+ movimento.idusuarios_cad,
+ movimento.idusuarios_baixa,
+ movimento.idclientes,
+ movimento.idveiculosdetalhes,
+ movimento.idbancos_p,
+ movimento.idmovimentosfixos,
+ movimento.competencia,
+ movimento.grupo_parcelamento,
+ movimento.origem,
+ movimento.saldo_processado,
+ movimento.observacao,
]
);
- return buscarMovimentoPorId(result.insertId);
+ return result.insertId;
}
-async function atualizarMovimento(id, dados) {
- const movimentoAtual = await buscarMovimentoPorId(id);
-
- if (!movimentoAtual) {
- return null;
- }
-
- const {
- movimento,
- descricao,
- dataentrada,
- datavencimento,
- databaixa,
- valor,
- parcela,
- parcelas,
- status,
- idcentrodecustos,
- idbancos,
- idusuarios_baixa,
- idclientes,
- idveiculosdetalhes,
- idbancos_p,
- observacao,
- } = dados;
-
- await pool.query(
+async function atualizarMovimentoBase(connection, id, movimento, saldoProcessado) {
+ await connection.query(
`
UPDATE contasapagar
SET
@@ -398,37 +546,472 @@ async function atualizarMovimento(id, dados) {
idclientes = ?,
idveiculosdetalhes = ?,
idbancos_p = ?,
+ idmovimentosfixos = ?,
+ competencia = ?,
+ grupo_parcelamento = ?,
+ origem = ?,
+ saldo_processado = ?,
observacao = ?,
update_date = NOW()
WHERE idcontasapagar = ?
`,
[
- movimento,
- normalizarTextoOuNull(descricao),
- dataentrada || null,
- datavencimento || null,
- databaixa || null,
- Number(valor || 0),
- Number(parcela || 1),
- Number(parcelas || 1),
- status || null,
- normalizarNumeroOuNull(idcentrodecustos),
- normalizarNumeroOuNull(idbancos),
- normalizarNumeroOuNull(idusuarios_baixa),
- normalizarNumeroOuNull(idclientes),
- normalizarNumeroOuNull(idveiculosdetalhes),
- normalizarNumeroOuNull(idbancos_p),
- normalizarTextoOuNull(observacao),
+ movimento.movimento,
+ movimento.descricao,
+ movimento.dataentrada,
+ movimento.datavencimento,
+ movimento.databaixa,
+ movimento.valor,
+ movimento.parcela,
+ movimento.parcelas,
+ movimento.status,
+ movimento.idcentrodecustos,
+ movimento.idbancos,
+ movimento.idusuarios_baixa,
+ movimento.idclientes,
+ movimento.idveiculosdetalhes,
+ movimento.idbancos_p,
+ movimento.idmovimentosfixos,
+ movimento.competencia,
+ movimento.grupo_parcelamento,
+ movimento.origem,
+ saldoProcessado,
+ movimento.observacao,
id,
]
);
+}
- return buscarMovimentoPorId(id);
+async function verificarDuplicidade(dados = {}) {
+ const idIgnorar = normalizarNumeroOuNull(dados.idcontasapagar);
+ const valor = Number(dados.valor || 0);
+ const idcentrodecustos = normalizarNumeroOuNull(dados.idcentrodecustos);
+ const idbancos = normalizarNumeroOuNull(dados.idbancos);
+
+ const params = [
+ dados.movimento,
+ valor,
+ idcentrodecustos,
+ idbancos,
+ dados.datavencimento,
+ ];
+
+ let filtroId = '';
+
+ if (idIgnorar) {
+ filtroId = 'AND idcontasapagar <> ?';
+ params.push(idIgnorar);
+ }
+
+ const [rows] = await pool.query(
+ `
+ SELECT
+ idcontasapagar,
+ movimento,
+ descricao,
+ valor,
+ status,
+ datavencimento,
+ idcentrodecustos,
+ idbancos
+ FROM contasapagar
+ WHERE deleted_at IS NULL
+ AND movimento = ?
+ AND valor = ?
+ AND (
+ (? IS NULL AND idcentrodecustos IS NULL)
+ OR idcentrodecustos = ?
+ )
+ AND (
+ (? IS NULL AND idbancos IS NULL)
+ OR idbancos = ?
+ )
+ AND DATE(datavencimento) = DATE(?)
+ ${filtroId}
+ LIMIT 1
+ `,
+ [
+ dados.movimento,
+ valor,
+ idcentrodecustos,
+ idcentrodecustos,
+ idbancos,
+ idbancos,
+ dados.datavencimento,
+ ...(idIgnorar ? [idIgnorar] : []),
+ ]
+ );
+
+ return rows[0] || null;
+}
+
+function criarErroDuplicidade(movimentoDuplicado) {
+ const erro = new Error(
+ `Já existe um movimento parecido cadastrado: ID ${movimentoDuplicado.idcontasapagar}, descrição "${movimentoDuplicado.descricao}", situação "${movimentoDuplicado.status}".`
+ );
+
+ erro.code = 'DUPLICIDADE_MOVIMENTO';
+ erro.data = movimentoDuplicado;
+
+ return erro;
+}
+
+function montarParcelas(idUsuario, dados) {
+ const parcelas = Number(dados.parcelas || 1);
+ const parcelaInicial = Number(dados.parcela || 1);
+ const gerarParcelas = dados.gerarParcelas !== false && parcelas > parcelaInicial;
+
+ if (!gerarParcelas) {
+ return {
+ grupoParcelamento: normalizarTextoOuNull(dados.grupo_parcelamento),
+ movimentos: [
+ normalizarMovimentoParaSalvar(idUsuario, dados, {
+ origem: normalizarTextoOuNull(dados.origem) || 'manual',
+ }),
+ ],
+ };
+ }
+
+ const grupoParcelamento = normalizarTextoOuNull(dados.grupo_parcelamento) || crypto.randomUUID();
+ const modoParcelamento = dados.modoParcelamento || 'valor_parcela';
+
+ const valorInformado = Number(dados.valor || 0);
+ const valorParcela = modoParcelamento === 'valor_total'
+ ? Number((valorInformado / parcelas).toFixed(2))
+ : valorInformado;
+
+ const movimentos = [];
+
+ for (let parcela = parcelaInicial; parcela <= parcelas; parcela++) {
+ const indiceMes = parcela - parcelaInicial;
+
+ const dadosParcela = {
+ ...dados,
+ valor: valorParcela,
+ parcela,
+ parcelas,
+ datavencimento: addMesesDataString(dados.datavencimento, indiceMes),
+ competencia: normalizarTextoOuNull(dados.competencia)
+ ? addMesesDataString(`${dados.competencia}-01`, indiceMes).slice(0, 7)
+ : extrairCompetencia(addMesesDataString(dados.datavencimento, indiceMes)),
+ };
+
+ movimentos.push(
+ normalizarMovimentoParaSalvar(idUsuario, dadosParcela, {
+ grupo_parcelamento: grupoParcelamento,
+ origem: parcela === parcelaInicial
+ ? (normalizarTextoOuNull(dados.origem) || 'manual')
+ : 'parcelamento',
+ })
+ );
+ }
+
+ return {
+ grupoParcelamento,
+ movimentos,
+ };
+}
+
+async function aplicarSaldoMovimento(connection, movimento, contexto) {
+ const deltas = await movimentosSaldoService.calcularImpactoMovimento(connection, movimento);
+
+ if (deltas.length === 0) {
+ return {
+ saldoProcessado: 0,
+ impactos: [],
+ };
+ }
+
+ const impactos = await movimentosSaldoService.aplicarDeltasSaldo(
+ connection,
+ deltas,
+ contexto
+ );
+
+ return {
+ saldoProcessado: impactos.length > 0 ? 1 : 0,
+ impactos,
+ };
+}
+
+async function criarMovimento(idUsuario, dados) {
+ const duplicado = await verificarDuplicidade(dados);
+
+ if (duplicado && dados.confirmarDuplicado !== true) {
+ throw criarErroDuplicidade(duplicado);
+ }
+
+ const connection = await pool.getConnection();
+
+ try {
+ await connection.beginTransaction();
+
+ const { movimentos } = montarParcelas(idUsuario, dados);
+
+ const movimentosCriados = [];
+ const impactosTodos = [];
+
+ for (const movimento of movimentos) {
+ const idCriado = await inserirMovimento(connection, movimento);
+
+ const movimentoCriado = await buscarMovimentoPorId(idCriado, {
+ connection,
+ });
+
+ const { saldoProcessado, impactos } = await aplicarSaldoMovimento(
+ connection,
+ movimentoCriado,
+ {
+ idcontasapagar: idCriado,
+ tipo_operacao: 'movimento_aplicado',
+ origem: movimentoCriado.origem || 'movimento',
+ idusuarios: idUsuario,
+ }
+ );
+
+ if (saldoProcessado === 1) {
+ await connection.query(
+ `
+ UPDATE contasapagar
+ SET saldo_processado = 1
+ WHERE idcontasapagar = ?
+ `,
+ [idCriado]
+ );
+ }
+
+ const movimentoFinal = await buscarMovimentoPorId(idCriado, {
+ connection,
+ });
+
+ await inserirAuditoriaMovimento(connection, {
+ idcontasapagar: idCriado,
+ acao: movimento.parcela > 1 ? 'gerar_parcela' : 'criar',
+ dados_antes: null,
+ dados_depois: movimentoFinal,
+ idusuarios: idUsuario,
+ });
+
+ movimentosCriados.push(movimentoFinal);
+ impactosTodos.push(...impactos);
+ }
+
+ await connection.commit();
+
+ return {
+ data: movimentosCriados[0],
+ parcelasGeradas: movimentosCriados.slice(1),
+ impactoSaldo: impactosTodos,
+ };
+ } catch (error) {
+ await connection.rollback();
+ throw error;
+ } finally {
+ connection.release();
+ }
+}
+
+async function atualizarMovimento(id, idUsuario, dados) {
+ const duplicado = await verificarDuplicidade({
+ ...dados,
+ idcontasapagar: id,
+ });
+
+ if (duplicado && dados.confirmarDuplicado !== true) {
+ throw criarErroDuplicidade(duplicado);
+ }
+
+ const connection = await pool.getConnection();
+
+ try {
+ await connection.beginTransaction();
+
+ const movimentoAntigo = await buscarMovimentoPorIdParaUpdate(connection, id);
+
+ if (!movimentoAntigo) {
+ await connection.rollback();
+
+ return null;
+ }
+
+ const dadosNovos = normalizarMovimentoParaSalvar(idUsuario, dados, {
+ idusuarios_cad: movimentoAntigo.idusuarios_cad,
+ grupo_parcelamento: dados.grupo_parcelamento || movimentoAntigo.grupo_parcelamento,
+ origem: dados.origem || movimentoAntigo.origem || 'manual',
+ });
+
+ const impactosTodos = [];
+
+ if (Number(movimentoAntigo.saldo_processado) === 1) {
+ const deltasAntigos = await movimentosSaldoService.calcularImpactoMovimento(
+ connection,
+ movimentoAntigo
+ );
+
+ const impactosReversao = await movimentosSaldoService.aplicarDeltasSaldo(
+ connection,
+ movimentosSaldoService.inverterDeltas(deltasAntigos),
+ {
+ idcontasapagar: id,
+ tipo_operacao: 'movimento_revertido',
+ origem: 'edicao_movimento',
+ idusuarios: idUsuario,
+ }
+ );
+
+ impactosTodos.push(...impactosReversao);
+ }
+
+ await atualizarMovimentoBase(connection, id, dadosNovos, 0);
+
+ const movimentoNovo = await buscarMovimentoPorId(id, {
+ connection,
+ });
+
+ const { saldoProcessado, impactos } = await aplicarSaldoMovimento(
+ connection,
+ movimentoNovo,
+ {
+ idcontasapagar: id,
+ tipo_operacao: 'movimento_aplicado',
+ origem: 'edicao_movimento',
+ idusuarios: idUsuario,
+ }
+ );
+
+ if (saldoProcessado === 1) {
+ await connection.query(
+ `
+ UPDATE contasapagar
+ SET saldo_processado = 1
+ WHERE idcontasapagar = ?
+ `,
+ [id]
+ );
+ }
+
+ const movimentoFinal = await buscarMovimentoPorId(id, {
+ connection,
+ });
+
+ await inserirAuditoriaMovimento(connection, {
+ idcontasapagar: id,
+ acao: 'editar',
+ dados_antes: movimentoAntigo,
+ dados_depois: movimentoFinal,
+ idusuarios: idUsuario,
+ });
+
+ impactosTodos.push(...impactos);
+
+ await connection.commit();
+
+ return {
+ data: movimentoFinal,
+ impactoSaldo: impactosTodos,
+ };
+ } catch (error) {
+ await connection.rollback();
+ throw error;
+ } finally {
+ connection.release();
+ }
+}
+
+async function deletarMovimento(id, idUsuario) {
+ const connection = await pool.getConnection();
+
+ try {
+ await connection.beginTransaction();
+
+ const movimentoAntigo = await buscarMovimentoPorIdParaUpdate(connection, id);
+
+ if (!movimentoAntigo) {
+ await connection.rollback();
+
+ return {
+ ok: false,
+ };
+ }
+
+ const impactosTodos = [];
+
+ if (Number(movimentoAntigo.saldo_processado) === 1) {
+ const deltasAntigos = await movimentosSaldoService.calcularImpactoMovimento(
+ connection,
+ movimentoAntigo
+ );
+
+ const impactosReversao = await movimentosSaldoService.aplicarDeltasSaldo(
+ connection,
+ movimentosSaldoService.inverterDeltas(deltasAntigos),
+ {
+ idcontasapagar: id,
+ tipo_operacao: 'movimento_revertido',
+ origem: 'exclusao_movimento',
+ idusuarios: idUsuario,
+ }
+ );
+
+ impactosTodos.push(...impactosReversao);
+ }
+
+ await connection.query(
+ `
+ UPDATE contasapagar
+ SET
+ deleted_at = NOW(),
+ saldo_processado = 0,
+ update_date = NOW()
+ WHERE idcontasapagar = ?
+ `,
+ [id]
+ );
+
+ await inserirAuditoriaMovimento(connection, {
+ idcontasapagar: id,
+ acao: 'excluir',
+ dados_antes: movimentoAntigo,
+ dados_depois: null,
+ idusuarios: idUsuario,
+ });
+
+ await connection.commit();
+
+ return {
+ ok: true,
+ impactoSaldo: impactosTodos,
+ };
+ } catch (error) {
+ await connection.rollback();
+ throw error;
+ } finally {
+ connection.release();
+ }
+}
+
+async function preverImpactoSaldo(dados, idcontasapagar = null) {
+ let movimentoAntigo = null;
+
+ if (idcontasapagar) {
+ movimentoAntigo = await buscarMovimentoPorId(idcontasapagar);
+ }
+
+ const movimentoNovo = normalizarMovimentoParaSalvar(null, dados, {
+ idusuarios_cad: dados.idusuarios_cad || null,
+ grupo_parcelamento: dados.grupo_parcelamento || null,
+ origem: dados.origem || 'manual',
+ });
+
+ return movimentosSaldoService.preverImpactoSaldo(movimentoNovo, movimentoAntigo);
}
module.exports = {
listarMovimentos,
buscarMovimentoPorId,
+ verificarDuplicidade,
+ preverImpactoSaldo,
criarMovimento,
atualizarMovimento,
+ deletarMovimento,
};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/movimentos/services/movimentosSaldo.service.js b/financeiro-api/src/modules/movimentos/services/movimentosSaldo.service.js
new file mode 100644
index 0000000..414b25b
--- /dev/null
+++ b/financeiro-api/src/modules/movimentos/services/movimentosSaldo.service.js
@@ -0,0 +1,259 @@
+const pool = require('../../../database/mysql');
+
+function valorMovimento(movimento) {
+ return Number(movimento.valor || 0);
+}
+
+function bancoEhDebito(banco) {
+ return Number(banco?.debito) === 1;
+}
+
+function bancoEhCredito(banco) {
+ return Number(banco?.debito) === 0;
+}
+
+async function buscarBanco(connection, idbancos) {
+ if (!idbancos) return null;
+
+ const [rows] = await connection.query(
+ `
+ SELECT
+ idbancos,
+ descricao,
+ saldo,
+ debito
+ FROM bancos
+ WHERE idbancos = ?
+ LIMIT 1
+ `,
+ [idbancos]
+ );
+
+ return rows[0] || null;
+}
+
+async function buscarBancoParaUpdate(connection, idbancos) {
+ if (!idbancos) return null;
+
+ const [rows] = await connection.query(
+ `
+ SELECT
+ idbancos,
+ descricao,
+ saldo,
+ debito
+ FROM bancos
+ WHERE idbancos = ?
+ LIMIT 1
+ FOR UPDATE
+ `,
+ [idbancos]
+ );
+
+ return rows[0] || null;
+}
+
+function calcularDeltasParaMovimento(movimento, bancoOrigem, bancoDestino = null) {
+ const deltas = [];
+
+ if (!movimento || !bancoOrigem) {
+ return deltas;
+ }
+
+ const valor = valorMovimento(movimento);
+ const origemDebito = bancoEhDebito(bancoOrigem);
+ const origemCredito = bancoEhCredito(bancoOrigem);
+
+ if (valor <= 0) {
+ return deltas;
+ }
+
+ if (movimento.movimento === 'Entrada') {
+ if (origemDebito && movimento.status === 'Recebido') {
+ deltas.push({
+ idbancos: bancoOrigem.idbancos,
+ valor_delta: valor,
+ descricao: `Entrada recebida: ${movimento.descricao}`,
+ });
+ }
+
+ return deltas;
+ }
+
+ if (movimento.movimento === 'Saida') {
+ if (origemDebito && movimento.status === 'Pago') {
+ deltas.push({
+ idbancos: bancoOrigem.idbancos,
+ valor_delta: -valor,
+ descricao: `Saída paga: ${movimento.descricao}`,
+ });
+ }
+
+ if (origemCredito) {
+ deltas.push({
+ idbancos: bancoOrigem.idbancos,
+ valor_delta: valor,
+ descricao: `Compra no crédito acumulada: ${movimento.descricao}`,
+ });
+ }
+
+ return deltas;
+ }
+
+ if (movimento.movimento === 'Estorno') {
+ if (origemDebito && movimento.status === 'Recebido') {
+ deltas.push({
+ idbancos: bancoOrigem.idbancos,
+ valor_delta: valor,
+ descricao: `Estorno recebido: ${movimento.descricao}`,
+ });
+ }
+
+ if (origemCredito) {
+ deltas.push({
+ idbancos: bancoOrigem.idbancos,
+ valor_delta: -valor,
+ descricao: `Estorno no crédito: ${movimento.descricao}`,
+ });
+ }
+
+ return deltas;
+ }
+
+ if (movimento.movimento === 'Sangria') {
+ if (!origemDebito) {
+ return deltas;
+ }
+
+ deltas.push({
+ idbancos: bancoOrigem.idbancos,
+ valor_delta: -valor,
+ descricao: `Sangria origem: ${movimento.descricao}`,
+ });
+
+ if (bancoDestino && bancoEhDebito(bancoDestino)) {
+ deltas.push({
+ idbancos: bancoDestino.idbancos,
+ valor_delta: valor,
+ descricao: `Sangria destino: ${movimento.descricao}`,
+ });
+ }
+
+ return deltas;
+ }
+
+ return deltas;
+}
+
+async function calcularImpactoMovimento(connection, movimento) {
+ const bancoOrigem = await buscarBanco(connection, movimento.idbancos);
+ const bancoDestino = await buscarBanco(connection, movimento.idbancos_p);
+
+ return calcularDeltasParaMovimento(movimento, bancoOrigem, bancoDestino);
+}
+
+function inverterDeltas(deltas) {
+ return deltas.map((delta) => ({
+ ...delta,
+ valor_delta: Number(delta.valor_delta || 0) * -1,
+ descricao: `Reversão - ${delta.descricao || ''}`.trim(),
+ }));
+}
+
+async function aplicarDeltasSaldo(connection, deltas, contexto = {}) {
+ const impactos = [];
+
+ for (const delta of deltas) {
+ const banco = await buscarBancoParaUpdate(connection, delta.idbancos);
+
+ if (!banco) {
+ continue;
+ }
+
+ const saldoAnterior = Number(banco.saldo || 0);
+ const valorDelta = Number(delta.valor_delta || 0);
+ const saldoPosterior = saldoAnterior + valorDelta;
+
+ await connection.query(
+ `
+ UPDATE bancos
+ SET
+ saldo = ?,
+ update_date = NOW()
+ WHERE idbancos = ?
+ `,
+ [
+ saldoPosterior,
+ banco.idbancos,
+ ]
+ );
+
+ await connection.query(
+ `
+ INSERT INTO bancos_saldo_auditoria (
+ idbancos,
+ idcontasapagar,
+ idcontasquitadas,
+ tipo_operacao,
+ origem,
+ valor_delta,
+ saldo_anterior,
+ saldo_posterior,
+ descricao,
+ idusuarios,
+ insert_date
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
+ `,
+ [
+ banco.idbancos,
+ contexto.idcontasapagar || null,
+ contexto.idcontasquitadas || null,
+ contexto.tipo_operacao || null,
+ contexto.origem || null,
+ valorDelta,
+ saldoAnterior,
+ saldoPosterior,
+ delta.descricao || null,
+ contexto.idusuarios || null,
+ ]
+ );
+
+ impactos.push({
+ idbancos: banco.idbancos,
+ banco_descricao: banco.descricao,
+ valor_delta: valorDelta,
+ saldo_anterior: saldoAnterior,
+ saldo_posterior: saldoPosterior,
+ descricao: delta.descricao || null,
+ });
+ }
+
+ return impactos;
+}
+
+async function preverImpactoSaldo(dados, movimentoAntigo = null) {
+ const connection = await pool.getConnection();
+
+ try {
+ const deltas = [];
+
+ if (movimentoAntigo) {
+ const deltasAntigos = await calcularImpactoMovimento(connection, movimentoAntigo);
+ deltas.push(...inverterDeltas(deltasAntigos));
+ }
+
+ const deltasNovos = await calcularImpactoMovimento(connection, dados);
+ deltas.push(...deltasNovos);
+
+ return deltas;
+ } finally {
+ connection.release();
+ }
+}
+
+module.exports = {
+ calcularImpactoMovimento,
+ aplicarDeltasSaldo,
+ inverterDeltas,
+ preverImpactoSaldo,
+};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/movimentosFixos/controllers/movimentosFixos.controller.js b/financeiro-api/src/modules/movimentosFixos/controllers/movimentosFixos.controller.js
new file mode 100644
index 0000000..d93637f
--- /dev/null
+++ b/financeiro-api/src/modules/movimentosFixos/controllers/movimentosFixos.controller.js
@@ -0,0 +1,292 @@
+const movimentosFixosService = require('../services/movimentosFixos.service');
+
+function validarDadosMovimentoFixo(dados) {
+ if (!dados.movimento) {
+ return 'Movimento é obrigatório.';
+ }
+
+ if (!['Entrada', 'Saida', 'Sangria', 'Estorno'].includes(dados.movimento)) {
+ return 'Movimento inválido.';
+ }
+
+ if (!dados.descricao || !String(dados.descricao).trim()) {
+ return 'Descrição é obrigatória.';
+ }
+
+ if (dados.valor === undefined || dados.valor === null || dados.valor === '') {
+ return 'Valor é obrigatório.';
+ }
+
+ if (!Number.isFinite(Number(dados.valor))) {
+ return 'Valor inválido.';
+ }
+
+ if (Number(dados.valor) <= 0) {
+ return 'Valor deve ser maior que zero.';
+ }
+
+ if (dados.dia_vencimento === undefined || dados.dia_vencimento === null || dados.dia_vencimento === '') {
+ return 'Dia de vencimento é obrigatório.';
+ }
+
+ const diaVencimento = Number(dados.dia_vencimento);
+
+ if (!Number.isInteger(diaVencimento) || diaVencimento < 1 || diaVencimento > 31) {
+ return 'Dia de vencimento deve estar entre 1 e 31.';
+ }
+
+ if (dados.idcentrodecustos !== undefined && dados.idcentrodecustos !== null && dados.idcentrodecustos !== '') {
+ const idcentrodecustos = Number(dados.idcentrodecustos);
+
+ if (!Number.isInteger(idcentrodecustos) || idcentrodecustos <= 0) {
+ return 'Centro de custo inválido.';
+ }
+ }
+
+ if (dados.idbancos !== undefined && dados.idbancos !== null && dados.idbancos !== '') {
+ const idbancos = Number(dados.idbancos);
+
+ if (!Number.isInteger(idbancos) || idbancos <= 0) {
+ return 'Banco/carteira inválido.';
+ }
+ }
+
+ if (dados.habilitado !== undefined && dados.habilitado !== null && dados.habilitado !== '') {
+ const habilitado = Number(dados.habilitado);
+
+ if (![0, 1].includes(habilitado)) {
+ return 'Status inválido.';
+ }
+ }
+
+ return null;
+}
+
+async function listar(req, res) {
+ try {
+ const resultado = await movimentosFixosService.listarMovimentosFixos({
+ limite: req.query.limite,
+ offset: req.query.offset,
+ page: req.query.page,
+ busca: req.query.busca,
+ movimento: req.query.movimento,
+ idbancos: req.query.idbancos,
+ idcentrodecustos: req.query.idcentrodecustos,
+ habilitado: req.query.habilitado,
+ diaInicio: req.query.diaInicio,
+ diaFim: req.query.diaFim,
+ orderBy: req.query.orderBy,
+ orderDirection: req.query.orderDirection,
+ });
+
+ return res.json({
+ ok: true,
+ data: resultado.data,
+ pagination: resultado.pagination,
+ summary: resultado.summary,
+ });
+ } catch (error) {
+ console.error('Erro ao listar movimentos fixos:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao listar movimentos fixos.',
+ });
+ }
+}
+
+async function buscarPorId(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const movimentoFixo = await movimentosFixosService.buscarMovimentoFixoPorId(id);
+
+ if (!movimentoFixo) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Movimento fixo não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ data: movimentoFixo,
+ });
+ } catch (error) {
+ console.error('Erro ao buscar movimento fixo:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao buscar movimento fixo.',
+ });
+ }
+}
+
+async function criar(req, res) {
+ try {
+ const erroValidacao = validarDadosMovimentoFixo(req.body);
+
+ if (erroValidacao) {
+ return res.status(400).json({
+ ok: false,
+ message: erroValidacao,
+ });
+ }
+
+ const novoMovimentoFixo = await movimentosFixosService.criarMovimentoFixo(req.body);
+
+ return res.status(201).json({
+ ok: true,
+ message: 'Movimento fixo cadastrado com sucesso.',
+ data: novoMovimentoFixo,
+ });
+ } catch (error) {
+ console.error('Erro ao criar movimento fixo:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao criar movimento fixo.',
+ });
+ }
+}
+
+async function atualizar(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const erroValidacao = validarDadosMovimentoFixo(req.body);
+
+ if (erroValidacao) {
+ return res.status(400).json({
+ ok: false,
+ message: erroValidacao,
+ });
+ }
+
+ const movimentoAtualizado = await movimentosFixosService.atualizarMovimentoFixo(id, req.body);
+
+ if (!movimentoAtualizado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Movimento fixo não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: 'Movimento fixo atualizado com sucesso.',
+ data: movimentoAtualizado,
+ });
+ } catch (error) {
+ console.error('Erro ao atualizar movimento fixo:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao atualizar movimento fixo.',
+ });
+ }
+}
+
+async function alterarHabilitado(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const habilitado = Number(req.body.habilitado);
+
+ if (![0, 1].includes(habilitado)) {
+ return res.status(400).json({
+ ok: false,
+ message: 'Status inválido.',
+ });
+ }
+
+ const movimentoAtualizado =
+ await movimentosFixosService.alterarHabilitadoMovimentoFixo(id, habilitado);
+
+ if (!movimentoAtualizado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Movimento fixo não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: habilitado === 1
+ ? 'Movimento fixo habilitado com sucesso.'
+ : 'Movimento fixo desabilitado com sucesso.',
+ data: movimentoAtualizado,
+ });
+ } catch (error) {
+ console.error('Erro ao alterar status do movimento fixo:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao alterar status do movimento fixo.',
+ });
+ }
+}
+
+async function deletar(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const deletado = await movimentosFixosService.deletarMovimentoFixo(id);
+
+ if (!deletado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Movimento fixo não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: 'Movimento fixo excluído com sucesso.',
+ });
+ } catch (error) {
+ console.error('Erro ao excluir movimento fixo:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao excluir movimento fixo.',
+ });
+ }
+}
+
+module.exports = {
+ listar,
+ buscarPorId,
+ criar,
+ atualizar,
+ alterarHabilitado,
+ deletar,
+};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/movimentosFixos/routes/movimentosFixos.routes.js b/financeiro-api/src/modules/movimentosFixos/routes/movimentosFixos.routes.js
new file mode 100644
index 0000000..1663a45
--- /dev/null
+++ b/financeiro-api/src/modules/movimentosFixos/routes/movimentosFixos.routes.js
@@ -0,0 +1,16 @@
+const express = require('express');
+const authMiddleware = require('../../../middlewares/auth.middleware');
+const movimentosFixosController = require('../controllers/movimentosFixos.controller');
+
+const router = express.Router();
+
+router.use(authMiddleware);
+
+router.get('/', movimentosFixosController.listar);
+router.get('/:id', movimentosFixosController.buscarPorId);
+router.post('/', movimentosFixosController.criar);
+router.put('/:id', movimentosFixosController.atualizar);
+router.patch('/:id/habilitado', movimentosFixosController.alterarHabilitado);
+router.delete('/:id', movimentosFixosController.deletar);
+
+module.exports = router;
\ No newline at end of file
diff --git a/financeiro-api/src/modules/movimentosFixos/services/movimentosFixos.service.js b/financeiro-api/src/modules/movimentosFixos/services/movimentosFixos.service.js
new file mode 100644
index 0000000..9fa9456
--- /dev/null
+++ b/financeiro-api/src/modules/movimentosFixos/services/movimentosFixos.service.js
@@ -0,0 +1,395 @@
+const pool = require('../../../database/mysql');
+
+const CAMPOS_MOVIMENTO_FIXO_SELECT = `
+ mf.idmovimentosfixos,
+ mf.movimento,
+ mf.descricao,
+ mf.valor,
+ mf.dia_vencimento,
+ mf.idcentrodecustos,
+ mf.habilitado,
+ mf.idbancos,
+ mf.insert_date,
+ mf.update_date,
+ b.descricao AS banco_descricao,
+ cc.descricao AS centro_custo_descricao
+`;
+
+const FROM_MOVIMENTO_FIXO_JOIN = `
+ FROM movimentosfixos mf
+ LEFT JOIN bancos b ON b.idbancos = mf.idbancos
+ LEFT JOIN centrodecustos cc ON cc.idcentrodecustos = mf.idcentrodecustos
+`;
+
+function normalizarTextoOuNull(valor) {
+ if (valor === undefined || valor === null || valor === '') {
+ return null;
+ }
+
+ return String(valor).trim();
+}
+
+function normalizarNumeroOuNull(valor) {
+ if (valor === undefined || valor === null || valor === '') {
+ return null;
+ }
+
+ const numero = Number(valor);
+
+ if (!Number.isFinite(numero)) {
+ return null;
+ }
+
+ return numero;
+}
+
+function normalizarNumero(valor, padrao = 0) {
+ if (valor === undefined || valor === null || valor === '') {
+ return padrao;
+ }
+
+ const numero = Number(valor);
+
+ if (!Number.isFinite(numero)) {
+ return padrao;
+ }
+
+ return numero;
+}
+
+function normalizarFlag(valor, padrao = 0) {
+ if (valor === undefined || valor === null || valor === '') {
+ return padrao;
+ }
+
+ return Number(valor) === 1 ? 1 : 0;
+}
+
+function limitarNumero(valor, padrao, minimo, maximo) {
+ const numero = Number(valor);
+
+ if (!Number.isFinite(numero)) {
+ return padrao;
+ }
+
+ if (numero < minimo) {
+ return minimo;
+ }
+
+ if (numero > maximo) {
+ return maximo;
+ }
+
+ return numero;
+}
+
+function resolverOrdenacao(orderBy) {
+ const camposPermitidos = {
+ idmovimentosfixos: 'mf.idmovimentosfixos',
+ movimento: 'mf.movimento',
+ descricao: 'mf.descricao',
+ valor: 'mf.valor',
+ dia_vencimento: 'mf.dia_vencimento',
+ idcentrodecustos: 'mf.idcentrodecustos',
+ idbancos: 'mf.idbancos',
+ habilitado: 'mf.habilitado',
+ insert_date: 'mf.insert_date',
+ update_date: 'mf.update_date',
+ banco_descricao: 'b.descricao',
+ centro_custo_descricao: 'cc.descricao',
+ };
+
+ return camposPermitidos[orderBy] || 'mf.dia_vencimento';
+}
+
+function resolverDirecao(orderDirection) {
+ return String(orderDirection || '').toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
+}
+
+function montarWhereMovimentosFixos(filtros = {}) {
+ const where = [];
+ const params = [];
+
+ if (filtros.movimento) {
+ where.push('mf.movimento = ?');
+ params.push(filtros.movimento);
+ }
+
+ if (filtros.idbancos) {
+ where.push('mf.idbancos = ?');
+ params.push(Number(filtros.idbancos));
+ }
+
+ if (filtros.idcentrodecustos) {
+ where.push('mf.idcentrodecustos = ?');
+ params.push(Number(filtros.idcentrodecustos));
+ }
+
+ if (filtros.habilitado !== undefined && filtros.habilitado !== null && filtros.habilitado !== '') {
+ where.push('mf.habilitado = ?');
+ params.push(Number(filtros.habilitado));
+ }
+
+ if (filtros.diaInicio) {
+ where.push('mf.dia_vencimento >= ?');
+ params.push(Number(filtros.diaInicio));
+ }
+
+ if (filtros.diaFim) {
+ where.push('mf.dia_vencimento <= ?');
+ params.push(Number(filtros.diaFim));
+ }
+
+ if (filtros.busca) {
+ where.push(`
+ (
+ mf.descricao LIKE ?
+ OR mf.movimento LIKE ?
+ OR b.descricao LIKE ?
+ OR cc.descricao LIKE ?
+ )
+ `);
+
+ const termo = `%${String(filtros.busca).trim()}%`;
+
+ params.push(
+ termo,
+ termo,
+ termo,
+ termo
+ );
+ }
+
+ const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
+
+ return {
+ whereSql,
+ params,
+ };
+}
+
+async function listarMovimentosFixos(filtros = {}) {
+ const limite = limitarNumero(filtros.limite, 20, 1, 100);
+ const page = limitarNumero(filtros.page, 1, 1, 999999);
+ const offset = filtros.offset !== undefined
+ ? limitarNumero(filtros.offset, 0, 0, 999999999)
+ : (page - 1) * limite;
+
+ const orderBy = resolverOrdenacao(filtros.orderBy);
+ const orderDirection = resolverDirecao(filtros.orderDirection);
+
+ const { whereSql, params } = montarWhereMovimentosFixos(filtros);
+
+ const [rows] = await pool.query(
+ `
+ SELECT
+ ${CAMPOS_MOVIMENTO_FIXO_SELECT}
+ ${FROM_MOVIMENTO_FIXO_JOIN}
+ ${whereSql}
+ ORDER BY ${orderBy} ${orderDirection}, mf.idmovimentosfixos ASC
+ LIMIT ? OFFSET ?
+ `,
+ [...params, limite, offset]
+ );
+
+ const [countRows] = await pool.query(
+ `
+ SELECT COUNT(*) AS total
+ ${FROM_MOVIMENTO_FIXO_JOIN}
+ ${whereSql}
+ `,
+ params
+ );
+
+ const [summaryRows] = await pool.query(
+ `
+ SELECT
+ COUNT(*) AS quantidade,
+ COALESCE(SUM(mf.valor), 0) AS valorTotal,
+ COALESCE(SUM(CASE WHEN mf.movimento = 'Entrada' THEN mf.valor ELSE 0 END), 0) AS totalEntradas,
+ COALESCE(SUM(CASE WHEN mf.movimento = 'Saida' THEN mf.valor ELSE 0 END), 0) AS totalSaidas,
+ COALESCE(SUM(CASE WHEN mf.movimento = 'Sangria' THEN mf.valor ELSE 0 END), 0) AS totalSangrias,
+ COALESCE(SUM(CASE WHEN mf.movimento = 'Estorno' THEN mf.valor ELSE 0 END), 0) AS totalEstornos,
+ COALESCE(SUM(CASE WHEN mf.habilitado = 1 THEN 1 ELSE 0 END), 0) AS habilitados,
+ COALESCE(SUM(CASE WHEN mf.habilitado = 0 THEN 1 ELSE 0 END), 0) AS desabilitados
+ ${FROM_MOVIMENTO_FIXO_JOIN}
+ ${whereSql}
+ `,
+ params
+ );
+
+ const total = Number(countRows[0]?.total || 0);
+ const totalPages = Math.max(1, Math.ceil(total / limite));
+
+ return {
+ data: rows,
+ pagination: {
+ total,
+ limite,
+ offset,
+ page: Math.floor(offset / limite) + 1,
+ totalPages,
+ },
+ summary: {
+ quantidade: Number(summaryRows[0]?.quantidade || 0),
+ valorTotal: Number(summaryRows[0]?.valorTotal || 0),
+ totalEntradas: Number(summaryRows[0]?.totalEntradas || 0),
+ totalSaidas: Number(summaryRows[0]?.totalSaidas || 0),
+ totalSangrias: Number(summaryRows[0]?.totalSangrias || 0),
+ totalEstornos: Number(summaryRows[0]?.totalEstornos || 0),
+ habilitados: Number(summaryRows[0]?.habilitados || 0),
+ desabilitados: Number(summaryRows[0]?.desabilitados || 0),
+ },
+ };
+}
+
+async function buscarMovimentoFixoPorId(id) {
+ const [rows] = await pool.query(
+ `
+ SELECT
+ ${CAMPOS_MOVIMENTO_FIXO_SELECT}
+ ${FROM_MOVIMENTO_FIXO_JOIN}
+ WHERE mf.idmovimentosfixos = ?
+ LIMIT 1
+ `,
+ [id]
+ );
+
+ return rows[0] || null;
+}
+
+async function criarMovimentoFixo(dados) {
+ const {
+ movimento,
+ descricao,
+ valor,
+ dia_vencimento,
+ idcentrodecustos,
+ habilitado,
+ idbancos,
+ } = dados;
+
+ const [result] = await pool.query(
+ `
+ INSERT INTO movimentosfixos (
+ movimento,
+ descricao,
+ valor,
+ dia_vencimento,
+ idcentrodecustos,
+ habilitado,
+ idbancos,
+ insert_date,
+ update_date
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
+ `,
+ [
+ normalizarTextoOuNull(movimento),
+ normalizarTextoOuNull(descricao),
+ normalizarNumero(valor, 0),
+ normalizarNumero(dia_vencimento, 1),
+ normalizarNumeroOuNull(idcentrodecustos),
+ normalizarFlag(habilitado, 1),
+ normalizarNumeroOuNull(idbancos),
+ ]
+ );
+
+ return buscarMovimentoFixoPorId(result.insertId);
+}
+
+async function atualizarMovimentoFixo(id, dados) {
+ const movimentoAtual = await buscarMovimentoFixoPorId(id);
+
+ if (!movimentoAtual) {
+ return null;
+ }
+
+ const {
+ movimento,
+ descricao,
+ valor,
+ dia_vencimento,
+ idcentrodecustos,
+ habilitado,
+ idbancos,
+ } = dados;
+
+ await pool.query(
+ `
+ UPDATE movimentosfixos
+ SET
+ movimento = ?,
+ descricao = ?,
+ valor = ?,
+ dia_vencimento = ?,
+ idcentrodecustos = ?,
+ habilitado = ?,
+ idbancos = ?,
+ update_date = NOW()
+ WHERE idmovimentosfixos = ?
+ `,
+ [
+ normalizarTextoOuNull(movimento),
+ normalizarTextoOuNull(descricao),
+ normalizarNumero(valor, 0),
+ normalizarNumero(dia_vencimento, 1),
+ normalizarNumeroOuNull(idcentrodecustos),
+ normalizarFlag(habilitado, 1),
+ normalizarNumeroOuNull(idbancos),
+ id,
+ ]
+ );
+
+ return buscarMovimentoFixoPorId(id);
+}
+
+async function alterarHabilitadoMovimentoFixo(id, habilitado) {
+ const movimentoAtual = await buscarMovimentoFixoPorId(id);
+
+ if (!movimentoAtual) {
+ return null;
+ }
+
+ await pool.query(
+ `
+ UPDATE movimentosfixos
+ SET
+ habilitado = ?,
+ update_date = NOW()
+ WHERE idmovimentosfixos = ?
+ `,
+ [
+ normalizarFlag(habilitado, 1),
+ id,
+ ]
+ );
+
+ return buscarMovimentoFixoPorId(id);
+}
+
+async function deletarMovimentoFixo(id) {
+ const movimentoAtual = await buscarMovimentoFixoPorId(id);
+
+ if (!movimentoAtual) {
+ return false;
+ }
+
+ await pool.query(
+ `
+ DELETE FROM movimentosfixos
+ WHERE idmovimentosfixos = ?
+ `,
+ [id]
+ );
+
+ return true;
+}
+
+module.exports = {
+ listarMovimentosFixos,
+ buscarMovimentoFixoPorId,
+ criarMovimentoFixo,
+ atualizarMovimentoFixo,
+ alterarHabilitadoMovimentoFixo,
+ deletarMovimentoFixo,
+};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/referencias/services/referencias.service.js b/financeiro-api/src/modules/referencias/services/referencias.service.js
index d3b8de0..2d227d7 100644
--- a/financeiro-api/src/modules/referencias/services/referencias.service.js
+++ b/financeiro-api/src/modules/referencias/services/referencias.service.js
@@ -9,6 +9,7 @@ async function listarBancos() {
saldo,
debito
FROM bancos
+ WHERE habilitado = 1
ORDER BY descricao ASC
`
);
@@ -26,6 +27,7 @@ async function listarCentrosCusto() {
simular,
investimento
FROM centrodecustos
+ WHERE habilitado = 1
ORDER BY descricao ASC
`
);
@@ -46,6 +48,7 @@ async function listarClientes() {
estado,
pessoafisica
FROM clientes
+ WHERE habilitado = 1
ORDER BY nome ASC
`
);
diff --git a/financeiro-api/src/modules/usuarios/controllers/usuarios.controller.js b/financeiro-api/src/modules/usuarios/controllers/usuarios.controller.js
new file mode 100644
index 0000000..1c8612c
--- /dev/null
+++ b/financeiro-api/src/modules/usuarios/controllers/usuarios.controller.js
@@ -0,0 +1,261 @@
+const usuariosService = require('../services/usuarios.service');
+
+function validarDadosUsuario(dados, modo = 'create') {
+ if (!dados.nome || !String(dados.nome).trim()) {
+ return 'Nome é obrigatório.';
+ }
+
+ if (modo === 'create' && (!dados.senha || !String(dados.senha).trim())) {
+ return 'Senha é obrigatória.';
+ }
+
+ if (dados.senha !== undefined && dados.senha !== null && dados.senha !== '') {
+ if (String(dados.senha).length < 3) {
+ return 'Senha deve ter pelo menos 3 caracteres.';
+ }
+
+ if (String(dados.senha).length > 45) {
+ return 'Senha deve ter no máximo 45 caracteres.';
+ }
+ }
+
+ if (dados.habilitado !== undefined && dados.habilitado !== null && dados.habilitado !== '') {
+ const habilitado = Number(dados.habilitado);
+
+ if (![0, 1].includes(habilitado)) {
+ return 'Status inválido.';
+ }
+ }
+
+ return null;
+}
+
+async function listar(req, res) {
+ try {
+ const resultado = await usuariosService.listarUsuarios({
+ limite: req.query.limite,
+ offset: req.query.offset,
+ page: req.query.page,
+ busca: req.query.busca,
+ habilitado: req.query.habilitado,
+ orderBy: req.query.orderBy,
+ orderDirection: req.query.orderDirection,
+ });
+
+ return res.json({
+ ok: true,
+ data: resultado.data,
+ pagination: resultado.pagination,
+ summary: resultado.summary,
+ });
+ } catch (error) {
+ console.error('Erro ao listar usuários:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao listar usuários.',
+ });
+ }
+}
+
+async function buscarPorId(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const usuario = await usuariosService.buscarUsuarioPorId(id);
+
+ if (!usuario) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Usuário não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ data: usuario,
+ });
+ } catch (error) {
+ console.error('Erro ao buscar usuário:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao buscar usuário.',
+ });
+ }
+}
+
+async function criar(req, res) {
+ try {
+ const erroValidacao = validarDadosUsuario(req.body, 'create');
+
+ if (erroValidacao) {
+ return res.status(400).json({
+ ok: false,
+ message: erroValidacao,
+ });
+ }
+
+ const novoUsuario = await usuariosService.criarUsuario(req.body);
+
+ return res.status(201).json({
+ ok: true,
+ message: 'Usuário cadastrado com sucesso.',
+ data: novoUsuario,
+ });
+ } catch (error) {
+ console.error('Erro ao criar usuário:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao criar usuário.',
+ });
+ }
+}
+
+async function atualizar(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const erroValidacao = validarDadosUsuario(req.body, 'edit');
+
+ if (erroValidacao) {
+ return res.status(400).json({
+ ok: false,
+ message: erroValidacao,
+ });
+ }
+
+ const usuarioAtualizado = await usuariosService.atualizarUsuario(id, req.body);
+
+ if (!usuarioAtualizado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Usuário não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: 'Usuário atualizado com sucesso.',
+ data: usuarioAtualizado,
+ });
+ } catch (error) {
+ console.error('Erro ao atualizar usuário:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao atualizar usuário.',
+ });
+ }
+}
+
+async function alterarHabilitado(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const habilitado = Number(req.body.habilitado);
+
+ if (![0, 1].includes(habilitado)) {
+ return res.status(400).json({
+ ok: false,
+ message: 'Status inválido.',
+ });
+ }
+
+ const usuarioAtualizado = await usuariosService.alterarHabilitadoUsuario(id, habilitado);
+
+ if (!usuarioAtualizado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Usuário não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: habilitado === 1
+ ? 'Usuário habilitado com sucesso.'
+ : 'Usuário desabilitado com sucesso.',
+ data: usuarioAtualizado,
+ });
+ } catch (error) {
+ console.error('Erro ao alterar status do usuário:', error);
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao alterar status do usuário.',
+ });
+ }
+}
+
+async function deletar(req, res) {
+ try {
+ const id = Number(req.params.id);
+
+ if (!id) {
+ return res.status(400).json({
+ ok: false,
+ message: 'ID inválido.',
+ });
+ }
+
+ const deletado = await usuariosService.deletarUsuario(id);
+
+ if (!deletado) {
+ return res.status(404).json({
+ ok: false,
+ message: 'Usuário não encontrado.',
+ });
+ }
+
+ return res.json({
+ ok: true,
+ message: 'Usuário excluído com sucesso.',
+ });
+ } catch (error) {
+ console.error('Erro ao excluir usuário:', error);
+
+ if (error.code === 'ER_ROW_IS_REFERENCED_2') {
+ return res.status(400).json({
+ ok: false,
+ message: 'Este usuário possui movimentos vinculados e não pode ser excluído.',
+ });
+ }
+
+ return res.status(500).json({
+ ok: false,
+ message: 'Erro ao excluir usuário.',
+ });
+ }
+}
+
+module.exports = {
+ listar,
+ buscarPorId,
+ criar,
+ atualizar,
+ alterarHabilitado,
+ deletar,
+};
\ No newline at end of file
diff --git a/financeiro-api/src/modules/usuarios/routes/usuarios.routes.js b/financeiro-api/src/modules/usuarios/routes/usuarios.routes.js
new file mode 100644
index 0000000..411e4df
--- /dev/null
+++ b/financeiro-api/src/modules/usuarios/routes/usuarios.routes.js
@@ -0,0 +1,16 @@
+const express = require('express');
+const authMiddleware = require('../../../middlewares/auth.middleware');
+const usuariosController = require('../controllers/usuarios.controller');
+
+const router = express.Router();
+
+router.use(authMiddleware);
+
+router.get('/', usuariosController.listar);
+router.get('/:id', usuariosController.buscarPorId);
+router.post('/', usuariosController.criar);
+router.put('/:id', usuariosController.atualizar);
+router.patch('/:id/habilitado', usuariosController.alterarHabilitado);
+router.delete('/:id', usuariosController.deletar);
+
+module.exports = router;
\ No newline at end of file
diff --git a/financeiro-api/src/modules/usuarios/services/usuarios.service.js b/financeiro-api/src/modules/usuarios/services/usuarios.service.js
new file mode 100644
index 0000000..ec812bd
--- /dev/null
+++ b/financeiro-api/src/modules/usuarios/services/usuarios.service.js
@@ -0,0 +1,313 @@
+const pool = require('../../../database/mysql');
+
+const CAMPOS_USUARIO_SELECT = `
+ idusuarios,
+ nome,
+ anotacoes,
+ habilitado,
+ insert_date,
+ update_date
+`;
+
+function normalizarTextoOuNull(valor) {
+ if (valor === undefined || valor === null || valor === '') {
+ return null;
+ }
+
+ return String(valor).trim();
+}
+
+function normalizarFlag(valor, padrao = 0) {
+ if (valor === undefined || valor === null || valor === '') {
+ return padrao;
+ }
+
+ return Number(valor) === 1 ? 1 : 0;
+}
+
+function limitarNumero(valor, padrao, minimo, maximo) {
+ const numero = Number(valor);
+
+ if (!Number.isFinite(numero)) {
+ return padrao;
+ }
+
+ if (numero < minimo) {
+ return minimo;
+ }
+
+ if (numero > maximo) {
+ return maximo;
+ }
+
+ return numero;
+}
+
+function resolverOrdenacao(orderBy) {
+ const camposPermitidos = {
+ idusuarios: 'idusuarios',
+ nome: 'nome',
+ habilitado: 'habilitado',
+ insert_date: 'insert_date',
+ update_date: 'update_date',
+ };
+
+ return camposPermitidos[orderBy] || 'nome';
+}
+
+function resolverDirecao(orderDirection) {
+ return String(orderDirection || '').toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
+}
+
+function montarWhereUsuarios(filtros = {}) {
+ const where = [];
+ const params = [];
+
+ if (filtros.habilitado !== undefined && filtros.habilitado !== null && filtros.habilitado !== '') {
+ where.push('habilitado = ?');
+ params.push(Number(filtros.habilitado));
+ }
+
+ if (filtros.busca) {
+ where.push(`
+ (
+ nome LIKE ?
+ OR anotacoes LIKE ?
+ )
+ `);
+
+ const termo = `%${String(filtros.busca).trim()}%`;
+
+ params.push(termo, termo);
+ }
+
+ const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
+
+ return {
+ whereSql,
+ params,
+ };
+}
+
+async function listarUsuarios(filtros = {}) {
+ const limite = limitarNumero(filtros.limite, 20, 1, 100);
+ const page = limitarNumero(filtros.page, 1, 1, 999999);
+ const offset = filtros.offset !== undefined
+ ? limitarNumero(filtros.offset, 0, 0, 999999999)
+ : (page - 1) * limite;
+
+ const orderBy = resolverOrdenacao(filtros.orderBy);
+ const orderDirection = resolverDirecao(filtros.orderDirection);
+
+ const { whereSql, params } = montarWhereUsuarios(filtros);
+
+ const [rows] = await pool.query(
+ `
+ SELECT
+ ${CAMPOS_USUARIO_SELECT}
+ FROM usuarios
+ ${whereSql}
+ ORDER BY ${orderBy} ${orderDirection}, idusuarios ASC
+ LIMIT ? OFFSET ?
+ `,
+ [...params, limite, offset]
+ );
+
+ const [countRows] = await pool.query(
+ `
+ SELECT COUNT(*) AS total
+ FROM usuarios
+ ${whereSql}
+ `,
+ params
+ );
+
+ const [summaryRows] = await pool.query(
+ `
+ SELECT
+ COUNT(*) AS quantidade,
+ 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
+ FROM usuarios
+ ${whereSql}
+ `,
+ params
+ );
+
+ const total = Number(countRows[0]?.total || 0);
+ const totalPages = Math.max(1, Math.ceil(total / limite));
+
+ return {
+ data: rows,
+ pagination: {
+ total,
+ limite,
+ offset,
+ page: Math.floor(offset / limite) + 1,
+ totalPages,
+ },
+ summary: {
+ quantidade: Number(summaryRows[0]?.quantidade || 0),
+ habilitados: Number(summaryRows[0]?.habilitados || 0),
+ desabilitados: Number(summaryRows[0]?.desabilitados || 0),
+ },
+ };
+}
+
+async function buscarUsuarioPorId(id) {
+ const [rows] = await pool.query(
+ `
+ SELECT
+ ${CAMPOS_USUARIO_SELECT}
+ FROM usuarios
+ WHERE idusuarios = ?
+ LIMIT 1
+ `,
+ [id]
+ );
+
+ return rows[0] || null;
+}
+
+async function buscarUsuarioComSenhaPorId(id) {
+ const [rows] = await pool.query(
+ `
+ SELECT
+ idusuarios,
+ nome,
+ senha,
+ anotacoes,
+ habilitado,
+ insert_date,
+ update_date
+ FROM usuarios
+ WHERE idusuarios = ?
+ LIMIT 1
+ `,
+ [id]
+ );
+
+ return rows[0] || null;
+}
+
+async function criarUsuario(dados) {
+ const {
+ nome,
+ senha,
+ anotacoes,
+ habilitado,
+ } = dados;
+
+ const [result] = await pool.query(
+ `
+ INSERT INTO usuarios (
+ nome,
+ senha,
+ anotacoes,
+ habilitado,
+ insert_date,
+ update_date
+ ) VALUES (?, ?, ?, ?, NOW(), NOW())
+ `,
+ [
+ normalizarTextoOuNull(nome),
+ normalizarTextoOuNull(senha),
+ normalizarTextoOuNull(anotacoes),
+ normalizarFlag(habilitado, 1),
+ ]
+ );
+
+ return buscarUsuarioPorId(result.insertId);
+}
+
+async function atualizarUsuario(id, dados) {
+ const usuarioAtual = await buscarUsuarioComSenhaPorId(id);
+
+ if (!usuarioAtual) {
+ return null;
+ }
+
+ const {
+ nome,
+ senha,
+ anotacoes,
+ habilitado,
+ } = dados;
+
+ const senhaFinal = senha === undefined || senha === null || senha === ''
+ ? usuarioAtual.senha
+ : normalizarTextoOuNull(senha);
+
+ await pool.query(
+ `
+ UPDATE usuarios
+ SET
+ nome = ?,
+ senha = ?,
+ anotacoes = ?,
+ habilitado = ?,
+ update_date = NOW()
+ WHERE idusuarios = ?
+ `,
+ [
+ normalizarTextoOuNull(nome),
+ senhaFinal,
+ normalizarTextoOuNull(anotacoes),
+ normalizarFlag(habilitado, 1),
+ id,
+ ]
+ );
+
+ return buscarUsuarioPorId(id);
+}
+
+async function alterarHabilitadoUsuario(id, habilitado) {
+ const usuarioAtual = await buscarUsuarioPorId(id);
+
+ if (!usuarioAtual) {
+ return null;
+ }
+
+ await pool.query(
+ `
+ UPDATE usuarios
+ SET
+ habilitado = ?,
+ update_date = NOW()
+ WHERE idusuarios = ?
+ `,
+ [
+ normalizarFlag(habilitado, 1),
+ id,
+ ]
+ );
+
+ return buscarUsuarioPorId(id);
+}
+
+async function deletarUsuario(id) {
+ const usuarioAtual = await buscarUsuarioPorId(id);
+
+ if (!usuarioAtual) {
+ return false;
+ }
+
+ await pool.query(
+ `
+ DELETE FROM usuarios
+ WHERE idusuarios = ?
+ `,
+ [id]
+ );
+
+ return true;
+}
+
+module.exports = {
+ listarUsuarios,
+ buscarUsuarioPorId,
+ criarUsuario,
+ atualizarUsuario,
+ alterarHabilitadoUsuario,
+ deletarUsuario,
+};
\ No newline at end of file
diff --git a/financeiro-web/src/app/router.tsx b/financeiro-web/src/app/router.tsx
index 208aa7f..f0c51df 100644
--- a/financeiro-web/src/app/router.tsx
+++ b/financeiro-web/src/app/router.tsx
@@ -9,6 +9,21 @@ import { NovoMovimentoPage } from '../features/movimentos/pages/NovoMovimentoPag
import { RelatoriosPage } from '../features/relatorios/pages/RelatoriosPage';
import { DashboardPage } from '../pages/DashboardPage';
import { NotFoundPage } from '../pages/NotFoundPage';
+import { BancosPage } from '../features/bancos/pages/BancosPage';
+import { NovoBancoPage } from '../features/bancos/pages/NovoBancoPage';
+import { EditarBancoPage } from '../features/bancos/pages/EditarBancoPage';
+import { ClientesPage } from '../features/clientes/pages/ClientesPage';
+import { NovoClientePage } from '../features/clientes/pages/NovoClientePage';
+import { EditarClientePage } from '../features/clientes/pages/EditarClientePage';
+import { CentrosCustoPage } from '../features/centrosCusto/pages/CentrosCustoPage';
+import { NovoCentroCustoPage } from '../features/centrosCusto/pages/NovoCentroCustoPage';
+import { EditarCentroCustoPage } from '../features/centrosCusto/pages/EditarCentroCustoPage';
+import { MovimentosFixosPage } from '../features/movimentosFixos/pages/MovimentosFixosPage';
+import { NovoMovimentoFixoPage } from '../features/movimentosFixos/pages/NovoMovimentoFixoPage';
+import { EditarMovimentoFixoPage } from '../features/movimentosFixos/pages/EditarMovimentoFixoPage';
+import { UsuariosPage } from '../features/usuarios/pages/UsuariosPage';
+import { NovoUsuarioPage } from '../features/usuarios/pages/NovoUsuarioPage';
+import { EditarUsuarioPage } from '../features/usuarios/pages/EditarUsuarioPage';
export const routes: RouteObject[] = [
{
@@ -45,6 +60,66 @@ export const routes: RouteObject[] = [
path: '/relatorios',
element: ,
},
+ {
+ path: 'bancos',
+ element: ,
+ },
+ {
+ path: 'bancos/novo',
+ element: ,
+ },
+ {
+ path: 'bancos/:id/editar',
+ element: ,
+ },
+ {
+ path: 'clientes',
+ element: ,
+ },
+ {
+ path: 'clientes/novo',
+ element: ,
+ },
+ {
+ path: 'clientes/:id/editar',
+ element: ,
+ },
+ {
+ path: 'centros-custo',
+ element: ,
+ },
+ {
+ path: 'centros-custo/novo',
+ element: ,
+ },
+ {
+ path: 'centros-custo/:id/editar',
+ element: ,
+ },
+ {
+ path: 'movimentos-fixos',
+ element: ,
+ },
+ {
+ path: 'movimentos-fixos/novo',
+ element: ,
+ },
+ {
+ path: 'movimentos-fixos/:id/editar',
+ element: ,
+ },
+ {
+ path: 'usuarios',
+ element: ,
+ },
+ {
+ path: 'usuarios/novo',
+ element: ,
+ },
+ {
+ path: 'usuarios/:id/editar',
+ element: ,
+ },
],
},
],
diff --git a/financeiro-web/src/components/layout/Sidebar.tsx b/financeiro-web/src/components/layout/Sidebar.tsx
index 75a8ddb..8b26f9f 100644
--- a/financeiro-web/src/components/layout/Sidebar.tsx
+++ b/financeiro-web/src/components/layout/Sidebar.tsx
@@ -12,6 +12,10 @@ import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import AssessmentIcon from '@mui/icons-material/Assessment';
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
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 { NavLink } from 'react-router-dom';
type SidebarProps = {
@@ -29,11 +33,31 @@ const menuItems = [
path: '/movimentos',
icon: ,
},
+ {
+ label: 'Clientes',
+ path: '/clientes',
+ icon: ,
+ },
{
label: 'Carteiras',
- path: '/carteiras',
+ path: '/bancos',
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',
@@ -98,6 +122,7 @@ export function Sidebar({ onNavigate }: SidebarProps) {
}}
>
{item.icon}
+
MVP ativo
+
- Cadastro e edição de movimentos funcionando.
+ Movimentos e carteiras em operação.
diff --git a/financeiro-web/src/features/bancos/components/BancoForm.tsx b/financeiro-web/src/features/bancos/components/BancoForm.tsx
new file mode 100644
index 0000000..e65fe3d
--- /dev/null
+++ b/financeiro-web/src/features/bancos/components/BancoForm.tsx
@@ -0,0 +1,415 @@
+import { useEffect, useState } from 'react';
+import type { FormEvent, ReactNode } from 'react';
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ CircularProgress,
+ Divider,
+ MenuItem,
+ Paper,
+ Stack,
+ TextField,
+ Typography,
+} from '@mui/material';
+import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+import SaveIcon from '@mui/icons-material/Save';
+import { useNavigate } from 'react-router-dom';
+import {
+ atualizarBanco,
+ criarBanco,
+} from '../services/bancosService';
+import type {
+ Banco,
+ CriarBancoRequest,
+} from '../types/bancoTypes';
+
+type BancoFormProps = {
+ mode: 'create' | 'edit';
+ initialData?: Banco | null;
+};
+
+type FormSectionProps = {
+ title: string;
+ description?: string;
+ children: ReactNode;
+};
+
+function formatarValorResumo(value: string) {
+ const numero = Number(value || 0);
+
+ return new Intl.NumberFormat('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ }).format(numero);
+}
+
+function FormSection({ title, description, children }: FormSectionProps) {
+ return (
+
+
+
+ {title}
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+
+ {children}
+
+
+ );
+}
+
+const fieldSx = {
+ '& .MuiOutlinedInput-root': {
+ borderRadius: 2.5,
+ backgroundColor: '#FFFFFF',
+ minHeight: 48,
+ },
+ '& .MuiInputLabel-root': {
+ backgroundColor: '#FFFFFF',
+ paddingX: 0.5,
+ },
+};
+
+export function BancoForm({ mode, initialData }: BancoFormProps) {
+ const navigate = useNavigate();
+
+ const isEdit = mode === 'edit';
+
+ const [saving, setSaving] = useState(false);
+ const [erro, setErro] = useState('');
+ const [sucesso, setSucesso] = useState('');
+
+ const [descricao, setDescricao] = useState('');
+ const [saldo, setSaldo] = useState('0');
+ const [debito, setDebito] = useState('0');
+ const [habilitado, setHabilitado] = useState('1');
+
+ const titulo = isEdit ? 'Editar banco/carteira' : 'Nova carteira';
+ const subtitulo = isEdit
+ ? 'Atualize os dados da carteira selecionada.'
+ : 'Cadastre contas, carteiras, caixas físicos ou cartões.';
+
+ useEffect(() => {
+ if (!initialData) return;
+
+ setDescricao(initialData.descricao || '');
+ setSaldo(String(initialData.saldo ?? 0));
+ setDebito(String(initialData.debito ?? 0));
+ setHabilitado(String(initialData.habilitado ?? 1));
+ }, [initialData]);
+
+ function limparFormulario() {
+ setDescricao('');
+ setSaldo('0');
+ setDebito('0');
+ setHabilitado('1');
+ }
+
+ async function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+
+ if (!descricao.trim()) {
+ setErro('Informe a descrição.');
+ return;
+ }
+
+ if (saldo === '' || !Number.isFinite(Number(saldo))) {
+ setErro('Informe um saldo válido.');
+ return;
+ }
+
+ try {
+ setSaving(true);
+ setErro('');
+ setSucesso('');
+
+ const payload: CriarBancoRequest = {
+ descricao: descricao.trim(),
+ saldo: Number(saldo || 0),
+ debito: Number(debito || 0),
+ habilitado: Number(habilitado || 1),
+ };
+
+ if (isEdit) {
+ if (!initialData?.idbancos) {
+ setErro('Banco/carteira inválido para edição.');
+ return;
+ }
+
+ await atualizarBanco(initialData.idbancos, payload);
+ setSucesso('Banco/carteira atualizado com sucesso.');
+ } else {
+ await criarBanco(payload);
+ setSucesso('Banco/carteira cadastrado com sucesso.');
+ limparFormulario();
+ }
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível salvar o banco/carteira.';
+
+ setErro(message);
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ return (
+
+
+
+ }
+ label={isEdit ? 'Edição de carteira' : 'Cadastro de carteira'}
+ color="primary"
+ variant="outlined"
+ sx={{ marginBottom: 1.25, fontWeight: 700 }}
+ />
+
+
+ {titulo}
+
+
+
+ {subtitulo}
+
+
+
+ }
+ onClick={() => navigate('/bancos')}
+ sx={{
+ minHeight: 48,
+ borderRadius: 3,
+ px: 2.5,
+ }}
+ >
+ Voltar para lista
+
+
+
+ {erro && (
+
+ {erro}
+
+ )}
+
+ {sucesso && (
+
+ {sucesso}
+
+ )}
+
+
+
+
+
+
+ setDescricao(event.target.value)}
+ placeholder="Ex: Caixa, Nubank, Sicredi, Mercado Pago..."
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setDebito(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+
+
+
+ setSaldo(event.target.value)}
+ inputProps={{
+ step: '0.01',
+ }}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setHabilitado(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+
+
+
+
+
+
+
+
+ :
+ }
+ sx={{
+ minHeight: 46,
+ px: 3,
+ boxShadow: 3,
+ }}
+ >
+ {saving
+ ? 'Salvando...'
+ : isEdit
+ ? 'Atualizar carteira'
+ : 'Salvar carteira'}
+
+
+
+
+
+
+
+
+ Resumo
+
+
+
+ Prévia rápida da carteira antes de salvar.
+
+
+
+
+
+
+
+ Descrição
+
+
+ {descricao || 'Sem descrição'}
+
+
+
+
+
+ Tipo
+
+
+ {Number(debito) === 1 ? 'Débito' : 'Crédito'}
+
+
+
+
+
+ Saldo
+
+ = 0 ? 'success.main' : 'error.main'}
+ >
+ {formatarValorResumo(saldo)}
+
+
+
+
+
+ Status
+
+
+ {Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado'}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/bancos/pages/BancosPage.tsx b/financeiro-web/src/features/bancos/pages/BancosPage.tsx
new file mode 100644
index 0000000..44145c9
--- /dev/null
+++ b/financeiro-web/src/features/bancos/pages/BancosPage.tsx
@@ -0,0 +1,742 @@
+import { useEffect, useMemo, useState } from 'react';
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ CircularProgress,
+ Divider,
+ IconButton,
+ MenuItem,
+ Pagination,
+ Paper,
+ Stack,
+ Switch,
+ TextField,
+ Tooltip,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ useMediaQuery,
+ useTheme,
+} from '@mui/material';
+import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
+import AddIcon from '@mui/icons-material/Add';
+import DeleteIcon from '@mui/icons-material/Delete';
+import EditIcon from '@mui/icons-material/Edit';
+import FilterAltIcon from '@mui/icons-material/FilterAlt';
+import RestartAltIcon from '@mui/icons-material/RestartAlt';
+import SearchIcon from '@mui/icons-material/Search';
+import { Link as RouterLink } from 'react-router-dom';
+import {
+ alterarHabilitadoBanco,
+ deletarBanco,
+ listarBancos,
+ type ListarBancosParams,
+} from '../services/bancosService';
+import type {
+ Banco,
+ BancosPagination,
+ BancosSummary,
+} from '../types/bancoTypes';
+
+const LIMITE_PADRAO = 20;
+
+function formatarValor(valor: number) {
+ return new Intl.NumberFormat('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ }).format(Number(valor || 0));
+}
+
+function formatarData(data: string | null) {
+ if (!data) return '-';
+
+ return new Date(`${String(data).slice(0, 10)}T00:00:00`).toLocaleDateString('pt-BR');
+}
+
+function tipoBancoLabel(debito: number) {
+ return Number(debito) === 1 ? 'Débito' : 'Crédito';
+}
+
+function tipoBancoColor(debito: number) {
+ return Number(debito) === 1 ? 'success' : 'info';
+}
+
+function statusBancoLabel(habilitado: number) {
+ return Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado';
+}
+
+function statusBancoColor(habilitado: number) {
+ return Number(habilitado) === 1 ? 'success' : 'default';
+}
+
+export function BancosPage() {
+ const [bancos, setBancos] = useState([]);
+ const [pagination, setPagination] = useState({
+ total: 0,
+ limite: LIMITE_PADRAO,
+ offset: 0,
+ page: 1,
+ totalPages: 1,
+ });
+ const [summary, setSummary] = useState({
+ quantidade: 0,
+ saldoTotal: 0,
+ saldoCarteiras: 0,
+ saldoDebito: 0,
+ });
+
+ const [alterandoStatusId, setAlterandoStatusId] = useState(null);
+
+ const [loading, setLoading] = useState(true);
+ const [erro, setErro] = useState('');
+ const [sucesso, setSucesso] = useState('');
+
+ const [page, setPage] = useState(1);
+ const [busca, setBusca] = useState('');
+ const [debito, setDebito] = useState('');
+ const [habilitado, setHabilitado] = useState('');
+ const [orderBy, setOrderBy] = useState('descricao');
+ const [orderDirection, setOrderDirection] = useState<'ASC' | 'DESC'>('ASC');
+
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
+
+ const filtrosAtivos = useMemo(() => {
+ let count = 0;
+
+ if (busca.trim()) count += 1;
+ if (debito !== '') count += 1;
+ if (habilitado !== '') count += 1;
+
+ return count;
+ }, [busca, debito, habilitado]);
+
+ async function carregarBancos(pageToLoad = page) {
+ try {
+ setLoading(true);
+ setErro('');
+
+ const params: ListarBancosParams = {
+ limite: LIMITE_PADRAO,
+ page: pageToLoad,
+ busca: busca.trim() || undefined,
+ debito,
+ habilitado,
+ orderBy,
+ orderDirection,
+ };
+
+ const response = await listarBancos(params);
+
+ setBancos(response.data);
+ setPagination(response.pagination);
+ setSummary(response.summary);
+ setPage(response.pagination.page);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar os bancos/carteiras.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ function aplicarFiltros() {
+ setPage(1);
+ carregarBancos(1);
+ }
+
+ function limparFiltros() {
+ setBusca('');
+ setDebito('');
+ setHabilitado('');
+ setOrderBy('descricao');
+ setOrderDirection('ASC');
+
+ setTimeout(() => {
+ setPage(1);
+ carregarBancos(1);
+ }, 0);
+ }
+
+ async function handleDeletarBanco(item: Banco) {
+ const confirmou = window.confirm(
+ `Deseja realmente excluir "${item.descricao}"?`
+ );
+
+ if (!confirmou) return;
+
+ try {
+ setLoading(true);
+ setErro('');
+ setSucesso('');
+
+ await deletarBanco(item.idbancos);
+
+ setSucesso('Banco/carteira excluído com sucesso.');
+ await carregarBancos(page);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível excluir o banco/carteira.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function handleAlterarStatusBanco(item: Banco) {
+ const novoStatus = Number(item.habilitado) === 1 ? 0 : 1;
+
+ try {
+ setAlterandoStatusId(item.idbancos);
+ setErro('');
+ setSucesso('');
+
+ const bancoAtualizado = await alterarHabilitadoBanco(item.idbancos, novoStatus);
+
+ setBancos((listaAtual) =>
+ listaAtual.map((banco) =>
+ banco.idbancos === item.idbancos ? bancoAtualizado : banco
+ )
+ );
+
+ setSucesso(
+ novoStatus === 1
+ ? 'Banco/carteira habilitado com sucesso.'
+ : 'Banco/carteira desabilitado com sucesso.'
+ );
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível alterar o status do banco/carteira.';
+
+ setErro(message);
+ } finally {
+ setAlterandoStatusId(null);
+ }
+ }
+
+ useEffect(() => {
+ carregarBancos(1);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+
+ }
+ label="Carteiras financeiras"
+ color="primary"
+ variant="outlined"
+ sx={{ marginBottom: 1.25, fontWeight: 700 }}
+ />
+
+
+ Bancos e carteiras
+
+
+
+ Cadastre, edite e acompanhe as carteiras usadas nos movimentos.
+
+
+
+ }
+ sx={{
+ minHeight: 48,
+ borderRadius: 3,
+ px: 2.5,
+ boxShadow: 3,
+ }}
+ >
+ Nova carteira
+
+
+
+ {erro && (
+
+ {erro}
+
+ )}
+
+ {sucesso && (
+
+ {sucesso}
+
+ )}
+
+
+
+
+
+ Carteiras cadastradas
+
+
+ {summary.quantidade}
+
+
+
+
+
+
+
+ Saldo total
+
+ = 0 ? 'success.main' : 'error.main'}
+ >
+ {formatarValor(summary.saldoTotal)}
+
+
+
+
+
+
+
+ Carteiras de crédito
+
+
+ {formatarValor(summary.saldoCarteiras)}
+
+
+
+
+
+
+
+ Carteiras de débito
+
+
+ {formatarValor(summary.saldoDebito)}
+
+
+
+
+
+
+
+
+
+
+
+ Filtros
+
+
+
+
+
+
+ setBusca(event.target.value)}
+ placeholder="Descrição da carteira..."
+ fullWidth
+ />
+
+
+ setDebito(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ fullWidth
+ >
+
+
+
+
+
+
+ setHabilitado(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ fullWidth
+ >
+
+
+
+
+
+ setOrderBy(event.target.value)}
+ fullWidth
+ >
+
+
+
+
+
+
+
+
+ setOrderDirection(event.target.value as 'ASC' | 'DESC')
+ }
+ fullWidth
+ >
+
+
+
+
+
+
+ }
+ onClick={limparFiltros}
+ >
+ Limpar
+
+
+ }
+ onClick={aplicarFiltros}
+ disabled={loading}
+ >
+ Aplicar filtros
+
+
+
+
+
+
+
+ {loading ? (
+
+
+ Carregando bancos/carteiras...
+
+ ) : bancos.length === 0 ? (
+
+
+ Nenhum banco/carteira encontrado.
+
+
+ Ajuste os filtros ou cadastre uma nova carteira.
+
+
+ ) : isMobile ? (
+ }>
+ {bancos.map((item) => (
+
+
+
+
+
+ {item.descricao}
+
+
+
+ Cadastro: {formatarData(item.insert_date)}
+
+
+
+ = 0 ? 'success.main' : 'error.main'}
+ whiteSpace="nowrap"
+ >
+ {formatarValor(item.saldo)}
+
+
+
+
+
+
+
+
+
+
+
+
+ handleAlterarStatusBanco(item)}
+ />
+
+
+
+ }
+ >
+ Editar
+
+
+ }
+ onClick={() => handleDeletarBanco(item)}
+ >
+ Excluir
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
+
+ Descrição
+ Tipo
+ Saldo
+ Cadastro
+ Atualização
+ Ações
+
+
+
+
+ {bancos.map((item) => (
+
+
+
+
+ {item.descricao}
+
+
+
+
+
+
+
+
+
+ = 0 ? 'success.main' : 'error.main'}
+ >
+ {formatarValor(item.saldo)}
+
+
+
+
+ {formatarData(item.insert_date)}
+
+
+
+ {formatarData(item.update_date)}
+
+
+
+
+
+
+ handleAlterarStatusBanco(item)}
+ />
+
+
+
+
+
+
+
+
+
+
+ handleDeletarBanco(item)}
+ >
+
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+ {pagination.totalPages > 1 && (
+
+ {
+ setPage(novaPagina);
+ carregarBancos(novaPagina);
+ }}
+ />
+
+ )}
+
+
+
+ Exibindo página {pagination.page} de {pagination.totalPages}, total de{' '}
+ {pagination.total} registro(s).
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/bancos/pages/EditarBancoPage.tsx b/financeiro-web/src/features/bancos/pages/EditarBancoPage.tsx
new file mode 100644
index 0000000..4e523df
--- /dev/null
+++ b/financeiro-web/src/features/bancos/pages/EditarBancoPage.tsx
@@ -0,0 +1,62 @@
+import { useEffect, useState } from 'react';
+import { Alert, Box, CircularProgress, Typography } from '@mui/material';
+import { useParams } from 'react-router-dom';
+import { BancoForm } from '../components/BancoForm';
+import { buscarBancoPorId } from '../services/bancosService';
+import type { Banco } from '../types/bancoTypes';
+
+export function EditarBancoPage() {
+ const { id } = useParams();
+
+ const [banco, setBanco] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [erro, setErro] = useState('');
+
+ useEffect(() => {
+ async function carregarBanco() {
+ try {
+ setLoading(true);
+ setErro('');
+
+ const bancoId = Number(id);
+
+ if (!bancoId) {
+ setErro('ID do banco/carteira inválido.');
+ return;
+ }
+
+ const data = await buscarBancoPorId(bancoId);
+ setBanco(data);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar o banco/carteira.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ carregarBanco();
+ }, [id]);
+
+ if (loading) {
+ return (
+
+
+ Carregando banco/carteira...
+
+ );
+ }
+
+ if (erro) {
+ return (
+
+ {erro}
+
+ );
+ }
+
+ return ;
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/bancos/pages/NovoBancoPage.tsx b/financeiro-web/src/features/bancos/pages/NovoBancoPage.tsx
new file mode 100644
index 0000000..6a0bd50
--- /dev/null
+++ b/financeiro-web/src/features/bancos/pages/NovoBancoPage.tsx
@@ -0,0 +1,5 @@
+import { BancoForm } from '../components/BancoForm';
+
+export function NovoBancoPage() {
+ return ;
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/bancos/services/bancosService.ts b/financeiro-web/src/features/bancos/services/bancosService.ts
new file mode 100644
index 0000000..3ad3975
--- /dev/null
+++ b/financeiro-web/src/features/bancos/services/bancosService.ts
@@ -0,0 +1,66 @@
+import { api } from '../../../services/api';
+import type {
+ ApiResponse,
+ AtualizarBancoRequest,
+ Banco,
+ BancosListResponse,
+ CriarBancoRequest,
+} from '../types/bancoTypes';
+
+export type OrderDirection = 'ASC' | 'DESC';
+
+export type ListarBancosParams = {
+ limite?: number;
+ offset?: number;
+ page?: number;
+ busca?: string;
+ debito?: number | '';
+ habilitado?: number | '';
+ orderBy?: string;
+ orderDirection?: OrderDirection;
+};
+
+export async function listarBancos(
+ params?: ListarBancosParams
+): Promise {
+ const response = await api.get('/bancos', {
+ params,
+ });
+
+ return response.data;
+}
+
+export async function buscarBancoPorId(id: number): Promise {
+ const response = await api.get>(`/bancos/${id}`);
+ return response.data.data;
+}
+
+export async function criarBanco(
+ data: CriarBancoRequest
+): Promise {
+ const response = await api.post>('/bancos', data);
+ return response.data.data;
+}
+
+export async function atualizarBanco(
+ id: number,
+ data: AtualizarBancoRequest
+): Promise {
+ const response = await api.put>(`/bancos/${id}`, data);
+ return response.data.data;
+}
+
+export async function alterarHabilitadoBanco(
+ id: number,
+ habilitado: number
+): Promise {
+ const response = await api.patch>(`/bancos/${id}/habilitado`, {
+ habilitado,
+ });
+
+ return response.data.data;
+}
+
+export async function deletarBanco(id: number): Promise {
+ await api.delete(`/bancos/${id}`);
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/bancos/types/bancoTypes.ts b/financeiro-web/src/features/bancos/types/bancoTypes.ts
new file mode 100644
index 0000000..500e3cb
--- /dev/null
+++ b/financeiro-web/src/features/bancos/types/bancoTypes.ts
@@ -0,0 +1,46 @@
+export type Banco = {
+ idbancos: number;
+ descricao: string;
+ saldo: number;
+ debito: number;
+ habilitado: number;
+ insert_date: string | null;
+ update_date: string | null;
+};
+
+export type CriarBancoRequest = {
+ descricao: string;
+ saldo: number;
+ debito: number;
+ habilitado: number;
+};
+
+export type AtualizarBancoRequest = CriarBancoRequest;
+
+export type BancosPagination = {
+ total: number;
+ limite: number;
+ offset: number;
+ page: number;
+ totalPages: number;
+};
+
+export type BancosSummary = {
+ quantidade: number;
+ saldoTotal: number;
+ saldoCarteiras: number;
+ saldoDebito: number;
+};
+
+export type ApiResponse = {
+ ok: boolean;
+ message?: string;
+ data: T;
+};
+
+export type BancosListResponse = {
+ ok: boolean;
+ data: Banco[];
+ pagination: BancosPagination;
+ summary: BancosSummary;
+};
\ 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
new file mode 100644
index 0000000..c5786dd
--- /dev/null
+++ b/financeiro-web/src/features/centrosCusto/components/CentroCustoForm.tsx
@@ -0,0 +1,439 @@
+import { useEffect, useState } from 'react';
+import type { FormEvent, ReactNode } from 'react';
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ CircularProgress,
+ Divider,
+ MenuItem,
+ Paper,
+ Stack,
+ 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 { useNavigate } from 'react-router-dom';
+import {
+ atualizarCentroCusto,
+ criarCentroCusto,
+} from '../services/centrosCustoService';
+import type {
+ CentroCusto,
+ CriarCentroCustoRequest,
+} from '../types/centroCustoTypes';
+
+type CentroCustoFormProps = {
+ mode: 'create' | 'edit';
+ initialData?: CentroCusto | null;
+};
+
+type FormSectionProps = {
+ title: string;
+ description?: string;
+ children: ReactNode;
+};
+
+function formatarValorResumo(value: string) {
+ const numero = Number(value || 0);
+
+ return new Intl.NumberFormat('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ }).format(numero);
+}
+
+function FormSection({ title, description, children }: FormSectionProps) {
+ return (
+
+
+
+ {title}
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+
+ {children}
+
+
+ );
+}
+
+const fieldSx = {
+ '& .MuiOutlinedInput-root': {
+ borderRadius: 2.5,
+ backgroundColor: '#FFFFFF',
+ minHeight: 48,
+ },
+ '& .MuiInputLabel-root': {
+ backgroundColor: '#FFFFFF',
+ paddingX: 0.5,
+ },
+};
+
+export function CentroCustoForm({ mode, initialData }: CentroCustoFormProps) {
+ const navigate = useNavigate();
+
+ const isEdit = mode === 'edit';
+
+ const [saving, setSaving] = useState(false);
+ const [erro, setErro] = useState('');
+ 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 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.';
+
+ 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));
+ }, [initialData]);
+
+ function limparFormulario() {
+ setDescricao('');
+ setLimite('0');
+ setSimular('0');
+ setInvestimento('0');
+ setHabilitado('1');
+ }
+
+ async function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+
+ if (!descricao.trim()) {
+ setErro('Informe a descrição.');
+ return;
+ }
+
+ if (limite === '' || !Number.isFinite(Number(limite))) {
+ setErro('Informe um limite válido.');
+ return;
+ }
+
+ try {
+ setSaving(true);
+ setErro('');
+ setSucesso('');
+
+ const payload: CriarCentroCustoRequest = {
+ descricao: descricao.trim(),
+ limite: Number(limite || 0),
+ simular: Number(simular || 0),
+ investimento: Number(investimento || 0),
+ habilitado: Number(habilitado || 1),
+ };
+
+ if (isEdit) {
+ if (!initialData?.idcentrodecustos) {
+ setErro('Centro de custo inválido para edição.');
+ return;
+ }
+
+ await atualizarCentroCusto(initialData.idcentrodecustos, payload);
+ setSucesso('Centro de custo atualizado com sucesso.');
+ } else {
+ await criarCentroCusto(payload);
+ setSucesso('Centro de custo cadastrado com sucesso.');
+ limparFormulario();
+ }
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível salvar o centro de custo.';
+
+ setErro(message);
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ return (
+
+
+
+ }
+ label={isEdit ? 'Edição de centro' : 'Cadastro de centro'}
+ color="primary"
+ variant="outlined"
+ sx={{ marginBottom: 1.25, fontWeight: 700 }}
+ />
+
+
+ {titulo}
+
+
+
+ {subtitulo}
+
+
+
+ }
+ onClick={() => navigate('/centros-custo')}
+ sx={{
+ minHeight: 48,
+ borderRadius: 3,
+ px: 2.5,
+ }}
+ >
+ Voltar para lista
+
+
+
+ {erro && (
+
+ {erro}
+
+ )}
+
+ {sucesso && (
+
+ {sucesso}
+
+ )}
+
+
+
+
+
+
+ setDescricao(event.target.value)}
+ placeholder="Ex: Administrativo, Comercial, Impostos..."
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setLimite(event.target.value)}
+ inputProps={{
+ step: '0.01',
+ }}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setSimular(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+
+
+
+ setInvestimento(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+
+
+
+ setHabilitado(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+
+
+
+
+
+
+
+
+ :
+ }
+ sx={{
+ minHeight: 46,
+ px: 3,
+ boxShadow: 3,
+ }}
+ >
+ {saving
+ ? 'Salvando...'
+ : isEdit
+ ? 'Atualizar centro'
+ : 'Salvar centro'}
+
+
+
+
+
+
+
+
+ Resumo
+
+
+
+ Prévia rápida do centro antes de salvar.
+
+
+
+
+
+
+
+ Descrição
+
+
+ {descricao || 'Sem descrição'}
+
+
+
+
+
+ Limite
+
+
+ {formatarValorResumo(limite)}
+
+
+
+
+
+ Simular
+
+
+ {Number(simular) === 1 ? 'Sim' : 'Não'}
+
+
+
+
+
+ Investimento
+
+
+ {Number(investimento) === 1 ? 'Sim' : 'Não'}
+
+
+
+
+
+ Status
+
+
+ {Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado'}
+
+
+
+
+
+
+ );
+}
\ 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
new file mode 100644
index 0000000..7385c4d
--- /dev/null
+++ b/financeiro-web/src/features/centrosCusto/pages/CentrosCustoPage.tsx
@@ -0,0 +1,783 @@
+import { useEffect, useMemo, useState } from 'react';
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ CircularProgress,
+ Divider,
+ IconButton,
+ MenuItem,
+ Pagination,
+ Paper,
+ Stack,
+ Switch,
+ TextField,
+ Tooltip,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ useMediaQuery,
+ useTheme,
+} from '@mui/material';
+import AddIcon from '@mui/icons-material/Add';
+import AccountTreeIcon from '@mui/icons-material/AccountTree';
+import DeleteIcon from '@mui/icons-material/Delete';
+import EditIcon from '@mui/icons-material/Edit';
+import FilterAltIcon from '@mui/icons-material/FilterAlt';
+import RestartAltIcon from '@mui/icons-material/RestartAlt';
+import SearchIcon from '@mui/icons-material/Search';
+import { Link as RouterLink } from 'react-router-dom';
+import {
+ alterarHabilitadoCentroCusto,
+ deletarCentroCusto,
+ listarCentrosCusto,
+ type ListarCentrosCustoParams,
+} from '../services/centrosCustoService';
+import type {
+ CentroCusto,
+ CentrosCustoPagination,
+ CentrosCustoSummary,
+} from '../types/centroCustoTypes';
+
+const LIMITE_PADRAO = 20;
+
+function formatarValor(valor: number) {
+ return new Intl.NumberFormat('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ }).format(Number(valor || 0));
+}
+
+function formatarData(data: string | null) {
+ if (!data) return '-';
+
+ return new Date(`${String(data).slice(0, 10)}T00:00:00`).toLocaleDateString('pt-BR');
+}
+
+function flagLabel(valor: number, positivo: string, negativo: string) {
+ return Number(valor) === 1 ? positivo : negativo;
+}
+
+function flagColor(valor: number) {
+ return Number(valor) === 1 ? 'success' : 'default';
+}
+
+function statusLabel(habilitado: number) {
+ return Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado';
+}
+
+export function CentrosCustoPage() {
+ const [centros, setCentros] = useState([]);
+ const [pagination, setPagination] = useState({
+ total: 0,
+ limite: LIMITE_PADRAO,
+ offset: 0,
+ page: 1,
+ totalPages: 1,
+ });
+ const [summary, setSummary] = useState({
+ quantidade: 0,
+ limiteTotal: 0,
+ habilitados: 0,
+ desabilitados: 0,
+ investimentos: 0,
+ simulaveis: 0,
+ });
+
+ const [alterandoStatusId, setAlterandoStatusId] = useState(null);
+
+ const [loading, setLoading] = useState(true);
+ const [erro, setErro] = useState('');
+ const [sucesso, setSucesso] = useState('');
+
+ const [page, setPage] = useState(1);
+ const [busca, setBusca] = useState('');
+ const [simular, setSimular] = useState('');
+ const [investimento, setInvestimento] = useState('');
+ const [habilitado, setHabilitado] = useState('');
+ const [orderBy, setOrderBy] = useState('descricao');
+ const [orderDirection, setOrderDirection] = useState<'ASC' | 'DESC'>('ASC');
+
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
+
+ const filtrosAtivos = useMemo(() => {
+ let count = 0;
+
+ if (busca.trim()) count += 1;
+ if (simular !== '') count += 1;
+ if (investimento !== '') count += 1;
+ if (habilitado !== '') count += 1;
+
+ return count;
+ }, [busca, simular, investimento, habilitado]);
+
+ async function carregarCentrosCusto(pageToLoad = page) {
+ try {
+ setLoading(true);
+ setErro('');
+
+ const params: ListarCentrosCustoParams = {
+ limite: LIMITE_PADRAO,
+ page: pageToLoad,
+ busca: busca.trim() || undefined,
+ simular,
+ investimento,
+ habilitado,
+ orderBy,
+ orderDirection,
+ };
+
+ const response = await listarCentrosCusto(params);
+
+ setCentros(response.data);
+ setPagination(response.pagination);
+ setSummary(response.summary);
+ setPage(response.pagination.page);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar os centros de custo.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ function aplicarFiltros() {
+ setPage(1);
+ carregarCentrosCusto(1);
+ }
+
+ function limparFiltros() {
+ setBusca('');
+ setSimular('');
+ setInvestimento('');
+ setHabilitado('');
+ setOrderBy('descricao');
+ setOrderDirection('ASC');
+
+ setTimeout(() => {
+ setPage(1);
+ carregarCentrosCusto(1);
+ }, 0);
+ }
+
+ async function handleDeletarCentroCusto(item: CentroCusto) {
+ const confirmou = window.confirm(
+ `Deseja realmente excluir "${item.descricao}"?`
+ );
+
+ if (!confirmou) return;
+
+ try {
+ setLoading(true);
+ setErro('');
+ setSucesso('');
+
+ await deletarCentroCusto(item.idcentrodecustos);
+
+ setSucesso('Centro de custo excluído com sucesso.');
+ await carregarCentrosCusto(page);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível excluir o centro de custo.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function handleAlterarStatusCentroCusto(item: CentroCusto) {
+ const novoStatus = Number(item.habilitado) === 1 ? 0 : 1;
+
+ try {
+ setAlterandoStatusId(item.idcentrodecustos);
+ setErro('');
+ setSucesso('');
+
+ const centroAtualizado = 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.'
+ );
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível alterar o status do centro de custo.';
+
+ setErro(message);
+ } finally {
+ setAlterandoStatusId(null);
+ }
+ }
+
+ useEffect(() => {
+ carregarCentrosCusto(1);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+
+ }
+ label="Classificação financeira"
+ color="primary"
+ variant="outlined"
+ sx={{ marginBottom: 1.25, fontWeight: 700 }}
+ />
+
+
+ Centros de custo
+
+
+
+ Cadastre e organize categorias financeiras para os movimentos.
+
+
+
+ }
+ sx={{
+ minHeight: 48,
+ borderRadius: 3,
+ px: 2.5,
+ boxShadow: 3,
+ }}
+ >
+ Novo centro
+
+
+
+ {erro && (
+
+ {erro}
+
+ )}
+
+ {sucesso && (
+
+ {sucesso}
+
+ )}
+
+
+
+
+
+ Centros encontrados
+
+
+ {summary.quantidade}
+
+
+
+
+
+
+
+ Limite total
+
+
+ {formatarValor(summary.limiteTotal)}
+
+
+
+
+
+
+
+ Investimentos
+
+
+ {summary.investimentos}
+
+
+
+
+
+
+
+ Simuláveis
+
+
+ {summary.simulaveis}
+
+
+
+
+
+
+
+
+
+
+
+ Filtros
+
+
+
+
+
+
+ setBusca(event.target.value)}
+ placeholder="Descrição do centro..."
+ fullWidth
+ />
+
+
+ setSimular(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ fullWidth
+ >
+
+
+
+
+
+
+ setInvestimento(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ fullWidth
+ >
+
+
+
+
+
+
+ setHabilitado(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ fullWidth
+ >
+
+
+
+
+
+ setOrderBy(event.target.value)}
+ fullWidth
+ >
+
+
+
+
+
+
+
+
+
+
+ setOrderDirection(event.target.value as 'ASC' | 'DESC')
+ }
+ fullWidth
+ >
+
+
+
+
+
+
+ }
+ onClick={limparFiltros}
+ >
+ Limpar
+
+
+ }
+ onClick={aplicarFiltros}
+ disabled={loading}
+ >
+ Aplicar filtros
+
+
+
+
+
+
+
+ {loading ? (
+
+
+ Carregando centros de custo...
+
+ ) : centros.length === 0 ? (
+
+
+ Nenhum centro de custo encontrado.
+
+
+ Ajuste os filtros ou cadastre um novo centro de custo.
+
+
+ ) : isMobile ? (
+ }>
+ {centros.map((item) => (
+
+
+
+
+
+ {item.descricao}
+
+
+
+ Cadastro: {formatarData(item.insert_date)}
+
+
+
+
+ {formatarValor(item.limite)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ handleAlterarStatusCentroCusto(item)}
+ />
+
+
+
+ }
+ >
+ Editar
+
+
+ }
+ onClick={() => handleDeletarCentroCusto(item)}
+ >
+ Excluir
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
+
+ Descrição
+ Limite
+ Simular
+ Investimento
+ Status
+ Cadastro
+ Atualização
+ Ações
+
+
+
+
+ {centros.map((item) => (
+
+
+
+
+ {item.descricao}
+
+
+
+
+
+
+ {formatarValor(item.limite)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatarData(item.insert_date)}
+
+ {formatarData(item.update_date)}
+
+
+
+
+
+ handleAlterarStatusCentroCusto(item)}
+ />
+
+
+
+
+
+
+
+
+
+
+ handleDeletarCentroCusto(item)}
+ >
+
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+ {pagination.totalPages > 1 && (
+
+ {
+ setPage(novaPagina);
+ carregarCentrosCusto(novaPagina);
+ }}
+ />
+
+ )}
+
+
+
+ Exibindo página {pagination.page} de {pagination.totalPages}, total de{' '}
+ {pagination.total} registro(s).
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/centrosCusto/pages/EditarCentroCustoPage.tsx b/financeiro-web/src/features/centrosCusto/pages/EditarCentroCustoPage.tsx
new file mode 100644
index 0000000..e8e856a
--- /dev/null
+++ b/financeiro-web/src/features/centrosCusto/pages/EditarCentroCustoPage.tsx
@@ -0,0 +1,62 @@
+import { useEffect, useState } from 'react';
+import { Alert, Box, CircularProgress, Typography } from '@mui/material';
+import { useParams } from 'react-router-dom';
+import { CentroCustoForm } from '../components/CentroCustoForm';
+import { buscarCentroCustoPorId } from '../services/centrosCustoService';
+import type { CentroCusto } from '../types/centroCustoTypes';
+
+export function EditarCentroCustoPage() {
+ const { id } = useParams();
+
+ const [centroCusto, setCentroCusto] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [erro, setErro] = useState('');
+
+ useEffect(() => {
+ async function carregarCentroCusto() {
+ try {
+ setLoading(true);
+ setErro('');
+
+ const centroCustoId = Number(id);
+
+ if (!centroCustoId) {
+ setErro('ID do centro de custo inválido.');
+ return;
+ }
+
+ const data = await buscarCentroCustoPorId(centroCustoId);
+ setCentroCusto(data);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar o centro de custo.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ carregarCentroCusto();
+ }, [id]);
+
+ if (loading) {
+ return (
+
+
+ Carregando centro de custo...
+
+ );
+ }
+
+ if (erro) {
+ return (
+
+ {erro}
+
+ );
+ }
+
+ return ;
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/centrosCusto/pages/NovoCentroCustoPage.tsx b/financeiro-web/src/features/centrosCusto/pages/NovoCentroCustoPage.tsx
new file mode 100644
index 0000000..de4c91d
--- /dev/null
+++ b/financeiro-web/src/features/centrosCusto/pages/NovoCentroCustoPage.tsx
@@ -0,0 +1,5 @@
+import { CentroCustoForm } from '../components/CentroCustoForm';
+
+export function NovoCentroCustoPage() {
+ return ;
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/centrosCusto/services/centrosCustoService.ts b/financeiro-web/src/features/centrosCusto/services/centrosCustoService.ts
new file mode 100644
index 0000000..1643272
--- /dev/null
+++ b/financeiro-web/src/features/centrosCusto/services/centrosCustoService.ts
@@ -0,0 +1,68 @@
+import { api } from '../../../services/api';
+import type {
+ ApiResponse,
+ AtualizarCentroCustoRequest,
+ CentroCusto,
+ CentrosCustoListResponse,
+ CriarCentroCustoRequest,
+} from '../types/centroCustoTypes';
+
+export type OrderDirection = 'ASC' | 'DESC';
+
+export type ListarCentrosCustoParams = {
+ limite?: number;
+ offset?: number;
+ page?: number;
+ busca?: string;
+ simular?: number | '';
+ investimento?: number | '';
+ habilitado?: number | '';
+ orderBy?: string;
+ orderDirection?: OrderDirection;
+};
+
+export async function listarCentrosCusto(
+ params?: ListarCentrosCustoParams
+): Promise {
+ 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}`);
+ return response.data.data;
+}
+
+export async function criarCentroCusto(
+ data: CriarCentroCustoRequest
+): Promise {
+ const response = await api.post>('/centros-custo', data);
+ return response.data.data;
+}
+
+export async function atualizarCentroCusto(
+ id: number,
+ data: AtualizarCentroCustoRequest
+): Promise {
+ const response = await api.put>(`/centros-custo/${id}`, data);
+ return response.data.data;
+}
+
+export async function alterarHabilitadoCentroCusto(
+ id: number,
+ habilitado: number
+): Promise {
+ const response = await api.patch>(
+ `/centros-custo/${id}/habilitado`,
+ { habilitado }
+ );
+
+ return response.data.data;
+}
+
+export async function deletarCentroCusto(id: number): Promise {
+ await api.delete(`/centros-custo/${id}`);
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/centrosCusto/types/centroCustoTypes.ts b/financeiro-web/src/features/centrosCusto/types/centroCustoTypes.ts
new file mode 100644
index 0000000..e7df198
--- /dev/null
+++ b/financeiro-web/src/features/centrosCusto/types/centroCustoTypes.ts
@@ -0,0 +1,50 @@
+export type CentroCusto = {
+ idcentrodecustos: number;
+ descricao: string;
+ limite: number;
+ simular: number;
+ investimento: number;
+ habilitado: number;
+ insert_date: string | null;
+ update_date: string | null;
+};
+
+export type CriarCentroCustoRequest = {
+ descricao: string;
+ limite: number;
+ simular: number;
+ investimento: number;
+ habilitado: number;
+};
+
+export type AtualizarCentroCustoRequest = CriarCentroCustoRequest;
+
+export type CentrosCustoPagination = {
+ total: number;
+ limite: number;
+ offset: number;
+ page: number;
+ totalPages: number;
+};
+
+export type CentrosCustoSummary = {
+ quantidade: number;
+ limiteTotal: number;
+ habilitados: number;
+ desabilitados: number;
+ investimentos: number;
+ simulaveis: number;
+};
+
+export type ApiResponse = {
+ ok: boolean;
+ message?: string;
+ data: T;
+};
+
+export type CentrosCustoListResponse = {
+ ok: boolean;
+ data: CentroCusto[];
+ pagination: CentrosCustoPagination;
+ summary: CentrosCustoSummary;
+};
\ No newline at end of file
diff --git a/financeiro-web/src/features/clientes/components/ClienteForm.tsx b/financeiro-web/src/features/clientes/components/ClienteForm.tsx
new file mode 100644
index 0000000..d9c842b
--- /dev/null
+++ b/financeiro-web/src/features/clientes/components/ClienteForm.tsx
@@ -0,0 +1,562 @@
+import { useEffect, useState } from 'react';
+import type { FormEvent, ReactNode } from 'react';
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ CircularProgress,
+ Divider,
+ MenuItem,
+ Paper,
+ Stack,
+ TextField,
+ Typography,
+} from '@mui/material';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+import PersonIcon from '@mui/icons-material/Person';
+import SaveIcon from '@mui/icons-material/Save';
+import { useNavigate } from 'react-router-dom';
+import {
+ atualizarCliente,
+ criarCliente,
+} from '../services/clientesService';
+import type {
+ Cliente,
+ CriarClienteRequest,
+} from '../types/clienteTypes';
+
+type ClienteFormProps = {
+ mode: 'create' | 'edit';
+ initialData?: Cliente | null;
+};
+
+type FormSectionProps = {
+ title: string;
+ description?: string;
+ children: ReactNode;
+};
+
+function FormSection({ title, description, children }: FormSectionProps) {
+ return (
+
+
+
+ {title}
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+
+ {children}
+
+
+ );
+}
+
+const fieldSx = {
+ '& .MuiOutlinedInput-root': {
+ borderRadius: 2.5,
+ backgroundColor: '#FFFFFF',
+ minHeight: 48,
+ },
+ '& .MuiInputLabel-root': {
+ backgroundColor: '#FFFFFF',
+ paddingX: 0.5,
+ },
+};
+
+function textoOuNull(valor: string): string | null {
+ const texto = valor.trim();
+ return texto || null;
+}
+
+function normalizarDocumento(valor: string) {
+ return valor.replace(/\D/g, '');
+}
+
+export function ClienteForm({ mode, initialData }: ClienteFormProps) {
+ const navigate = useNavigate();
+
+ const isEdit = mode === 'edit';
+
+ const [saving, setSaving] = useState(false);
+ const [erro, setErro] = useState('');
+ const [sucesso, setSucesso] = useState('');
+
+ const [cpfCnpj, setCpfCnpj] = useState('');
+ const [nome, setNome] = useState('');
+ const [rg, setRg] = useState('');
+ const [celular, setCelular] = useState('');
+ const [email, setEmail] = useState('');
+ const [cep, setCep] = useState('');
+ const [logradouro, setLogradouro] = useState('');
+ const [numero, setNumero] = useState('');
+ const [bairro, setBairro] = useState('');
+ const [cidade, setCidade] = useState('');
+ const [estado, setEstado] = useState('');
+ const [sexo, setSexo] = useState('');
+ const [pessoafisica, setPessoafisica] = useState('F');
+ const [habilitado, setHabilitado] = useState('1');
+
+ const titulo = isEdit ? 'Editar cliente' : 'Novo cliente';
+ const subtitulo = isEdit
+ ? 'Atualize os dados cadastrais do cliente selecionado.'
+ : 'Cadastre clientes para vincular aos movimentos financeiros.';
+
+ useEffect(() => {
+ if (!initialData) return;
+
+ setCpfCnpj(initialData.cpf_cnpj || '');
+ setNome(initialData.nome || '');
+ setRg(initialData.rg || '');
+ setCelular(initialData.celular || '');
+ setEmail(initialData.email || '');
+ setCep(initialData.cep || '');
+ setLogradouro(initialData.logradouro || '');
+ setNumero(initialData.numero || '');
+ setBairro(initialData.bairro || '');
+ setCidade(initialData.cidade || '');
+ setEstado(initialData.estado || '');
+ setSexo(initialData.sexo || '');
+ setPessoafisica(initialData.pessoafisica || 'F');
+ setHabilitado(String(initialData.habilitado ?? 1));
+ }, [initialData]);
+
+ function limparFormulario() {
+ setCpfCnpj('');
+ setNome('');
+ setRg('');
+ setCelular('');
+ setEmail('');
+ setCep('');
+ setLogradouro('');
+ setNumero('');
+ setBairro('');
+ setCidade('');
+ setEstado('');
+ setSexo('');
+ setPessoafisica('F');
+ setHabilitado('1');
+ }
+
+ async function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+
+ if (!cpfCnpj.trim()) {
+ setErro('Informe o CPF/CNPJ.');
+ return;
+ }
+
+ if (!nome.trim()) {
+ setErro('Informe o nome.');
+ return;
+ }
+
+ if (email.trim() && !email.includes('@')) {
+ setErro('Informe um e-mail válido.');
+ return;
+ }
+
+ try {
+ setSaving(true);
+ setErro('');
+ setSucesso('');
+
+ const payload: CriarClienteRequest = {
+ cpf_cnpj: normalizarDocumento(cpfCnpj) || cpfCnpj.trim(),
+ nome: nome.trim(),
+ rg: textoOuNull(rg),
+ celular: textoOuNull(celular),
+ email: textoOuNull(email),
+ cep: textoOuNull(cep),
+ logradouro: textoOuNull(logradouro),
+ numero: textoOuNull(numero),
+ bairro: textoOuNull(bairro),
+ cidade: textoOuNull(cidade),
+ estado: textoOuNull(estado),
+ sexo: textoOuNull(sexo),
+ pessoafisica: textoOuNull(pessoafisica),
+ habilitado: Number(habilitado || 1),
+ };
+
+ if (isEdit) {
+ if (!initialData?.idclientes) {
+ setErro('Cliente inválido para edição.');
+ return;
+ }
+
+ await atualizarCliente(initialData.idclientes, payload);
+ setSucesso('Cliente atualizado com sucesso.');
+ } else {
+ await criarCliente(payload);
+ setSucesso('Cliente cadastrado com sucesso.');
+ limparFormulario();
+ }
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível salvar o cliente.';
+
+ setErro(message);
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ return (
+
+
+
+ }
+ label={isEdit ? 'Edição de cliente' : 'Cadastro de cliente'}
+ color="primary"
+ variant="outlined"
+ sx={{ marginBottom: 1.25, fontWeight: 700 }}
+ />
+
+
+ {titulo}
+
+
+
+ {subtitulo}
+
+
+
+ }
+ onClick={() => navigate('/clientes')}
+ sx={{
+ minHeight: 48,
+ borderRadius: 3,
+ px: 2.5,
+ }}
+ >
+ Voltar para lista
+
+
+
+ {erro && (
+
+ {erro}
+
+ )}
+
+ {sucesso && (
+
+ {sucesso}
+
+ )}
+
+
+
+
+
+
+ setPessoafisica(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+
+
+
+ setHabilitado(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+
+
+
+ setCpfCnpj(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setNome(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setRg(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setSexo(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ disabled={pessoafisica === 'J'}
+ >
+
+
+
+
+
+
+
+
+ setCelular(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setEmail(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+
+
+ setCep(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setLogradouro(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setNumero(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setBairro(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setCidade(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setEstado(event.target.value)}
+ inputProps={{ maxLength: 2 }}
+ fullWidth
+ sx={fieldSx}
+ />
+
+
+
+
+
+
+ :
+ }
+ sx={{
+ minHeight: 46,
+ px: 3,
+ boxShadow: 3,
+ }}
+ >
+ {saving
+ ? 'Salvando...'
+ : isEdit
+ ? 'Atualizar cliente'
+ : 'Salvar cliente'}
+
+
+
+
+
+
+
+
+ Resumo
+
+
+
+ Prévia rápida do cliente antes de salvar.
+
+
+
+
+
+
+
+ Nome
+
+
+ {nome || 'Sem nome'}
+
+
+
+
+
+ CPF/CNPJ
+
+
+ {cpfCnpj || '-'}
+
+
+
+
+
+ Tipo
+
+
+ {pessoafisica === 'J' ? 'Pessoa jurídica' : 'Pessoa física'}
+
+
+
+
+
+ Status
+
+
+ {Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado'}
+
+
+
+
+
+ Local
+
+
+ {[cidade, estado].filter(Boolean).join(' / ') || '-'}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/clientes/pages/ClientesPage.tsx b/financeiro-web/src/features/clientes/pages/ClientesPage.tsx
new file mode 100644
index 0000000..60b9af2
--- /dev/null
+++ b/financeiro-web/src/features/clientes/pages/ClientesPage.tsx
@@ -0,0 +1,787 @@
+import { useEffect, useMemo, useState } from 'react';
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ CircularProgress,
+ Divider,
+ IconButton,
+ MenuItem,
+ Pagination,
+ Paper,
+ Stack,
+ Switch,
+ TextField,
+ Tooltip,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ useMediaQuery,
+ useTheme,
+} from '@mui/material';
+import AddIcon from '@mui/icons-material/Add';
+import DeleteIcon from '@mui/icons-material/Delete';
+import EditIcon from '@mui/icons-material/Edit';
+import FilterAltIcon from '@mui/icons-material/FilterAlt';
+import PersonIcon from '@mui/icons-material/Person';
+import RestartAltIcon from '@mui/icons-material/RestartAlt';
+import SearchIcon from '@mui/icons-material/Search';
+import { Link as RouterLink } from 'react-router-dom';
+import {
+ alterarHabilitadoCliente,
+ deletarCliente,
+ listarClientes,
+ type ListarClientesParams,
+} from '../services/clientesService';
+import type {
+ Cliente,
+ ClientesPagination,
+} from '../types/clienteTypes';
+
+const LIMITE_PADRAO = 20;
+
+function formatarData(data: string | null) {
+ if (!data) return '-';
+
+ return new Date(`${String(data).slice(0, 10)}T00:00:00`).toLocaleDateString('pt-BR');
+}
+
+function pessoaLabel(pessoafisica: string | null) {
+ if (!pessoafisica) return '-';
+
+ const valor = String(pessoafisica).toUpperCase();
+
+ if (valor === 'F' || valor === 'S') return 'Pessoa física';
+ if (valor === 'J' || valor === 'N') return 'Pessoa jurídica';
+
+ return valor;
+}
+
+function pessoaColor(pessoafisica: string | null) {
+ const valor = String(pessoafisica || '').toUpperCase();
+
+ if (valor === 'F' || valor === 'S') return 'success';
+ if (valor === 'J' || valor === 'N') return 'info';
+
+ return 'default';
+}
+
+function statusClienteLabel(habilitado: number) {
+ return Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado';
+}
+
+function statusClienteColor(habilitado: number) {
+ return Number(habilitado) === 1 ? 'success' : 'default';
+}
+
+function enderecoResumo(item: Cliente) {
+ const partes = [
+ item.cidade,
+ item.estado,
+ ].filter(Boolean);
+
+ return partes.length > 0 ? partes.join(' / ') : '-';
+}
+
+export function ClientesPage() {
+ const [clientes, setClientes] = useState([]);
+ const [pagination, setPagination] = useState({
+ total: 0,
+ limite: LIMITE_PADRAO,
+ offset: 0,
+ page: 1,
+ totalPages: 1,
+ });
+
+ const [alterandoStatusId, setAlterandoStatusId] = useState(null);
+
+ const [loading, setLoading] = useState(true);
+ const [erro, setErro] = useState('');
+ const [sucesso, setSucesso] = useState('');
+
+ const [page, setPage] = useState(1);
+ const [busca, setBusca] = useState('');
+ const [pessoafisica, setPessoafisica] = useState('');
+ const [habilitado, setHabilitado] = useState('');
+ const [cidade, setCidade] = useState('');
+ const [estado, setEstado] = useState('');
+ const [orderBy, setOrderBy] = useState('nome');
+ const [orderDirection, setOrderDirection] = useState<'ASC' | 'DESC'>('ASC');
+
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
+
+ const filtrosAtivos = useMemo(() => {
+ let count = 0;
+
+ if (busca.trim()) count += 1;
+ if (pessoafisica) count += 1;
+ if (habilitado !== '') count += 1;
+ if (cidade.trim()) count += 1;
+ if (estado.trim()) count += 1;
+
+ return count;
+ }, [busca, pessoafisica, habilitado, cidade, estado]);
+
+ async function carregarClientes(pageToLoad = page) {
+ try {
+ setLoading(true);
+ setErro('');
+
+ const params: ListarClientesParams = {
+ limite: LIMITE_PADRAO,
+ page: pageToLoad,
+ busca: busca.trim() || undefined,
+ pessoafisica: pessoafisica || undefined,
+ habilitado,
+ cidade: cidade.trim() || undefined,
+ estado: estado.trim() || undefined,
+ orderBy,
+ orderDirection,
+ };
+
+ const response = await listarClientes(params);
+
+ setClientes(response.data);
+ setPagination(response.pagination);
+ setPage(response.pagination.page);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar os clientes.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ function aplicarFiltros() {
+ setPage(1);
+ carregarClientes(1);
+ }
+
+ function limparFiltros() {
+ setBusca('');
+ setPessoafisica('');
+ setHabilitado('');
+ setCidade('');
+ setEstado('');
+ setOrderBy('nome');
+ setOrderDirection('ASC');
+
+ setTimeout(() => {
+ setPage(1);
+ carregarClientes(1);
+ }, 0);
+ }
+
+ async function handleDeletarCliente(item: Cliente) {
+ const confirmou = window.confirm(
+ `Deseja realmente excluir "${item.nome}"?`
+ );
+
+ if (!confirmou) return;
+
+ try {
+ setLoading(true);
+ setErro('');
+ setSucesso('');
+
+ await deletarCliente(item.idclientes);
+
+ setSucesso('Cliente excluído com sucesso.');
+ await carregarClientes(page);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível excluir o cliente.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function handleAlterarStatusCliente(item: Cliente) {
+ const novoStatus = Number(item.habilitado) === 1 ? 0 : 1;
+
+ try {
+ setAlterandoStatusId(item.idclientes);
+ setErro('');
+ setSucesso('');
+
+ const clienteAtualizado = await alterarHabilitadoCliente(
+ item.idclientes,
+ novoStatus
+ );
+
+ setClientes((listaAtual) =>
+ listaAtual.map((cliente) =>
+ cliente.idclientes === item.idclientes ? clienteAtualizado : cliente
+ )
+ );
+
+ setSucesso(
+ novoStatus === 1
+ ? 'Cliente habilitado com sucesso.'
+ : 'Cliente desabilitado com sucesso.'
+ );
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível alterar o status do cliente.';
+
+ setErro(message);
+ } finally {
+ setAlterandoStatusId(null);
+ }
+ }
+
+ useEffect(() => {
+ carregarClientes(1);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+
+ }
+ label="Cadastro de clientes"
+ color="primary"
+ variant="outlined"
+ sx={{ marginBottom: 1.25, fontWeight: 700 }}
+ />
+
+
+ Clientes
+
+
+
+ Consulte, filtre e gerencie os clientes usados nos movimentos.
+
+
+
+ }
+ sx={{
+ minHeight: 48,
+ borderRadius: 3,
+ px: 2.5,
+ boxShadow: 3,
+ }}
+ >
+ Novo cliente
+
+
+
+ {erro && (
+
+ {erro}
+
+ )}
+
+ {sucesso && (
+
+ {sucesso}
+
+ )}
+
+
+
+
+
+ Clientes encontrados
+
+
+ {pagination.total}
+
+
+
+
+
+
+
+ Página atual
+
+
+ {pagination.page} / {pagination.totalPages}
+
+
+
+
+
+
+
+ Exibindo
+
+
+ {clientes.length}
+
+
+
+
+
+
+
+
+
+
+
+ Filtros
+
+
+
+
+
+
+ setBusca(event.target.value)}
+ placeholder="Nome, CPF/CNPJ, celular, e-mail..."
+ fullWidth
+ />
+
+ setPessoafisica(event.target.value)}
+ fullWidth
+ >
+
+
+
+
+
+
+ setHabilitado(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ fullWidth
+ >
+
+
+
+
+
+ setCidade(event.target.value)}
+ fullWidth
+ />
+
+ setEstado(event.target.value)}
+ fullWidth
+ />
+
+ setOrderBy(event.target.value)}
+ fullWidth
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ setOrderDirection(event.target.value as 'ASC' | 'DESC')
+ }
+ fullWidth
+ >
+
+
+
+
+
+
+ }
+ onClick={limparFiltros}
+ >
+ Limpar
+
+
+ }
+ onClick={aplicarFiltros}
+ disabled={loading}
+ >
+ Aplicar filtros
+
+
+
+
+
+
+
+ {loading ? (
+
+
+ Carregando clientes...
+
+ ) : clientes.length === 0 ? (
+
+
+ Nenhum cliente encontrado.
+
+
+ Ajuste os filtros ou cadastre um novo cliente.
+
+
+ ) : isMobile ? (
+ }>
+ {clientes.map((item) => (
+
+
+
+
+
+ {item.nome}
+
+
+
+ {item.cpf_cnpj || '-'}
+
+
+
+
+
+
+
+
+
+
+
+ Celular: {item.celular || '-'}
+
+
+
+ E-mail: {item.email || '-'}
+
+
+
+ Local: {enderecoResumo(item)}
+
+
+
+
+
+ handleAlterarStatusCliente(item)}
+ />
+
+
+
+ }
+ >
+ Editar
+
+
+ }
+ onClick={() => handleDeletarCliente(item)}
+ >
+ Excluir
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
+
+ Nome
+ CPF/CNPJ
+ Tipo
+ Celular
+ E-mail
+ Cidade
+ Estado
+ Status
+ Cadastro
+ Atualização
+ Ações
+
+
+
+
+ {clientes.map((item) => (
+
+
+
+
+ {item.nome}
+
+
+
+
+ {item.cpf_cnpj || '-'}
+
+
+
+
+
+ {item.celular || '-'}
+
+
+
+
+ {item.email || '-'}
+
+
+
+
+ {item.cidade || '-'}
+
+ {item.estado || '-'}
+
+
+
+
+
+ {formatarData(item.insert_date)}
+
+ {formatarData(item.update_date)}
+
+
+
+
+
+ handleAlterarStatusCliente(item)}
+ />
+
+
+
+
+
+
+
+
+
+
+ handleDeletarCliente(item)}
+ >
+
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+ {pagination.totalPages > 1 && (
+
+ {
+ setPage(novaPagina);
+ carregarClientes(novaPagina);
+ }}
+ />
+
+ )}
+
+
+
+ Exibindo página {pagination.page} de {pagination.totalPages}, total de{' '}
+ {pagination.total} registro(s).
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/clientes/pages/EditarClientePage.tsx b/financeiro-web/src/features/clientes/pages/EditarClientePage.tsx
new file mode 100644
index 0000000..478a478
--- /dev/null
+++ b/financeiro-web/src/features/clientes/pages/EditarClientePage.tsx
@@ -0,0 +1,62 @@
+import { useEffect, useState } from 'react';
+import { Alert, Box, CircularProgress, Typography } from '@mui/material';
+import { useParams } from 'react-router-dom';
+import { ClienteForm } from '../components/ClienteForm';
+import { buscarClientePorId } from '../services/clientesService';
+import type { Cliente } from '../types/clienteTypes';
+
+export function EditarClientePage() {
+ const { id } = useParams();
+
+ const [cliente, setCliente] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [erro, setErro] = useState('');
+
+ useEffect(() => {
+ async function carregarCliente() {
+ try {
+ setLoading(true);
+ setErro('');
+
+ const clienteId = Number(id);
+
+ if (!clienteId) {
+ setErro('ID do cliente inválido.');
+ return;
+ }
+
+ const data = await buscarClientePorId(clienteId);
+ setCliente(data);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar o cliente.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ carregarCliente();
+ }, [id]);
+
+ if (loading) {
+ return (
+
+
+ Carregando cliente...
+
+ );
+ }
+
+ if (erro) {
+ return (
+
+ {erro}
+
+ );
+ }
+
+ return ;
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/clientes/pages/NovoClientePage.tsx b/financeiro-web/src/features/clientes/pages/NovoClientePage.tsx
new file mode 100644
index 0000000..c7375da
--- /dev/null
+++ b/financeiro-web/src/features/clientes/pages/NovoClientePage.tsx
@@ -0,0 +1,5 @@
+import { ClienteForm } from '../components/ClienteForm';
+
+export function NovoClientePage() {
+ return ;
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/clientes/services/clientesService.ts b/financeiro-web/src/features/clientes/services/clientesService.ts
new file mode 100644
index 0000000..c8b5a57
--- /dev/null
+++ b/financeiro-web/src/features/clientes/services/clientesService.ts
@@ -0,0 +1,68 @@
+import { api } from '../../../services/api';
+import type {
+ ApiResponse,
+ AtualizarClienteRequest,
+ Cliente,
+ ClientesListResponse,
+ CriarClienteRequest,
+} from '../types/clienteTypes';
+
+export type OrderDirection = 'ASC' | 'DESC';
+
+export type ListarClientesParams = {
+ limite?: number;
+ offset?: number;
+ page?: number;
+ busca?: string;
+ pessoafisica?: string;
+ habilitado?: number | '';
+ cidade?: string;
+ estado?: string;
+ orderBy?: string;
+ orderDirection?: OrderDirection;
+};
+
+export async function listarClientes(
+ params?: ListarClientesParams
+): Promise {
+ const response = await api.get('/clientes', {
+ params,
+ });
+
+ return response.data;
+}
+
+export async function buscarClientePorId(id: number): Promise {
+ const response = await api.get>(`/clientes/${id}`);
+ return response.data.data;
+}
+
+export async function criarCliente(
+ data: CriarClienteRequest
+): Promise {
+ const response = await api.post>('/clientes', data);
+ return response.data.data;
+}
+
+export async function atualizarCliente(
+ id: number,
+ data: AtualizarClienteRequest
+): Promise {
+ const response = await api.put>(`/clientes/${id}`, data);
+ return response.data.data;
+}
+
+export async function alterarHabilitadoCliente(
+ id: number,
+ habilitado: number
+): Promise {
+ const response = await api.patch>(`/clientes/${id}/habilitado`, {
+ habilitado,
+ });
+
+ return response.data.data;
+}
+
+export async function deletarCliente(id: number): Promise {
+ await api.delete(`/clientes/${id}`);
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/clientes/types/clienteTypes.ts b/financeiro-web/src/features/clientes/types/clienteTypes.ts
new file mode 100644
index 0000000..4908233
--- /dev/null
+++ b/financeiro-web/src/features/clientes/types/clienteTypes.ts
@@ -0,0 +1,58 @@
+export type Cliente = {
+ idclientes: number;
+ cpf_cnpj: string;
+ nome: string;
+ rg: string | null;
+ celular: string | null;
+ email: string | null;
+ cep: string | null;
+ logradouro: string | null;
+ numero: string | null;
+ bairro: string | null;
+ cidade: string | null;
+ estado: string | null;
+ sexo: string | null;
+ pessoafisica: string | null;
+ habilitado: number;
+ insert_date: string | null;
+ update_date: string | null;
+};
+
+export type CriarClienteRequest = {
+ cpf_cnpj: string;
+ nome: string;
+ rg: string | null;
+ celular: string | null;
+ email: string | null;
+ cep: string | null;
+ logradouro: string | null;
+ numero: string | null;
+ bairro: string | null;
+ cidade: string | null;
+ estado: string | null;
+ sexo: string | null;
+ pessoafisica: string | null;
+ habilitado: number;
+};
+
+export type AtualizarClienteRequest = CriarClienteRequest;
+
+export type ClientesPagination = {
+ total: number;
+ limite: number;
+ offset: number;
+ page: number;
+ totalPages: number;
+};
+
+export type ApiResponse = {
+ ok: boolean;
+ message?: string;
+ data: T;
+};
+
+export type ClientesListResponse = {
+ ok: boolean;
+ data: Cliente[];
+ pagination: ClientesPagination;
+};
\ No newline at end of file
diff --git a/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx b/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx
index b520ea1..06f9fc1 100644
--- a/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx
+++ b/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx
@@ -9,8 +9,11 @@ import {
Chip,
CircularProgress,
Divider,
+ FormControlLabel,
MenuItem,
Paper,
+ Radio,
+ RadioGroup,
Stack,
TextField,
Typography,
@@ -32,10 +35,13 @@ import type {
import {
atualizarMovimento,
criarMovimento,
+ preverImpactoSaldoMovimento,
} from '../services/movimentosService';
import type {
CriarMovimentoRequest,
+ ModoParcelamento,
Movimento,
+ MovimentoImpactoSaldo,
MovimentoStatus,
MovimentoTipo,
} from '../types/movimentoTypes';
@@ -74,6 +80,18 @@ function formatarValorResumo(value: string) {
}).format(numero);
}
+function extrairCompetencia(data: string) {
+ if (!data) return null;
+ return data.slice(0, 7);
+}
+
+function formatarValorMoeda(valor: number) {
+ return new Intl.NumberFormat('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ }).format(Number(valor || 0));
+}
+
function FormSection({ title, description, children }: FormSectionProps) {
return (
('valor_parcela');
+
+ const [gerarParcelas, setGerarParcelas] = useState(true);
+ const [impactoSaldo, setImpactoSaldo] = useState([]);
+ const [loadingImpacto, setLoadingImpacto] = useState(false);
+
const titulo = isEdit ? 'Editar movimento' : 'Novo movimento';
const subtitulo = isEdit
? 'Atualize os dados do lançamento selecionado.'
: 'Cadastre entradas, saídas, sangrias e estornos direto no financeiro.';
+ const temParcelamento = Number(parcelas || 1) > 1;
+
const statusDisponiveis = useMemo(() => {
if (movimento === 'Entrada') return ['Recebido', 'A receber'];
if (movimento === 'Saida') return ['Pago', 'A pagar'];
@@ -200,7 +226,13 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
setStatus(statusDisponiveis[0]);
}
- if (movimento !== 'Sangria' && referenciaTipo === 'banco') {
+ if (movimento === 'Sangria') {
+ setReferenciaTipo('banco');
+ setStatus('Pago');
+ return;
+ }
+
+ if (referenciaTipo === 'banco') {
setReferenciaTipo('nenhum');
setIdBancoReferencia('');
}
@@ -235,6 +267,25 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
carregarReferencias();
}, []);
+ useEffect(() => {
+ const timer = window.setTimeout(() => {
+ carregarImpactoSaldo();
+ }, 450);
+
+ return () => window.clearTimeout(timer);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ movimento,
+ status,
+ valor,
+ parcelas,
+ parcela,
+ idBanco,
+ idBancoReferencia,
+ dataVencimento,
+ dataBaixa,
+ ]);
+
function limparFormulario() {
setDescricao('');
setValor('');
@@ -264,14 +315,31 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
return;
}
+ if (!idBanco) {
+ setErro('Selecione o banco/carteira.');
+ return;
+ }
+
+ if (Number(parcela || 1) > Number(parcelas || 1)) {
+ setErro('A parcela atual não pode ser maior que o total de parcelas.');
+ return;
+ }
+
if (referenciaTipo === 'cliente' && !idCliente) {
setErro('Selecione o cliente de referência.');
return;
}
- if (referenciaTipo === 'banco' && !idBancoReferencia) {
- setErro('Selecione o banco de referência.');
- return;
+ if (movimento === 'Sangria') {
+ if (referenciaTipo !== 'banco' || !idBancoReferencia) {
+ setErro('Selecione o banco de destino da sangria.');
+ return;
+ }
+
+ if (idBanco === idBancoReferencia) {
+ setErro('Banco de origem e destino não podem ser iguais.');
+ return;
+ }
}
try {
@@ -279,37 +347,82 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
setErro('');
setSucesso('');
- const payload: CriarMovimentoRequest = {
- movimento,
- descricao: descricao.trim(),
- valor: Number(valor),
- parcela: Number(parcela || 1),
- parcelas: Number(parcelas || 1),
- status,
- dataentrada: dataEntrada,
- datavencimento: dataVencimento || null,
- databaixa: status === 'Pago' || status === 'Recebido' ? dataBaixa || null : null,
- idcentrodecustos: toNumberOrNull(idCentroCusto),
- idbancos: toNumberOrNull(idBanco),
- idusuarios_baixa: null,
- idclientes: referenciaTipo === 'cliente' ? toNumberOrNull(idCliente) : null,
- idveiculosdetalhes: null,
- idbancos_p: referenciaTipo === 'banco' ? toNumberOrNull(idBancoReferencia) : null,
- observacao: observacao.trim() || null,
- };
+ let payload = montarPayload(false);
+
+ try {
+ if (isEdit) {
+ if (!initialData?.idcontasapagar) {
+ setErro('Movimento inválido para edição.');
+ return;
+ }
+
+ const response = await atualizarMovimento(initialData.idcontasapagar, payload);
+
+ setSucesso(
+ `Movimento atualizado com sucesso. Impactos de saldo: ${
+ response.impactoSaldo?.length || 0
+ }.`
+ );
+
+ setImpactoSaldo(response.impactoSaldo || []);
+ } else {
+ const response = await criarMovimento(payload);
+
+ setSucesso(
+ `Movimento cadastrado com sucesso. Parcelas geradas: ${
+ response.parcelasGeradas?.length || 0
+ }. Impactos de saldo: ${response.impactoSaldo?.length || 0}.`
+ );
+
+ setImpactoSaldo(response.impactoSaldo || []);
+ limparFormulario();
+ }
+ } catch (error: any) {
+ if (error?.response?.status === 409 && error?.response?.data?.requiresConfirmation) {
+ const duplicado = error.response.data.data;
+
+ const confirmou = window.confirm(
+ `Já existe um movimento parecido:\n\n` +
+ `ID: ${duplicado?.idcontasapagar}\n` +
+ `Descrição: ${duplicado?.descricao}\n` +
+ `Status: ${duplicado?.status}\n\n` +
+ `Deseja cadastrar mesmo assim?`
+ );
+
+ if (!confirmou) {
+ setErro('Cadastro cancelado para evitar duplicidade.');
+ return;
+ }
+
+ payload = montarPayload(true);
+
+ if (isEdit) {
+ if (!initialData?.idcontasapagar) {
+ setErro('Movimento inválido para edição.');
+ return;
+ }
+
+ const response = await atualizarMovimento(initialData.idcontasapagar, payload);
+
+ setSucesso('Movimento atualizado com sucesso.');
+ setImpactoSaldo(response.impactoSaldo || []);
+ } else {
+ const response = await criarMovimento(payload);
+
+ setSucesso(
+ `Movimento cadastrado com sucesso. Parcelas geradas: ${
+ response.parcelasGeradas?.length || 0
+ }.`
+ );
+
+ setImpactoSaldo(response.impactoSaldo || []);
+ limparFormulario();
+ }
- if (isEdit) {
- if (!initialData?.idcontasapagar) {
- setErro('Movimento inválido para edição.');
return;
}
- await atualizarMovimento(initialData.idcontasapagar, payload);
- setSucesso('Movimento atualizado com sucesso.');
- } else {
- await criarMovimento(payload);
- setSucesso('Movimento cadastrado com sucesso.');
- limparFormulario();
+ throw error;
}
} catch (error: any) {
const message =
@@ -322,6 +435,62 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
}
}
+ function montarPayload(confirmarDuplicado = false): CriarMovimentoRequest {
+ return {
+ movimento,
+ descricao: descricao.trim(),
+ valor: Number(valor),
+ parcela: Number(parcela || 1),
+ parcelas: Number(parcelas || 1),
+ status,
+ dataentrada: dataEntrada,
+ datavencimento: dataVencimento || null,
+ databaixa: status === 'Pago' || status === 'Recebido' ? dataBaixa || null : null,
+ idcentrodecustos: toNumberOrNull(idCentroCusto),
+ idbancos: toNumberOrNull(idBanco),
+ idusuarios_baixa: null,
+ idclientes: referenciaTipo === 'cliente' ? toNumberOrNull(idCliente) : null,
+ idveiculosdetalhes: null,
+ idbancos_p: referenciaTipo === 'banco' ? toNumberOrNull(idBancoReferencia) : null,
+
+ idmovimentosfixos: null,
+ competencia: extrairCompetencia(dataVencimento || dataEntrada),
+ grupo_parcelamento: initialData?.grupo_parcelamento || null,
+ origem: initialData?.origem || 'manual',
+ saldo_processado: initialData?.saldo_processado || 0,
+
+ observacao: observacao.trim() || null,
+
+ gerarParcelas: temParcelamento ? gerarParcelas : false,
+ modoParcelamento: temParcelamento ? modoParcelamento : 'valor_parcela',
+ confirmarDuplicado,
+ };
+ }
+
+ async function carregarImpactoSaldo() {
+ if (!descricao.trim() || !valor || Number(valor) <= 0 || !idBanco) {
+ setImpactoSaldo([]);
+ return;
+ }
+
+ try {
+ setLoadingImpacto(true);
+
+ const payload = montarPayload(true);
+
+ const data = await preverImpactoSaldoMovimento({
+ ...payload,
+ idcontasapagar: initialData?.idcontasapagar,
+ });
+
+ setImpactoSaldo(data);
+ } catch {
+ setImpactoSaldo([]);
+ } finally {
+ setLoadingImpacto(false);
+ }
+ }
+
return (
+ {temParcelamento && (
+
+
+ Como tratar o valor informado?
+
+
+
+ setModoParcelamento(event.target.value as ModoParcelamento)
+ }
+ >
+ }
+ label="Valor de cada parcela"
+ />
+
+ }
+ label="Valor total, dividir entre parcelas"
+ />
+
+
+ setGerarParcelas(!gerarParcelas)}
+ />
+ }
+ label="Gerar parcelas automaticamente"
+ />
+
+ )}
+
+
+
+
+
+
+ Impacto previsto no saldo
+
+
+ {loadingImpacto ? (
+
+
+ Calculando...
+
+ ) : impactoSaldo.length === 0 ? (
+
+ Sem impacto imediato
+
+ ) : (
+
+ {impactoSaldo.map((impacto, index) => (
+
+
+ {impacto.banco_descricao || `Banco ${impacto.idbancos}`}
+
+
+ = 0 ? 'success.main' : 'error.main'}
+ >
+ {impacto.valor_delta >= 0 ? '+' : ''}
+ {formatarValorMoeda(impacto.valor_delta)}
+
+
+ {impacto.saldo_anterior !== undefined && (
+
+ {formatarValorMoeda(impacto.saldo_anterior)} →{' '}
+ {formatarValorMoeda(impacto.saldo_posterior || 0)}
+
+ )}
+
+ ))}
+
+ )}
+
+
+ {temParcelamento && (
+
+
+ Parcelamento
+
+
+ {parcelas}x ·{' '}
+ {modoParcelamento === 'valor_total'
+ ? 'dividindo valor total'
+ : 'valor por parcela'}
+
+
+ )}
diff --git a/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx b/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx
index 2a1934a..aba8259 100644
--- a/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx
+++ b/financeiro-web/src/features/movimentos/pages/MovimentosPage.tsx
@@ -31,8 +31,10 @@ import FilterAltIcon from '@mui/icons-material/FilterAlt';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import SearchIcon from '@mui/icons-material/Search';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
+import DeleteIcon from '@mui/icons-material/Delete';
import { Link as RouterLink } from 'react-router-dom';
import {
+ deletarMovimento,
listarMovimentos,
type DataCampo,
type ListarMovimentosParams,
@@ -122,6 +124,8 @@ function getReferencia(item: Movimento) {
}
export function MovimentosPage() {
+ const [sucesso, setSucesso] = useState('');
+
const [movimentos, setMovimentos] = useState([]);
const [pagination, setPagination] = useState({
total: 0,
@@ -136,6 +140,9 @@ export function MovimentosPage() {
totalSaidas: 0,
totalSangrias: 0,
totalEstornos: 0,
+ totalBaixado: 0,
+ totalAberto: 0,
+ saldoProcessado: 0,
saldo: 0,
});
@@ -158,6 +165,10 @@ export function MovimentosPage() {
const [idCentroCusto, setIdCentroCusto] = useState('');
const [idCliente, setIdCliente] = useState('');
const [referenciaTipo, setReferenciaTipo] = useState('');
+ const [competencia, setCompetencia] = useState('');
+ const [origem, setOrigem] = useState('');
+ const [saldoProcessado, setSaldoProcessado] = useState('');
+ const [incluirExcluidos, setIncluirExcluidos] = useState('');
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
@@ -173,6 +184,10 @@ export function MovimentosPage() {
if (idCliente) count += 1;
if (referenciaTipo) count += 1;
if (dataInicio || dataFim) count += 1;
+ if (competencia.trim()) count += 1;
+ if (origem) count += 1;
+ if (saldoProcessado !== '') count += 1;
+ if (incluirExcluidos !== '') count += 1;
return count;
}, [
@@ -185,6 +200,10 @@ export function MovimentosPage() {
referenciaTipo,
dataInicio,
dataFim,
+ competencia,
+ origem,
+ saldoProcessado,
+ incluirExcluidos,
]);
async function carregarReferencias() {
@@ -223,6 +242,10 @@ export function MovimentosPage() {
idcentrodecustos: idCentroCusto || undefined,
idclientes: idCliente || undefined,
referenciaTipo: referenciaTipo || undefined,
+ competencia: competencia.trim() || undefined,
+ origem: origem || undefined,
+ saldo_processado: saldoProcessado,
+ incluirExcluidos,
orderBy: dataCampo,
orderDirection: 'DESC',
};
@@ -260,6 +283,10 @@ export function MovimentosPage() {
setIdCentroCusto('');
setIdCliente('');
setReferenciaTipo('');
+ setCompetencia('');
+ setOrigem('');
+ setSaldoProcessado('');
+ setIncluirExcluidos('');
setTimeout(() => {
setPage(1);
@@ -267,6 +294,69 @@ export function MovimentosPage() {
}, 0);
}
+ function origemLabel(origem: string | null | undefined) {
+ if (!origem) return 'Manual';
+
+ const labels: Record = {
+ manual: 'Manual',
+ parcelamento: 'Parcelamento',
+ movimento_fixo: 'Movimento fixo',
+ quitacao: 'Quitação',
+ ajuste_saldo: 'Ajuste de saldo',
+ };
+
+ return labels[origem] || origem;
+ }
+
+ function origemChipColor(origem: string | null | undefined) {
+ if (origem === 'movimento_fixo') return 'info';
+ if (origem === 'parcelamento') return 'secondary';
+ if (origem === 'quitacao') return 'success';
+ if (origem === 'ajuste_saldo') return 'warning';
+
+ return 'default';
+ }
+
+ function saldoProcessadoLabel(valor: number | null | undefined) {
+ return Number(valor) === 1 ? 'Saldo processado' : 'Sem impacto';
+ }
+
+ function saldoProcessadoColor(valor: number | null | undefined) {
+ return Number(valor) === 1 ? 'success' : 'default';
+ }
+
+ function movimentoExcluido(item: Movimento) {
+ return Boolean(item.deleted_at);
+ }
+
+ async function handleDeletarMovimento(item: Movimento) {
+ const confirmou = window.confirm(
+ `Deseja realmente excluir "${item.descricao}"?\n\n` +
+ 'Se o movimento já impactou saldo, a API vai reverter automaticamente.'
+ );
+
+ if (!confirmou) return;
+
+ try {
+ setLoading(true);
+ setErro('');
+ setSucesso('');
+
+ await deletarMovimento(item.idcontasapagar);
+
+ setSucesso('Movimento excluído com sucesso. O saldo foi revertido se necessário.');
+ await carregarMovimentos(page);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível excluir o movimento.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
useEffect(() => {
carregarReferencias();
carregarMovimentos(1);
@@ -322,11 +412,17 @@ export function MovimentosPage() {
)}
+ {sucesso && (
+
+ {sucesso}
+
+ )}
+
+
+
+
+
+ Em aberto
+
+
+ {formatarValor(summary.totalAberto)}
+
+
+
+
+
+
+
+ Baixados
+
+
+ {formatarValor(summary.totalBaixado)}
+
+
+
@@ -428,6 +546,7 @@ export function MovimentosPage() {
+
Com banco referência
+
+ setCompetencia(event.target.value)}
+ InputLabelProps={{ shrink: true }}
+ fullWidth
+ />
+
+ setOrigem(event.target.value)}
+ fullWidth
+ >
+
+
+
+
+
+
+
+
+
+ setSaldoProcessado(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ fullWidth
+ >
+
+
+
+
+
+
+ setIncluirExcluidos(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ fullWidth
+ >
+
+
+
+
+
+
+
+
+ {movimentoExcluido(item) && (
+
+ )}
@@ -664,6 +857,14 @@ export function MovimentosPage() {
Referência: {getReferencia(item)}
+
+ Competência: {item.competencia || '-'}
+
+
+
+ Origem: {origemLabel(item.origem)}
+
+
+
+ {formatarData(item.deleted_at)}
+
+
-
-
-
-
-
+
+ {!movimentoExcluido(item) && (
+ <>
+
+
+
+
+
+
+
+ handleDeletarMovimento(item)}
+ >
+
+
+
+ >
+ )}
+
diff --git a/financeiro-web/src/features/movimentos/services/movimentosService.ts b/financeiro-web/src/features/movimentos/services/movimentosService.ts
index 9790dff..ef3059d 100644
--- a/financeiro-web/src/features/movimentos/services/movimentosService.ts
+++ b/financeiro-web/src/features/movimentos/services/movimentosService.ts
@@ -4,16 +4,12 @@ import type {
AtualizarMovimentoRequest,
CriarMovimentoRequest,
Movimento,
+ MovimentoImpactoSaldo,
MovimentosListResponse,
+ MovimentoSaveResponse,
+ VerificarDuplicidadeResponse,
} from '../types/movimentoTypes';
-export type DataCampo =
- | 'dataentrada'
- | 'datavencimento'
- | 'databaixa'
- | 'insert_date'
- | 'update_date';
-
export type OrderDirection = 'ASC' | 'DESC';
export type ListarMovimentosParams = {
@@ -26,11 +22,17 @@ export type ListarMovimentosParams = {
idcentrodecustos?: number | '';
idclientes?: number | '';
idbancos_p?: number | '';
+ idmovimentosfixos?: number | '';
+ competencia?: string;
+ grupo_parcelamento?: string;
+ origem?: string;
+ saldo_processado?: number | '';
referenciaTipo?: string;
- dataCampo?: DataCampo;
+ dataCampo?: string;
dataInicio?: string;
dataFim?: string;
busca?: string;
+ incluirExcluidos?: number | '';
orderBy?: string;
orderDirection?: OrderDirection;
};
@@ -50,17 +52,43 @@ export async function buscarMovimentoPorId(id: number): Promise {
return response.data.data;
}
+export async function verificarDuplicidadeMovimento(
+ data: CriarMovimentoRequest & { idcontasapagar?: number }
+): Promise {
+ const response = await api.post>(
+ '/movimentos/verificar-duplicidade',
+ data
+ );
+
+ return response.data.data;
+}
+
+export async function preverImpactoSaldoMovimento(
+ data: CriarMovimentoRequest & { idcontasapagar?: number }
+): Promise {
+ const response = await api.post>(
+ '/movimentos/prever-impacto-saldo',
+ data
+ );
+
+ return response.data.data;
+}
+
export async function criarMovimento(
data: CriarMovimentoRequest
-): Promise {
- const response = await api.post>('/movimentos', data);
- return response.data.data;
+): Promise {
+ const response = await api.post('/movimentos', data);
+ return response.data;
}
export async function atualizarMovimento(
id: number,
data: AtualizarMovimentoRequest
-): Promise {
- const response = await api.put>(`/movimentos/${id}`, data);
- return response.data.data;
+): Promise {
+ const response = await api.put(`/movimentos/${id}`, data);
+ return response.data;
+}
+
+export async function deletarMovimento(id: number): Promise {
+ await api.delete(`/movimentos/${id}`);
}
\ No newline at end of file
diff --git a/financeiro-web/src/features/movimentos/types/movimentoTypes.ts b/financeiro-web/src/features/movimentos/types/movimentoTypes.ts
index 8443a10..c743f74 100644
--- a/financeiro-web/src/features/movimentos/types/movimentoTypes.ts
+++ b/financeiro-web/src/features/movimentos/types/movimentoTypes.ts
@@ -2,6 +2,47 @@ export type MovimentoTipo = 'Entrada' | 'Saida' | 'Sangria' | 'Estorno';
export type MovimentoStatus = 'Pago' | 'A pagar' | 'Recebido' | 'A receber';
+export type ModoParcelamento = 'valor_parcela' | 'valor_total';
+
+export type Movimento = {
+ idcontasapagar: number;
+ movimento: MovimentoTipo;
+ descricao: string;
+ dataentrada: string | null;
+ datavencimento: string | null;
+ databaixa: string | null;
+ valor: number;
+ parcela: number;
+ parcelas: number;
+ status: MovimentoStatus;
+ idcentrodecustos: number | null;
+ idbancos: number | null;
+ idusuarios_cad: number | null;
+ idusuarios_baixa: number | null;
+ idclientes: number | null;
+ idveiculosdetalhes: number | null;
+ idbancos_p: number | null;
+
+ idmovimentosfixos: number | null;
+ competencia: string | null;
+ grupo_parcelamento: string | null;
+ origem: string | null;
+ saldo_processado: number;
+
+ observacao: string | null;
+ insert_date: string | null;
+ update_date: string | null;
+ deleted_at: string | null;
+
+ banco_descricao?: string | null;
+ banco_debito?: number | null;
+ centro_custo_descricao?: string | null;
+ cliente_nome?: string | null;
+ banco_referencia_descricao?: string | null;
+ banco_referencia_debito?: number | null;
+ movimento_fixo_descricao?: string | null;
+};
+
export type CriarMovimentoRequest = {
movimento: MovimentoTipo;
descricao: string;
@@ -18,21 +59,39 @@ export type CriarMovimentoRequest = {
idclientes: number | null;
idveiculosdetalhes: number | null;
idbancos_p: number | null;
+
+ idmovimentosfixos?: number | null;
+ competencia?: string | null;
+ grupo_parcelamento?: string | null;
+ origem?: string | null;
+ saldo_processado?: number;
+
observacao: string | null;
+
+ gerarParcelas?: boolean;
+ modoParcelamento?: ModoParcelamento;
+ confirmarDuplicado?: boolean;
};
export type AtualizarMovimentoRequest = CriarMovimentoRequest;
-export type Movimento = CriarMovimentoRequest & {
- idcontasapagar: number;
- idusuarios_cad: number | null;
- insert_date: string | null;
- update_date: string | null;
-
+export type MovimentoImpactoSaldo = {
+ idbancos: number;
banco_descricao?: string | null;
- centro_custo_descricao?: string | null;
- cliente_nome?: string | null;
- banco_referencia_descricao?: string | null;
+ valor_delta: number;
+ saldo_anterior?: number;
+ saldo_posterior?: number;
+ descricao?: string | null;
+};
+
+export type VerificarDuplicidadeResponse = Movimento | null;
+
+export type MovimentoSaveResponse = {
+ ok: boolean;
+ message?: string;
+ data: Movimento;
+ parcelasGeradas?: Movimento[];
+ impactoSaldo?: MovimentoImpactoSaldo[];
};
export type MovimentosPagination = {
@@ -49,6 +108,9 @@ export type MovimentosSummary = {
totalSaidas: number;
totalSangrias: number;
totalEstornos: number;
+ totalBaixado: number;
+ totalAberto: number;
+ saldoProcessado: number;
saldo: number;
};
@@ -58,11 +120,6 @@ export type ApiResponse = {
data: T;
};
-export type ApiListResponse = {
- ok: boolean;
- data: T[];
-};
-
export type MovimentosListResponse = {
ok: boolean;
data: Movimento[];
diff --git a/financeiro-web/src/features/movimentosFixos/components/MovimentoFIxoForm.tsx b/financeiro-web/src/features/movimentosFixos/components/MovimentoFIxoForm.tsx
new file mode 100644
index 0000000..d92b2ba
--- /dev/null
+++ b/financeiro-web/src/features/movimentosFixos/components/MovimentoFIxoForm.tsx
@@ -0,0 +1,567 @@
+import { useEffect, useMemo, useState } from 'react';
+import type { FormEvent, ReactNode } from 'react';
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ CircularProgress,
+ Divider,
+ MenuItem,
+ Paper,
+ Stack,
+ TextField,
+ Typography,
+} from '@mui/material';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+import EventRepeatIcon from '@mui/icons-material/EventRepeat';
+import SaveIcon from '@mui/icons-material/Save';
+import { useNavigate } from 'react-router-dom';
+import {
+ listarBancos,
+ listarCentrosCusto,
+} from '../../referencias/services/referenciasService';
+import type {
+ Banco,
+ CentroCusto,
+} from '../../referencias/types/referenciasTypes';
+import {
+ atualizarMovimentoFixo,
+ criarMovimentoFixo,
+} from '../services/movimentosFixosService';
+import type {
+ CriarMovimentoFixoRequest,
+ MovimentoFixo,
+ MovimentoFixoTipo,
+} from '../types/movimentoFixoTypes';
+
+type MovimentoFixoFormProps = {
+ mode: 'create' | 'edit';
+ initialData?: MovimentoFixo | null;
+};
+
+type FormSectionProps = {
+ title: string;
+ description?: string;
+ children: ReactNode;
+};
+
+function FormSection({ title, description, children }: FormSectionProps) {
+ return (
+
+
+
+ {title}
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+
+ {children}
+
+
+ );
+}
+
+const fieldSx = {
+ '& .MuiOutlinedInput-root': {
+ borderRadius: 2.5,
+ backgroundColor: '#FFFFFF',
+ minHeight: 48,
+ },
+ '& .MuiInputLabel-root': {
+ backgroundColor: '#FFFFFF',
+ paddingX: 0.5,
+ },
+};
+
+function toNumberOrNull(value: string): number | null {
+ if (!value) return null;
+ return Number(value);
+}
+
+function formatarValorResumo(value: string) {
+ const numero = Number(value || 0);
+
+ return new Intl.NumberFormat('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ }).format(numero);
+}
+
+function movimentoLabel(movimento: MovimentoFixoTipo) {
+ if (movimento === 'Saida') return 'Saída';
+ return movimento;
+}
+
+export function MovimentoFixoForm({ mode, initialData }: MovimentoFixoFormProps) {
+ const navigate = useNavigate();
+
+ const isEdit = mode === 'edit';
+
+ const [bancos, setBancos] = useState([]);
+ const [centrosCusto, setCentrosCusto] = useState([]);
+
+ const [loadingRefs, setLoadingRefs] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [erro, setErro] = useState('');
+ const [sucesso, setSucesso] = useState('');
+
+ const [movimento, setMovimento] = useState('Saida');
+ const [descricao, setDescricao] = useState('');
+ const [valor, setValor] = useState('');
+ const [diaVencimento, setDiaVencimento] = useState('1');
+ const [idCentroCusto, setIdCentroCusto] = useState('');
+ const [idBanco, setIdBanco] = useState('');
+ const [habilitado, setHabilitado] = useState('1');
+
+ const titulo = isEdit ? 'Editar movimento fixo' : 'Novo movimento fixo';
+ const subtitulo = isEdit
+ ? 'Atualize os dados do movimento recorrente selecionado.'
+ : 'Cadastre lançamentos recorrentes para automatizar o financeiro depois.';
+
+ const movimentoDescricao = useMemo(() => {
+ if (movimento === 'Entrada') return 'Entrada recorrente';
+ if (movimento === 'Saida') return 'Saída recorrente';
+ if (movimento === 'Sangria') return 'Sangria recorrente';
+ return 'Estorno recorrente';
+ }, [movimento]);
+
+ useEffect(() => {
+ if (!initialData) return;
+
+ setMovimento(initialData.movimento);
+ setDescricao(initialData.descricao || '');
+ setValor(String(initialData.valor || ''));
+ setDiaVencimento(String(initialData.dia_vencimento || 1));
+ setIdCentroCusto(initialData.idcentrodecustos ? String(initialData.idcentrodecustos) : '');
+ setIdBanco(initialData.idbancos ? String(initialData.idbancos) : '');
+ setHabilitado(String(initialData.habilitado ?? 1));
+ }, [initialData]);
+
+ useEffect(() => {
+ async function carregarReferencias() {
+ try {
+ setLoadingRefs(true);
+ setErro('');
+
+ const [bancosData, centrosData] = await Promise.all([
+ listarBancos(),
+ listarCentrosCusto(),
+ ]);
+
+ setBancos(bancosData);
+ setCentrosCusto(centrosData);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar os dados de referência.';
+
+ setErro(message);
+ } finally {
+ setLoadingRefs(false);
+ }
+ }
+
+ carregarReferencias();
+ }, []);
+
+ function limparFormulario() {
+ setMovimento('Saida');
+ setDescricao('');
+ setValor('');
+ setDiaVencimento('1');
+ setIdCentroCusto('');
+ setIdBanco('');
+ setHabilitado('1');
+ }
+
+ async function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+
+ if (!descricao.trim()) {
+ setErro('Informe a descrição.');
+ return;
+ }
+
+ if (!valor || Number(valor) <= 0) {
+ setErro('Informe um valor válido.');
+ return;
+ }
+
+ const dia = Number(diaVencimento);
+
+ if (!Number.isInteger(dia) || dia < 1 || dia > 31) {
+ setErro('Informe um dia de vencimento entre 1 e 31.');
+ return;
+ }
+
+ try {
+ setSaving(true);
+ setErro('');
+ setSucesso('');
+
+ const payload: CriarMovimentoFixoRequest = {
+ movimento,
+ descricao: descricao.trim(),
+ valor: Number(valor),
+ dia_vencimento: dia,
+ idcentrodecustos: toNumberOrNull(idCentroCusto),
+ habilitado: Number(habilitado || 1),
+ idbancos: toNumberOrNull(idBanco),
+ };
+
+ if (isEdit) {
+ if (!initialData?.idmovimentosfixos) {
+ setErro('Movimento fixo inválido para edição.');
+ return;
+ }
+
+ await atualizarMovimentoFixo(initialData.idmovimentosfixos, payload);
+ setSucesso('Movimento fixo atualizado com sucesso.');
+ } else {
+ await criarMovimentoFixo(payload);
+ setSucesso('Movimento fixo cadastrado com sucesso.');
+ limparFormulario();
+ }
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível salvar o movimento fixo.';
+
+ setErro(message);
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ return (
+
+
+
+ }
+ label={isEdit ? 'Edição de recorrência' : 'Cadastro de recorrência'}
+ color="primary"
+ variant="outlined"
+ sx={{ marginBottom: 1.25, fontWeight: 700 }}
+ />
+
+
+ {titulo}
+
+
+
+ {subtitulo}
+
+
+
+ }
+ onClick={() => navigate('/movimentos-fixos')}
+ sx={{
+ minHeight: 48,
+ borderRadius: 3,
+ px: 2.5,
+ }}
+ >
+ Voltar para lista
+
+
+
+ {erro && (
+
+ {erro}
+
+ )}
+
+ {sucesso && (
+
+ {sucesso}
+
+ )}
+
+
+
+
+ {loadingRefs ? (
+
+
+ Carregando dados...
+
+ ) : (
+
+
+ setMovimento(event.target.value as MovimentoFixoTipo)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+
+
+
+
+
+ setHabilitado(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+
+
+
+ setDescricao(event.target.value)}
+ placeholder="Ex: Internet, aluguel, energia..."
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setValor(event.target.value)}
+ inputProps={{
+ step: '0.01',
+ min: '0',
+ }}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setDiaVencimento(event.target.value)}
+ inputProps={{
+ min: '1',
+ max: '31',
+ }}
+ fullWidth
+ sx={fieldSx}
+ />
+
+
+
+ setIdBanco(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+ {bancos.map((banco) => (
+
+ ))}
+
+
+ setIdCentroCusto(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+ {centrosCusto.map((centro) => (
+
+ ))}
+
+
+
+
+
+
+
+ :
+ }
+ sx={{
+ minHeight: 46,
+ px: 3,
+ boxShadow: 3,
+ }}
+ >
+ {saving
+ ? 'Salvando...'
+ : isEdit
+ ? 'Atualizar movimento fixo'
+ : 'Salvar movimento fixo'}
+
+
+
+ )}
+
+
+
+
+
+ Resumo
+
+
+
+ Prévia da recorrência antes de salvar.
+
+
+
+
+
+
+
+ Movimento
+
+
+ {movimentoLabel(movimento)}
+
+
+
+
+
+ Tipo
+
+
+ {movimentoDescricao}
+
+
+
+
+
+ Valor
+
+
+ {formatarValorResumo(valor)}
+
+
+
+
+
+ Vencimento
+
+
+ Todo dia {diaVencimento || '-'}
+
+
+
+
+
+ Status
+
+
+ {Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado'}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/movimentosFixos/pages/EditarMovimentoFixoPage.tsx b/financeiro-web/src/features/movimentosFixos/pages/EditarMovimentoFixoPage.tsx
new file mode 100644
index 0000000..e5911db
--- /dev/null
+++ b/financeiro-web/src/features/movimentosFixos/pages/EditarMovimentoFixoPage.tsx
@@ -0,0 +1,62 @@
+import { useEffect, useState } from 'react';
+import { Alert, Box, CircularProgress, Typography } from '@mui/material';
+import { useParams } from 'react-router-dom';
+import { MovimentoFixoForm } from '../components/MovimentoFixoForm';
+import { buscarMovimentoFixoPorId } from '../services/movimentosFixosService';
+import type { MovimentoFixo } from '../types/movimentoFixoTypes';
+
+export function EditarMovimentoFixoPage() {
+ const { id } = useParams();
+
+ const [movimentoFixo, setMovimentoFixo] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [erro, setErro] = useState('');
+
+ useEffect(() => {
+ async function carregarMovimentoFixo() {
+ try {
+ setLoading(true);
+ setErro('');
+
+ const movimentoFixoId = Number(id);
+
+ if (!movimentoFixoId) {
+ setErro('ID do movimento fixo inválido.');
+ return;
+ }
+
+ const data = await buscarMovimentoFixoPorId(movimentoFixoId);
+ setMovimentoFixo(data);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar o movimento fixo.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ carregarMovimentoFixo();
+ }, [id]);
+
+ if (loading) {
+ return (
+
+
+ Carregando movimento fixo...
+
+ );
+ }
+
+ if (erro) {
+ return (
+
+ {erro}
+
+ );
+ }
+
+ return ;
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/movimentosFixos/pages/MovimentosFixosPage.tsx b/financeiro-web/src/features/movimentosFixos/pages/MovimentosFixosPage.tsx
new file mode 100644
index 0000000..8af8d6e
--- /dev/null
+++ b/financeiro-web/src/features/movimentosFixos/pages/MovimentosFixosPage.tsx
@@ -0,0 +1,920 @@
+import { useEffect, useMemo, useState } from 'react';
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ CircularProgress,
+ Divider,
+ IconButton,
+ MenuItem,
+ Pagination,
+ Paper,
+ Stack,
+ Switch,
+ TextField,
+ Tooltip,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ useMediaQuery,
+ useTheme,
+} from '@mui/material';
+import AddIcon from '@mui/icons-material/Add';
+import DeleteIcon from '@mui/icons-material/Delete';
+import EditIcon from '@mui/icons-material/Edit';
+import EventRepeatIcon from '@mui/icons-material/EventRepeat';
+import FilterAltIcon from '@mui/icons-material/FilterAlt';
+import RestartAltIcon from '@mui/icons-material/RestartAlt';
+import SearchIcon from '@mui/icons-material/Search';
+import { Link as RouterLink } from 'react-router-dom';
+import {
+ alterarHabilitadoMovimentoFixo,
+ deletarMovimentoFixo,
+ listarMovimentosFixos,
+ type ListarMovimentosFixosParams,
+} from '../services/movimentosFixosService';
+import type {
+ MovimentoFixo,
+ MovimentoFixoTipo,
+ MovimentosFixosPagination,
+ MovimentosFixosSummary,
+} from '../types/movimentoFixoTypes';
+import {
+ listarBancos,
+ listarCentrosCusto,
+} from '../../referencias/services/referenciasService';
+import type {
+ Banco,
+ CentroCusto,
+} from '../../referencias/types/referenciasTypes';
+
+const LIMITE_PADRAO = 20;
+
+function formatarValor(valor: number) {
+ return new Intl.NumberFormat('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ }).format(Number(valor || 0));
+}
+
+function formatarData(data: string | null) {
+ if (!data) return '-';
+
+ return new Date(`${String(data).slice(0, 10)}T00:00:00`).toLocaleDateString('pt-BR');
+}
+
+function movimentoChipColor(movimento: string) {
+ if (movimento === 'Entrada') return 'success';
+ if (movimento === 'Saida') return 'error';
+ if (movimento === 'Sangria') return 'warning';
+
+ return 'default';
+}
+
+function statusLabel(habilitado: number) {
+ return Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado';
+}
+
+function statusColor(habilitado: number) {
+ return Number(habilitado) === 1 ? 'success' : 'default';
+}
+
+export function MovimentosFixosPage() {
+ const [movimentosFixos, setMovimentosFixos] = useState([]);
+ const [pagination, setPagination] = useState({
+ total: 0,
+ limite: LIMITE_PADRAO,
+ offset: 0,
+ page: 1,
+ totalPages: 1,
+ });
+ const [summary, setSummary] = useState({
+ quantidade: 0,
+ valorTotal: 0,
+ totalEntradas: 0,
+ totalSaidas: 0,
+ totalSangrias: 0,
+ totalEstornos: 0,
+ habilitados: 0,
+ desabilitados: 0,
+ });
+
+ const [bancos, setBancos] = useState([]);
+ const [centrosCusto, setCentrosCusto] = useState([]);
+
+ const [alterandoStatusId, setAlterandoStatusId] = useState(null);
+
+ const [loading, setLoading] = useState(true);
+ const [loadingRefs, setLoadingRefs] = useState(true);
+ const [erro, setErro] = useState('');
+ const [sucesso, setSucesso] = useState('');
+
+ const [page, setPage] = useState(1);
+ const [busca, setBusca] = useState('');
+ const [movimento, setMovimento] = useState('');
+ const [idBanco, setIdBanco] = useState('');
+ const [idCentroCusto, setIdCentroCusto] = useState('');
+ const [habilitado, setHabilitado] = useState('');
+ const [diaInicio, setDiaInicio] = useState('');
+ const [diaFim, setDiaFim] = useState('');
+ const [orderBy, setOrderBy] = useState('dia_vencimento');
+ const [orderDirection, setOrderDirection] = useState<'ASC' | 'DESC'>('ASC');
+
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
+
+ const filtrosAtivos = useMemo(() => {
+ let count = 0;
+
+ if (busca.trim()) count += 1;
+ if (movimento) count += 1;
+ if (idBanco) count += 1;
+ if (idCentroCusto) count += 1;
+ if (habilitado !== '') count += 1;
+ if (diaInicio !== '') count += 1;
+ if (diaFim !== '') count += 1;
+
+ return count;
+ }, [
+ busca,
+ movimento,
+ idBanco,
+ idCentroCusto,
+ habilitado,
+ diaInicio,
+ diaFim,
+ ]);
+
+ async function carregarReferencias() {
+ try {
+ setLoadingRefs(true);
+
+ const [bancosData, centrosData] = await Promise.all([
+ listarBancos(),
+ listarCentrosCusto(),
+ ]);
+
+ setBancos(bancosData);
+ setCentrosCusto(centrosData);
+ } finally {
+ setLoadingRefs(false);
+ }
+ }
+
+ async function carregarMovimentosFixos(pageToLoad = page) {
+ try {
+ setLoading(true);
+ setErro('');
+
+ const params: ListarMovimentosFixosParams = {
+ limite: LIMITE_PADRAO,
+ page: pageToLoad,
+ busca: busca.trim() || undefined,
+ movimento: movimento || undefined,
+ idbancos: idBanco || undefined,
+ idcentrodecustos: idCentroCusto || undefined,
+ habilitado,
+ diaInicio,
+ diaFim,
+ orderBy,
+ orderDirection,
+ };
+
+ const response = await listarMovimentosFixos(params);
+
+ setMovimentosFixos(response.data);
+ setPagination(response.pagination);
+ setSummary(response.summary);
+ setPage(response.pagination.page);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar os movimentos fixos.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ function aplicarFiltros() {
+ setPage(1);
+ carregarMovimentosFixos(1);
+ }
+
+ function limparFiltros() {
+ setBusca('');
+ setMovimento('');
+ setIdBanco('');
+ setIdCentroCusto('');
+ setHabilitado('');
+ setDiaInicio('');
+ setDiaFim('');
+ setOrderBy('dia_vencimento');
+ setOrderDirection('ASC');
+
+ setTimeout(() => {
+ setPage(1);
+ carregarMovimentosFixos(1);
+ }, 0);
+ }
+
+ async function handleDeletarMovimentoFixo(item: MovimentoFixo) {
+ const confirmou = window.confirm(
+ `Deseja realmente excluir "${item.descricao}"?`
+ );
+
+ if (!confirmou) return;
+
+ try {
+ setLoading(true);
+ setErro('');
+ setSucesso('');
+
+ await deletarMovimentoFixo(item.idmovimentosfixos);
+
+ setSucesso('Movimento fixo excluído com sucesso.');
+ await carregarMovimentosFixos(page);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível excluir o movimento fixo.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function handleAlterarStatusMovimentoFixo(item: MovimentoFixo) {
+ const novoStatus = Number(item.habilitado) === 1 ? 0 : 1;
+
+ try {
+ setAlterandoStatusId(item.idmovimentosfixos);
+ setErro('');
+ setSucesso('');
+
+ const movimentoAtualizado = await alterarHabilitadoMovimentoFixo(
+ item.idmovimentosfixos,
+ novoStatus
+ );
+
+ setMovimentosFixos((listaAtual) =>
+ listaAtual.map((movimentoFixo) =>
+ movimentoFixo.idmovimentosfixos === item.idmovimentosfixos
+ ? movimentoAtualizado
+ : movimentoFixo
+ )
+ );
+
+ setSucesso(
+ novoStatus === 1
+ ? 'Movimento fixo habilitado com sucesso.'
+ : 'Movimento fixo desabilitado com sucesso.'
+ );
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível alterar o status do movimento fixo.';
+
+ setErro(message);
+ } finally {
+ setAlterandoStatusId(null);
+ }
+ }
+
+ useEffect(() => {
+ carregarReferencias();
+ carregarMovimentosFixos(1);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+
+ }
+ label="Automação financeira"
+ color="primary"
+ variant="outlined"
+ sx={{ marginBottom: 1.25, fontWeight: 700 }}
+ />
+
+
+ Movimentos fixos
+
+
+
+ Cadastre lançamentos recorrentes para gerar movimentos automaticamente depois.
+
+
+
+ }
+ sx={{
+ minHeight: 48,
+ borderRadius: 3,
+ px: 2.5,
+ boxShadow: 3,
+ }}
+ >
+ Novo movimento fixo
+
+
+
+ {erro && (
+
+ {erro}
+
+ )}
+
+ {sucesso && (
+
+ {sucesso}
+
+ )}
+
+
+
+
+
+ Movimentos encontrados
+
+
+ {summary.quantidade}
+
+
+
+
+
+
+
+ Entradas fixas
+
+
+ {formatarValor(summary.totalEntradas)}
+
+
+
+
+
+
+
+ Saídas fixas
+
+
+ {formatarValor(summary.totalSaidas + summary.totalSangrias)}
+
+
+
+
+
+
+
+ Habilitados
+
+
+ {summary.habilitados}
+
+
+
+
+
+
+
+
+
+
+
+ Filtros
+
+
+
+
+
+
+ setBusca(event.target.value)}
+ placeholder="Descrição, banco, centro..."
+ fullWidth
+ />
+
+ setMovimento(event.target.value as MovimentoFixoTipo | '')}
+ fullWidth
+ >
+
+
+
+
+
+
+
+
+ setIdBanco(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ disabled={loadingRefs}
+ fullWidth
+ >
+
+ {bancos.map((banco) => (
+
+ ))}
+
+
+
+ setIdCentroCusto(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ disabled={loadingRefs}
+ fullWidth
+ >
+
+ {centrosCusto.map((centro) => (
+
+ ))}
+
+
+
+ setHabilitado(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ fullWidth
+ >
+
+
+
+
+
+
+ setDiaInicio(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ inputProps={{ min: 1, max: 31 }}
+ fullWidth
+ />
+
+
+ setDiaFim(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ inputProps={{ min: 1, max: 31 }}
+ fullWidth
+ />
+
+ setOrderBy(event.target.value)}
+ fullWidth
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ setOrderDirection(event.target.value as 'ASC' | 'DESC')
+ }
+ fullWidth
+ >
+
+
+
+
+
+
+ }
+ onClick={limparFiltros}
+ >
+ Limpar
+
+
+ }
+ onClick={aplicarFiltros}
+ disabled={loading}
+ >
+ Aplicar filtros
+
+
+
+
+
+
+
+ {loading ? (
+
+
+ Carregando movimentos fixos...
+
+ ) : movimentosFixos.length === 0 ? (
+
+
+ Nenhum movimento fixo encontrado.
+
+
+ Ajuste os filtros ou cadastre um novo movimento fixo.
+
+
+ ) : isMobile ? (
+ }>
+ {movimentosFixos.map((item) => (
+
+
+
+
+
+ {item.descricao}
+
+
+
+ Vence todo dia {item.dia_vencimento}
+
+
+
+
+ {formatarValor(item.valor)}
+
+
+
+
+
+
+
+
+
+
+ Banco: {item.banco_descricao || '-'}
+
+
+
+ Centro: {item.centro_custo_descricao || '-'}
+
+
+
+
+
+ handleAlterarStatusMovimentoFixo(item)}
+ />
+
+
+
+ }
+ >
+ Editar
+
+
+ }
+ onClick={() => handleDeletarMovimentoFixo(item)}
+ >
+ Excluir
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
+
+ Descrição
+ Movimento
+ Valor
+ Dia venc.
+ Centro
+ Banco
+ Status
+ Cadastro
+ Atualização
+ Ações
+
+
+
+
+ {movimentosFixos.map((item) => (
+
+
+
+
+ {item.descricao}
+
+
+
+
+
+
+
+
+
+
+ {formatarValor(item.valor)}
+
+
+
+
+ {item.dia_vencimento}
+
+
+
+
+ {item.centro_custo_descricao || '-'}
+
+
+
+
+
+ {item.banco_descricao || '-'}
+
+
+
+
+
+
+
+
+ {formatarData(item.insert_date)}
+
+
+
+ {formatarData(item.update_date)}
+
+
+
+
+
+
+ handleAlterarStatusMovimentoFixo(item)}
+ />
+
+
+
+
+
+
+
+
+
+
+ handleDeletarMovimentoFixo(item)}
+ >
+
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+ {pagination.totalPages > 1 && (
+
+ {
+ setPage(novaPagina);
+ carregarMovimentosFixos(novaPagina);
+ }}
+ />
+
+ )}
+
+
+
+ Exibindo página {pagination.page} de {pagination.totalPages}, total de{' '}
+ {pagination.total} registro(s).
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/movimentosFixos/pages/NovoMovimentoFixoPage.tsx b/financeiro-web/src/features/movimentosFixos/pages/NovoMovimentoFixoPage.tsx
new file mode 100644
index 0000000..3870f2b
--- /dev/null
+++ b/financeiro-web/src/features/movimentosFixos/pages/NovoMovimentoFixoPage.tsx
@@ -0,0 +1,5 @@
+import { MovimentoFixoForm } from '../components/MovimentoFixoForm';
+
+export function NovoMovimentoFixoPage() {
+ return ;
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/movimentosFixos/services/movimentosFixosService.ts b/financeiro-web/src/features/movimentosFixos/services/movimentosFixosService.ts
new file mode 100644
index 0000000..56f8690
--- /dev/null
+++ b/financeiro-web/src/features/movimentosFixos/services/movimentosFixosService.ts
@@ -0,0 +1,75 @@
+import { api } from '../../../services/api';
+import type {
+ ApiResponse,
+ AtualizarMovimentoFixoRequest,
+ CriarMovimentoFixoRequest,
+ MovimentoFixo,
+ MovimentosFixosListResponse,
+} from '../types/movimentoFixoTypes';
+
+export type OrderDirection = 'ASC' | 'DESC';
+
+export type ListarMovimentosFixosParams = {
+ limite?: number;
+ offset?: number;
+ page?: number;
+ busca?: string;
+ movimento?: string;
+ idbancos?: number | '';
+ idcentrodecustos?: number | '';
+ habilitado?: number | '';
+ diaInicio?: number | '';
+ diaFim?: number | '';
+ orderBy?: string;
+ orderDirection?: OrderDirection;
+};
+
+export async function listarMovimentosFixos(
+ params?: ListarMovimentosFixosParams
+): Promise {
+ const response = await api.get('/movimentos-fixos', {
+ params,
+ });
+
+ return response.data;
+}
+
+export async function buscarMovimentoFixoPorId(id: number): Promise {
+ const response = await api.get>(`/movimentos-fixos/${id}`);
+ return response.data.data;
+}
+
+export async function criarMovimentoFixo(
+ data: CriarMovimentoFixoRequest
+): Promise {
+ const response = await api.post>('/movimentos-fixos', data);
+ return response.data.data;
+}
+
+export async function atualizarMovimentoFixo(
+ id: number,
+ data: AtualizarMovimentoFixoRequest
+): Promise {
+ const response = await api.put>(
+ `/movimentos-fixos/${id}`,
+ data
+ );
+
+ return response.data.data;
+}
+
+export async function alterarHabilitadoMovimentoFixo(
+ id: number,
+ habilitado: number
+): Promise {
+ const response = await api.patch>(
+ `/movimentos-fixos/${id}/habilitado`,
+ { habilitado }
+ );
+
+ return response.data.data;
+}
+
+export async function deletarMovimentoFixo(id: number): Promise {
+ await api.delete(`/movimentos-fixos/${id}`);
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/movimentosFixos/types/MovimentoFixoTypes.ts b/financeiro-web/src/features/movimentosFixos/types/MovimentoFixoTypes.ts
new file mode 100644
index 0000000..c67ff3d
--- /dev/null
+++ b/financeiro-web/src/features/movimentosFixos/types/MovimentoFixoTypes.ts
@@ -0,0 +1,61 @@
+export type MovimentoFixoTipo = 'Entrada' | 'Saida' | 'Sangria' | 'Estorno';
+
+export type MovimentoFixo = {
+ idmovimentosfixos: number;
+ movimento: MovimentoFixoTipo;
+ descricao: string;
+ valor: number;
+ dia_vencimento: number;
+ idcentrodecustos: number | null;
+ habilitado: number;
+ idbancos: number | null;
+ insert_date: string | null;
+ update_date: string | null;
+
+ banco_descricao?: string | null;
+ centro_custo_descricao?: string | null;
+};
+
+export type CriarMovimentoFixoRequest = {
+ movimento: MovimentoFixoTipo;
+ descricao: string;
+ valor: number;
+ dia_vencimento: number;
+ idcentrodecustos: number | null;
+ habilitado: number;
+ idbancos: number | null;
+};
+
+export type AtualizarMovimentoFixoRequest = CriarMovimentoFixoRequest;
+
+export type MovimentosFixosPagination = {
+ total: number;
+ limite: number;
+ offset: number;
+ page: number;
+ totalPages: number;
+};
+
+export type MovimentosFixosSummary = {
+ quantidade: number;
+ valorTotal: number;
+ totalEntradas: number;
+ totalSaidas: number;
+ totalSangrias: number;
+ totalEstornos: number;
+ habilitados: number;
+ desabilitados: number;
+};
+
+export type ApiResponse = {
+ ok: boolean;
+ message?: string;
+ data: T;
+};
+
+export type MovimentosFixosListResponse = {
+ ok: boolean;
+ data: MovimentoFixo[];
+ pagination: MovimentosFixosPagination;
+ summary: MovimentosFixosSummary;
+};
\ No newline at end of file
diff --git a/financeiro-web/src/features/usuarios/components/UsuarioForm.tsx b/financeiro-web/src/features/usuarios/components/UsuarioForm.tsx
new file mode 100644
index 0000000..5562982
--- /dev/null
+++ b/financeiro-web/src/features/usuarios/components/UsuarioForm.tsx
@@ -0,0 +1,461 @@
+import { useEffect, useState } from 'react';
+import type { FormEvent, ReactNode } from 'react';
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ CircularProgress,
+ Divider,
+ MenuItem,
+ Paper,
+ Stack,
+ TextField,
+ Typography,
+} from '@mui/material';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
+import SaveIcon from '@mui/icons-material/Save';
+import { useNavigate } from 'react-router-dom';
+import {
+ atualizarUsuario,
+ criarUsuario,
+} from '../services/usuariosService';
+import type {
+ CriarUsuarioRequest,
+ AtualizarUsuarioRequest,
+ Usuario,
+} from '../types/usuarioTypes';
+
+type UsuarioFormProps = {
+ mode: 'create' | 'edit';
+ initialData?: Usuario | null;
+};
+
+type FormSectionProps = {
+ title: string;
+ description?: string;
+ children: ReactNode;
+};
+
+function FormSection({ title, description, children }: FormSectionProps) {
+ return (
+
+
+
+ {title}
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+
+ {children}
+
+
+ );
+}
+
+const fieldSx = {
+ '& .MuiOutlinedInput-root': {
+ borderRadius: 2.5,
+ backgroundColor: '#FFFFFF',
+ minHeight: 48,
+ },
+ '& .MuiInputLabel-root': {
+ backgroundColor: '#FFFFFF',
+ paddingX: 0.5,
+ },
+};
+
+function textoOuNull(valor: string): string | null {
+ const texto = valor.trim();
+ return texto || null;
+}
+
+export function UsuarioForm({ mode, initialData }: UsuarioFormProps) {
+ const navigate = useNavigate();
+
+ const isEdit = mode === 'edit';
+
+ const [saving, setSaving] = useState(false);
+ const [erro, setErro] = useState('');
+ const [sucesso, setSucesso] = useState('');
+
+ const [nome, setNome] = useState('');
+ const [senha, setSenha] = useState('');
+ const [confirmarSenha, setConfirmarSenha] = useState('');
+ const [anotacoes, setAnotacoes] = useState('');
+ const [habilitado, setHabilitado] = useState('1');
+
+ const titulo = isEdit ? 'Editar usuário' : 'Novo usuário';
+ const subtitulo = isEdit
+ ? 'Atualize os dados do usuário selecionado.'
+ : 'Cadastre um novo usuário para acessar o sistema.';
+
+ useEffect(() => {
+ if (!initialData) return;
+
+ setNome(initialData.nome || '');
+ setAnotacoes(initialData.anotacoes || '');
+ setHabilitado(String(initialData.habilitado ?? 1));
+ setSenha('');
+ setConfirmarSenha('');
+ }, [initialData]);
+
+ function limparFormulario() {
+ setNome('');
+ setSenha('');
+ setConfirmarSenha('');
+ setAnotacoes('');
+ setHabilitado('1');
+ }
+
+ async function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+
+ if (!nome.trim()) {
+ setErro('Informe o nome.');
+ return;
+ }
+
+ if (!isEdit && !senha.trim()) {
+ setErro('Informe a senha.');
+ return;
+ }
+
+ if (senha.trim() && senha.length < 3) {
+ setErro('A senha deve ter pelo menos 3 caracteres.');
+ return;
+ }
+
+ if (senha.trim() && senha.length > 45) {
+ setErro('A senha deve ter no máximo 45 caracteres.');
+ return;
+ }
+
+ if (senha.trim() || confirmarSenha.trim()) {
+ if (senha !== confirmarSenha) {
+ setErro('A confirmação de senha não confere.');
+ return;
+ }
+ }
+
+ try {
+ setSaving(true);
+ setErro('');
+ setSucesso('');
+
+ if (isEdit) {
+ if (!initialData?.idusuarios) {
+ setErro('Usuário inválido para edição.');
+ return;
+ }
+
+ const payload: AtualizarUsuarioRequest = {
+ nome: nome.trim(),
+ senha: senha.trim() ? senha.trim() : null,
+ anotacoes: textoOuNull(anotacoes),
+ habilitado: Number(habilitado || 1),
+ };
+
+ await atualizarUsuario(initialData.idusuarios, payload);
+ setSucesso('Usuário atualizado com sucesso.');
+ setSenha('');
+ setConfirmarSenha('');
+ } else {
+ const payload: CriarUsuarioRequest = {
+ nome: nome.trim(),
+ senha: senha.trim(),
+ anotacoes: textoOuNull(anotacoes),
+ habilitado: Number(habilitado || 1),
+ };
+
+ await criarUsuario(payload);
+ setSucesso('Usuário cadastrado com sucesso.');
+ limparFormulario();
+ }
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível salvar o usuário.';
+
+ setErro(message);
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ return (
+
+
+
+ }
+ label={isEdit ? 'Edição de usuário' : 'Cadastro de usuário'}
+ color="primary"
+ variant="outlined"
+ sx={{ marginBottom: 1.25, fontWeight: 700 }}
+ />
+
+
+ {titulo}
+
+
+
+ {subtitulo}
+
+
+
+ }
+ onClick={() => navigate('/usuarios')}
+ sx={{
+ minHeight: 48,
+ borderRadius: 3,
+ px: 2.5,
+ }}
+ >
+ Voltar para lista
+
+
+
+ {erro && (
+
+ {erro}
+
+ )}
+
+ {sucesso && (
+
+ {sucesso}
+
+ )}
+
+
+
+
+
+
+ setNome(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setHabilitado(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ >
+
+
+
+
+ setAnotacoes(event.target.value)}
+ multiline
+ minRows={4}
+ fullWidth
+ sx={{
+ ...fieldSx,
+ gridColumn: { xs: 'auto', md: '1 / -1' },
+ }}
+ />
+
+
+
+ setSenha(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+ setConfirmarSenha(event.target.value)}
+ fullWidth
+ sx={fieldSx}
+ />
+
+
+
+
+
+
+ :
+ }
+ sx={{
+ minHeight: 46,
+ px: 3,
+ boxShadow: 3,
+ }}
+ >
+ {saving
+ ? 'Salvando...'
+ : isEdit
+ ? 'Atualizar usuário'
+ : 'Salvar usuário'}
+
+
+
+
+
+
+
+
+ Resumo
+
+
+
+ Prévia rápida do usuário antes de salvar.
+
+
+
+
+
+
+
+ Nome
+
+
+ {nome || 'Sem nome'}
+
+
+
+
+
+ Status
+
+
+ {Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado'}
+
+
+
+
+
+ Senha
+
+
+ {isEdit
+ ? senha
+ ? 'Será alterada'
+ : 'Sem alteração'
+ : senha
+ ? 'Definida'
+ : 'Pendente'}
+
+
+
+
+
+ Anotações
+
+
+ {anotacoes || '-'}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/usuarios/pages/EditarUsuarioPage.tsx b/financeiro-web/src/features/usuarios/pages/EditarUsuarioPage.tsx
new file mode 100644
index 0000000..e30730f
--- /dev/null
+++ b/financeiro-web/src/features/usuarios/pages/EditarUsuarioPage.tsx
@@ -0,0 +1,62 @@
+import { useEffect, useState } from 'react';
+import { Alert, Box, CircularProgress, Typography } from '@mui/material';
+import { useParams } from 'react-router-dom';
+import { UsuarioForm } from '../components/UsuarioForm';
+import { buscarUsuarioPorId } from '../services/usuariosService';
+import type { Usuario } from '../types/usuarioTypes';
+
+export function EditarUsuarioPage() {
+ const { id } = useParams();
+
+ const [usuario, setUsuario] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [erro, setErro] = useState('');
+
+ useEffect(() => {
+ async function carregarUsuario() {
+ try {
+ setLoading(true);
+ setErro('');
+
+ const usuarioId = Number(id);
+
+ if (!usuarioId) {
+ setErro('ID do usuário inválido.');
+ return;
+ }
+
+ const data = await buscarUsuarioPorId(usuarioId);
+ setUsuario(data);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar o usuário.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ carregarUsuario();
+ }, [id]);
+
+ if (loading) {
+ return (
+
+
+ Carregando usuário...
+
+ );
+ }
+
+ if (erro) {
+ return (
+
+ {erro}
+
+ );
+ }
+
+ return ;
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/usuarios/pages/NovoUsuarioPage.tsx b/financeiro-web/src/features/usuarios/pages/NovoUsuarioPage.tsx
new file mode 100644
index 0000000..5083d85
--- /dev/null
+++ b/financeiro-web/src/features/usuarios/pages/NovoUsuarioPage.tsx
@@ -0,0 +1,5 @@
+import { UsuarioForm } from '../components/UsuarioForm';
+
+export function NovoUsuarioPage() {
+ return ;
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/usuarios/pages/UsuariosPage.tsx b/financeiro-web/src/features/usuarios/pages/UsuariosPage.tsx
new file mode 100644
index 0000000..c63445e
--- /dev/null
+++ b/financeiro-web/src/features/usuarios/pages/UsuariosPage.tsx
@@ -0,0 +1,685 @@
+import { useEffect, useMemo, useState } from 'react';
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ CircularProgress,
+ Divider,
+ IconButton,
+ MenuItem,
+ Pagination,
+ Paper,
+ Stack,
+ Switch,
+ TextField,
+ Tooltip,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ useMediaQuery,
+ useTheme,
+} from '@mui/material';
+import AddIcon from '@mui/icons-material/Add';
+import DeleteIcon from '@mui/icons-material/Delete';
+import EditIcon from '@mui/icons-material/Edit';
+import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
+import FilterAltIcon from '@mui/icons-material/FilterAlt';
+import RestartAltIcon from '@mui/icons-material/RestartAlt';
+import SearchIcon from '@mui/icons-material/Search';
+import { Link as RouterLink } from 'react-router-dom';
+import {
+ alterarHabilitadoUsuario,
+ deletarUsuario,
+ listarUsuarios,
+ type ListarUsuariosParams,
+} from '../services/usuariosService';
+import type {
+ Usuario,
+ UsuariosPagination,
+ UsuariosSummary,
+} from '../types/usuarioTypes';
+
+const LIMITE_PADRAO = 20;
+
+function formatarData(data: string | null) {
+ if (!data) return '-';
+
+ return new Date(`${String(data).slice(0, 10)}T00:00:00`).toLocaleDateString('pt-BR');
+}
+
+function statusLabel(habilitado: number) {
+ return Number(habilitado) === 1 ? 'Habilitado' : 'Desabilitado';
+}
+
+function statusColor(habilitado: number) {
+ return Number(habilitado) === 1 ? 'success' : 'default';
+}
+
+export function UsuariosPage() {
+ const [usuarios, setUsuarios] = useState([]);
+ const [pagination, setPagination] = useState({
+ total: 0,
+ limite: LIMITE_PADRAO,
+ offset: 0,
+ page: 1,
+ totalPages: 1,
+ });
+ const [summary, setSummary] = useState({
+ quantidade: 0,
+ habilitados: 0,
+ desabilitados: 0,
+ });
+
+ const [alterandoStatusId, setAlterandoStatusId] = useState(null);
+
+ const [loading, setLoading] = useState(true);
+ const [erro, setErro] = useState('');
+ const [sucesso, setSucesso] = useState('');
+
+ const [page, setPage] = useState(1);
+ const [busca, setBusca] = useState('');
+ const [habilitado, setHabilitado] = useState('');
+ const [orderBy, setOrderBy] = useState('nome');
+ const [orderDirection, setOrderDirection] = useState<'ASC' | 'DESC'>('ASC');
+
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
+
+ const filtrosAtivos = useMemo(() => {
+ let count = 0;
+
+ if (busca.trim()) count += 1;
+ if (habilitado !== '') count += 1;
+
+ return count;
+ }, [busca, habilitado]);
+
+ async function carregarUsuarios(pageToLoad = page) {
+ try {
+ setLoading(true);
+ setErro('');
+
+ const params: ListarUsuariosParams = {
+ limite: LIMITE_PADRAO,
+ page: pageToLoad,
+ busca: busca.trim() || undefined,
+ habilitado,
+ orderBy,
+ orderDirection,
+ };
+
+ const response = await listarUsuarios(params);
+
+ setUsuarios(response.data);
+ setPagination(response.pagination);
+ setSummary(response.summary);
+ setPage(response.pagination.page);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível carregar os usuários.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ function aplicarFiltros() {
+ setPage(1);
+ carregarUsuarios(1);
+ }
+
+ function limparFiltros() {
+ setBusca('');
+ setHabilitado('');
+ setOrderBy('nome');
+ setOrderDirection('ASC');
+
+ setTimeout(() => {
+ setPage(1);
+ carregarUsuarios(1);
+ }, 0);
+ }
+
+ async function handleDeletarUsuario(item: Usuario) {
+ const confirmou = window.confirm(
+ `Deseja realmente excluir "${item.nome}"?`
+ );
+
+ if (!confirmou) return;
+
+ try {
+ setLoading(true);
+ setErro('');
+ setSucesso('');
+
+ await deletarUsuario(item.idusuarios);
+
+ setSucesso('Usuário excluído com sucesso.');
+ await carregarUsuarios(page);
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível excluir o usuário.';
+
+ setErro(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function handleAlterarStatusUsuario(item: Usuario) {
+ const novoStatus = Number(item.habilitado) === 1 ? 0 : 1;
+
+ try {
+ setAlterandoStatusId(item.idusuarios);
+ setErro('');
+ setSucesso('');
+
+ const usuarioAtualizado = await alterarHabilitadoUsuario(
+ item.idusuarios,
+ novoStatus
+ );
+
+ setUsuarios((listaAtual) =>
+ listaAtual.map((usuario) =>
+ usuario.idusuarios === item.idusuarios ? usuarioAtualizado : usuario
+ )
+ );
+
+ setSucesso(
+ novoStatus === 1
+ ? 'Usuário habilitado com sucesso.'
+ : 'Usuário desabilitado com sucesso.'
+ );
+ } catch (error: any) {
+ const message =
+ error?.response?.data?.message ||
+ 'Não foi possível alterar o status do usuário.';
+
+ setErro(message);
+ } finally {
+ setAlterandoStatusId(null);
+ }
+ }
+
+ useEffect(() => {
+ carregarUsuarios(1);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+
+ }
+ label="Controle de acesso"
+ color="primary"
+ variant="outlined"
+ sx={{ marginBottom: 1.25, fontWeight: 700 }}
+ />
+
+
+ Usuários
+
+
+
+ Cadastre e gerencie os usuários que acessam o sistema.
+
+
+
+ }
+ sx={{
+ minHeight: 48,
+ borderRadius: 3,
+ px: 2.5,
+ boxShadow: 3,
+ }}
+ >
+ Novo usuário
+
+
+
+ {erro && (
+
+ {erro}
+
+ )}
+
+ {sucesso && (
+
+ {sucesso}
+
+ )}
+
+
+
+
+
+ Usuários encontrados
+
+
+ {summary.quantidade}
+
+
+
+
+
+
+
+ Habilitados
+
+
+ {summary.habilitados}
+
+
+
+
+
+
+
+ Desabilitados
+
+
+ {summary.desabilitados}
+
+
+
+
+
+
+
+
+
+
+
+ Filtros
+
+
+
+
+
+
+ setBusca(event.target.value)}
+ placeholder="Nome ou anotações..."
+ fullWidth
+ />
+
+
+ setHabilitado(event.target.value === '' ? '' : Number(event.target.value))
+ }
+ fullWidth
+ >
+
+
+
+
+
+ setOrderBy(event.target.value)}
+ fullWidth
+ >
+
+
+
+
+
+
+
+ setOrderDirection(event.target.value as 'ASC' | 'DESC')
+ }
+ fullWidth
+ >
+
+
+
+
+
+
+ }
+ onClick={limparFiltros}
+ >
+ Limpar
+
+
+ }
+ onClick={aplicarFiltros}
+ disabled={loading}
+ >
+ Aplicar filtros
+
+
+
+
+
+
+
+ {loading ? (
+
+
+ Carregando usuários...
+
+ ) : usuarios.length === 0 ? (
+
+
+ Nenhum usuário encontrado.
+
+
+ Ajuste os filtros ou cadastre um novo usuário.
+
+
+ ) : isMobile ? (
+ }>
+ {usuarios.map((item) => (
+
+
+
+
+
+ {item.nome}
+
+
+
+ Cadastro: {formatarData(item.insert_date)}
+
+
+
+
+
+
+
+
+
+ Anotações: {item.anotacoes || '-'}
+
+
+
+
+
+ handleAlterarStatusUsuario(item)}
+ />
+
+
+
+ }
+ >
+ Editar
+
+
+ }
+ onClick={() => handleDeletarUsuario(item)}
+ >
+ Excluir
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
+
+ Nome
+ Anotações
+ Status
+ Cadastro
+ Atualização
+ Ações
+
+
+
+
+ {usuarios.map((item) => (
+
+
+
+
+ {item.nome}
+
+
+
+
+
+
+
+ {item.anotacoes || '-'}
+
+
+
+
+
+
+
+
+
+ {formatarData(item.insert_date)}
+
+
+
+ {formatarData(item.update_date)}
+
+
+
+
+
+
+ handleAlterarStatusUsuario(item)}
+ />
+
+
+
+
+
+
+
+
+
+
+ handleDeletarUsuario(item)}
+ >
+
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+ {pagination.totalPages > 1 && (
+
+ {
+ setPage(novaPagina);
+ carregarUsuarios(novaPagina);
+ }}
+ />
+
+ )}
+
+
+
+ Exibindo página {pagination.page} de {pagination.totalPages}, total de{' '}
+ {pagination.total} registro(s).
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/usuarios/services/usuariosService.ts b/financeiro-web/src/features/usuarios/services/usuariosService.ts
new file mode 100644
index 0000000..a3aaee7
--- /dev/null
+++ b/financeiro-web/src/features/usuarios/services/usuariosService.ts
@@ -0,0 +1,66 @@
+import { api } from '../../../services/api';
+import type {
+ ApiResponse,
+ AtualizarUsuarioRequest,
+ CriarUsuarioRequest,
+ Usuario,
+ UsuariosListResponse,
+} from '../types/usuarioTypes';
+
+export type OrderDirection = 'ASC' | 'DESC';
+
+export type ListarUsuariosParams = {
+ limite?: number;
+ offset?: number;
+ page?: number;
+ busca?: string;
+ habilitado?: number | '';
+ orderBy?: string;
+ orderDirection?: OrderDirection;
+};
+
+export async function listarUsuarios(
+ params?: ListarUsuariosParams
+): Promise {
+ const response = await api.get('/usuarios', {
+ params,
+ });
+
+ return response.data;
+}
+
+export async function buscarUsuarioPorId(id: number): Promise {
+ const response = await api.get>(`/usuarios/${id}`);
+ return response.data.data;
+}
+
+export async function criarUsuario(
+ data: CriarUsuarioRequest
+): Promise {
+ const response = await api.post>('/usuarios', data);
+ return response.data.data;
+}
+
+export async function atualizarUsuario(
+ id: number,
+ data: AtualizarUsuarioRequest
+): Promise {
+ const response = await api.put>(`/usuarios/${id}`, data);
+ return response.data.data;
+}
+
+export async function alterarHabilitadoUsuario(
+ id: number,
+ habilitado: number
+): Promise {
+ const response = await api.patch>(
+ `/usuarios/${id}/habilitado`,
+ { habilitado }
+ );
+
+ return response.data.data;
+}
+
+export async function deletarUsuario(id: number): Promise {
+ await api.delete(`/usuarios/${id}`);
+}
\ No newline at end of file
diff --git a/financeiro-web/src/features/usuarios/types/usuarioTypes.ts b/financeiro-web/src/features/usuarios/types/usuarioTypes.ts
new file mode 100644
index 0000000..1636627
--- /dev/null
+++ b/financeiro-web/src/features/usuarios/types/usuarioTypes.ts
@@ -0,0 +1,49 @@
+export type Usuario = {
+ idusuarios: number;
+ nome: string;
+ anotacoes: string | null;
+ habilitado: number;
+ insert_date: string | null;
+ update_date: string | null;
+};
+
+export type CriarUsuarioRequest = {
+ nome: string;
+ senha: string;
+ anotacoes: string | null;
+ habilitado: number;
+};
+
+export type AtualizarUsuarioRequest = {
+ nome: string;
+ senha?: string | null;
+ anotacoes: string | null;
+ habilitado: number;
+};
+
+export type UsuariosPagination = {
+ total: number;
+ limite: number;
+ offset: number;
+ page: number;
+ totalPages: number;
+};
+
+export type UsuariosSummary = {
+ quantidade: number;
+ habilitados: number;
+ desabilitados: number;
+};
+
+export type ApiResponse = {
+ ok: boolean;
+ message?: string;
+ data: T;
+};
+
+export type UsuariosListResponse = {
+ ok: boolean;
+ data: Usuario[];
+ pagination: UsuariosPagination;
+ summary: UsuariosSummary;
+};
\ No newline at end of file