diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3a7d783..6b366e8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,23 @@ -#### O que essa PR faz? - Descreva tudo o que essa PR faz e suas alterações +## Descrição -#### Tarefas desenvolvidas: -- [ ] tarefa 1 -- [ ] tarefa 2 -- [ ] tarefa 3 +Inclua um resumo da alteração e qual problema foi corrigido. Inclua também motivação e contexto relevantes. Liste todas as dependências necessárias para essa alteração. -#### Link relacionada a issue: [Link da issue]() +Correções # [Link da issue]() +## Tipo de alteração +Marque com um 'x' as alterações relevantes. -#### Observações adicionais: +Exclua as opções que não são relevantes. + +- [ ] Correção de bug (mudança ininterrupta que corrige um problema) +- [ ] Novo recurso (mudança ininterrupta que adiciona funcionalidade) +- [ ] Mudança de última hora (correção ou recurso que faria com que a funcionalidade existente não funcionasse conforme o esperado) +- [ ] Esta alteração requer uma atualização da documentação + +## Lista de controle: + +- [ ] Meu código segue as diretrizes de estilo deste projeto +- [ ] Realizei uma auto-revisão do meu próprio código +- [ ] Eu comentei meu código, principalmente em áreas difíceis de entender +- [ ] Fiz as alterações correspondentes na documentação +- [ ] Minhas alterações não geram novos avisos +- [ ] Verifiquei meu código e corrigi qualquer erro de ortografia diff --git a/.gitignore b/.gitignore index 7be7da1..98aa75a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ Back/node_modules Back/package-lock.json Front/node_modules Front/package-lock.json - +Back/coverage/ # Logs Front/logs Front/*.log diff --git a/Back/jest.config.js b/Back/jest.config.js new file mode 100644 index 0000000..8f9a124 --- /dev/null +++ b/Back/jest.config.js @@ -0,0 +1,194 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/tmp/jest_rs", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: undefined, + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state before every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state and implementation before every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + // testEnvironment: "jest-environment-node", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + testMatch: [ + "**/__tests__/**/*.test.js?(x)", + ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; diff --git a/Back/package.json b/Back/package.json old mode 100644 new mode 100755 index 5ba83b8..090fa8d --- a/Back/package.json +++ b/Back/package.json @@ -7,8 +7,8 @@ "doc": "docs" }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "nodemon src/server.js" + "test": "jest", + "start": "nodemon src/server.js --ignore tests" }, "repository": { "type": "git", @@ -32,5 +32,9 @@ "mysql2": "^2.3.3", "nodemon": "^2.0.19", "sequelize": "^6.21.3" + }, + "devDependencies": { + "jest": "^28.1.3", + "supertest": "^6.2.4" } } diff --git a/Back/src/__tests__/sum.test.js b/Back/src/__tests__/sum.test.js new file mode 100644 index 0000000..ea6e038 --- /dev/null +++ b/Back/src/__tests__/sum.test.js @@ -0,0 +1,13 @@ +describe('sum test', () => { + it('should sum to numbers', () => { + const a = 1, b = 2; + const c = a + b; + expect(c).toBe(3); + }); + + it('should sum to numbers',() => { + const a = 1, b = 2; + const c = a + b; + expect(c).toBe(3); + }); + }); \ No newline at end of file diff --git a/Back/src/app.js b/Back/src/app.js old mode 100644 new mode 100755 index bc7c0e3..2ad58eb --- a/Back/src/app.js +++ b/Back/src/app.js @@ -3,7 +3,8 @@ const app = express(); const bodyParser = require("body-parser"); const cors = require("cors"); -const userControler = require('../src/controller/UserController.js'); +const userController = require('../src/controller/UserController.js'); +const ativoController = require('./controller/AtivoController.js'); app.use(express.json()); @@ -17,6 +18,7 @@ app.get("/", (req, res) => { }); }); -app.use('/usuario', userControler); +app.use('/usuario', userController); +app.use('/ativo', ativoController); module.exports = app; \ No newline at end of file diff --git a/Back/src/controller/AtivoController.js b/Back/src/controller/AtivoController.js new file mode 100755 index 0000000..70f1e3c --- /dev/null +++ b/Back/src/controller/AtivoController.js @@ -0,0 +1,291 @@ +const express = require("express"); +const router = express.Router(); +const Axios = require("axios"); +const sequelize = require('sequelize'); + +const auth = require("../middleware/auth"); +const Ativo = require("../models/Ativo"); +const AtivosB3 = require("../models/AtivosB3"); + +const ativosB3Util = require("../util/AtivosB3Util"); + +router.post("/cadastrar", auth, async (req, res) => { + const novo_ativo = { + id_usuario: req.usuario.id, + nomeAtivo: req.body.nomeAtivo, + sigla: req.body.sigla, + preco: req.body.preco, + quantidade: req.body.quantidade, + data: req.body.data, + execucao: "compra" + }; + + await Ativo.create(novo_ativo) + .then(() => { + return res.json({ + erro: false, + message: "Compra de ativo cadastrada com sucesso!" + }) + }).catch((error) => { + console.log(error); + return res.status(400).json({ + erro: true, + message: error.message + }) + }); +}); + +router.post("/vender", auth, async (req,res) => { + + // filtro que soma a quantidade de compra do ativo + const ativo_comprado = await Ativo.findAll({ + attributes: [ + "id_usuario", + "nomeAtivo", + "execucao", + [sequelize.fn("sum", sequelize.col("quantidade")), "total"]], + group : ['id_usuario', 'nomeAtivo', 'execucao'], + raw: true, + where: { + "id_usuario" : req.usuario.id, + "nomeAtivo" : req.body.nomeAtivo, + "execucao" : "compra" + }, + }) + + // filtro que soma a quantidade de venda do ativo + const ativo_vendido = await Ativo.findAll({ + attributes: [ + "id_usuario", + "nomeAtivo", + "execucao", + [sequelize.fn("sum", sequelize.col("quantidade")), "total"]], + group : ['id_usuario', 'nomeAtivo', 'execucao'], + raw: true, + where: { + "id_usuario" : req.usuario.id, + "nomeAtivo" : req.body.nomeAtivo, + "execucao" : "venda" + }, + }) + + + if (ativo_comprado[0] == null) { + return res.status(400).json({ + erro: true, + message: "Não existe ativo para vender" + }) + } else { + if (ativo_vendido[0] == null) { + var totalQuantidade = ativo_comprado[0].total - 0; + } else { + var totalQuantidade = ativo_comprado[0].total - ativo_vendido[0].total; + } + } + + + + + const nova_venda = { + id_usuario: req.usuario.id, + nomeAtivo: req.body.nomeAtivo, + sigla: req.body.sigla, + preco: req.body.preco, + quantidade: req.body.quantidade, + data: req.body.data, + execucao: "venda" + }; + + if (req.body.quantidade <= totalQuantidade) { + + await Ativo.create(nova_venda) + .then(() => { + return res.json({ + erro: false, + message: "Ativo vendido com sucesso!" + }) + }).catch((error) => { + console.log(error); + return res.status(400).json({ + erro: true, + message: "Erro na venda do ativo" + }) + }); + } else { + return res.status(400).json({ + erro: true, + message: "Quantidade maior do que disponivel para venda!" + }) + } + +}); + +// Rota que envia o historico da acao do usuario +router.post("/historico", auth, async (req,res) => { + + const dadoHistorico = await Ativo.findAll({ + attributes: [ + "id", + "nomeAtivo", + "sigla", + "preco", + "quantidade", + "data", + "execucao", + ], + raw: true, + where: { + "id_usuario": req.usuario.id + }, + }) + return res.json({ + historico: dadoHistorico + }) + +}) + +// Rota que envia o patrimonio do usuario +router.post("/patrimonio", auth, async (req, res) => { + await Ativo.findAll({ + attributes: [ + [sequelize.fn('DISTINCT', sequelize.col('sigla')), 'sigla'], + ], + where: { + "id_usuario": req.usuario.id + }, + }).then(async (ativos) => { + let siglas = [] + for (let ativo of ativos) { + siglas.push(ativo.dataValues.sigla); + } + + const patrimonio = await ativosB3Util.calculaPatrimonio(siglas, req.usuario.id); + + return res.json({ + erro: false, + ativos: patrimonio + }); + + }).catch((error) => { + console.log(error); + return res.status(400).json({ + erro: true, + ativos: [] + }) + }); +}); + +// Rota que envia a Rentabilidade do usuario +router.post("/rentabilidade", auth, async (req, res) => { + const sc = new sequelize("usuario", "root", "12345678", { + host: 'localhost', + dialect: 'mysql' + }); + + await sc.query(`SELECT DISTINCT(SUBSTRING_INDEX(data, '-', 2)) as data FROM ativos WHERE id_usuario = ${req.usuario.id}`).then(async (results) => { + let datas = [] + for (let data of results[0]) { + datas.push(data.data); + } + const rentabilidade = await ativosB3Util.calculaRentabilidade(datas, req.usuario.id); + + return res.json({ + erro: false, + rentabilidade: rentabilidade + }); + + }).catch((error) => { + console.log(error); + return res.status(400).json({ + erro: true, + ativos: [] + }) + }); +}); + +router.post("/editar", auth, async (req,res) => { + const { id } = req.body; + const { data } = req.body; + const { preco } = req.body; + const { quantidade } = req.body; + + try { + if (data !== null) { + await Ativo.update( + { data: data }, + { where: {id: id}} + ); + } + + if (preco !== null) { + await Ativo.update( + { preco: preco }, + { where: {id: id}} + ) + } + + if (quantidade !== null) { + await Ativo.update( + { quantidade: quantidade }, + { where: {id: id}} + ); + } + + return res.json({ + erro: false, + message: "Ativo editado com sucesso!" + }); + + } catch (error) { + return res.status(400).json({ + erro: true, + message: error.message + }); + } +}); + +router.post("/excluir", auth, async (req,res) => { + const { id } = req.body; + + await Ativo.destroy({ + where: { id: id } + }).then(() => { + return res.json({ + erro: false, + message: `Ativo ${id} excluido com sucesso!` + }) + }).catch((error) => { + console.log(error); + return res.status(400).json({ + erro: true, + message: error.message + }) + }); +}); + +router.get("/buscaativos", async (req,res) => { + var lista = []; + var linha = []; + await AtivosB3.findAll(). + then(function(response) { + for (let ativo of response) { + const { nome_empresa } = ativo; + const { codigo_acao } = ativo; + linha = {nome: nome_empresa, sigla: codigo_acao}; + lista.push(linha); + } + + return res.json({ + erro: false, + lista: lista + }); + + }).catch(() => { + return res.status(400).json({ + erro: true, + lista: lista + }); + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/Back/src/controller/UserController.js b/Back/src/controller/UserController.js old mode 100644 new mode 100755 index 6f64a25..81b016a --- a/Back/src/controller/UserController.js +++ b/Back/src/controller/UserController.js @@ -1,12 +1,13 @@ const express = require("express"); +const router = express.Router(); const bcrypt = require("bcryptjs"); const jwt = require('jsonwebtoken'); const User = require("../models/User"); -const app = express(); +const AtivosB3 = require("../util/AtivosB3Util"); // funciona -app.post("/cadastrar", async (req, res) => { +router.post("/cadastrar", async (req, res) => { const salt = await bcrypt.genSalt(10); const novo_usuario = { @@ -41,8 +42,8 @@ app.post("/cadastrar", async (req, res) => { } }) -// funciona -app.post("/login", async (req, res) => { +//funciona +router.post("/login", async (req, res) => { const usuario = await User.findOne({ attributes: ["id", "email", "senha"], where: { @@ -65,9 +66,26 @@ app.post("/login", async (req, res) => { const token = jwt.sign({id: usuario.id}, "INVEXTGFOURD62ST92Y7A6V7K5C6W9ZU6W8KS3", { // expiresIn: 600 //10 min // expiresIn: '7d' // 7 dia - expiresIn: 1800 //30 min + // expiresIn: 1800 //30 min }); + // Quando o usuario fizer login, o banco de dados + // com todos os ativos eh atualizado + //await AtivosB3.updateAtivosB3(). + /*then(async () => { + return res.json({ + erro: false, + message: "Login realizado com sucesso!", + token + }); + }).catch(async () => { + return res.status(400).json({ + erro: true, + message: "Login nao realizado com sucesso!!" + }); + });*/ + + AtivosB3.updateAtivosB3(); return res.json({ erro: false, message: "Login realizado com sucesso!", @@ -78,13 +96,13 @@ app.post("/login", async (req, res) => { }); // nao funciona -> somente o usuario logado deve conseguir atualizar suas infos -app.post("/atualizar", async (req, res) => { +router.post("/atualizar", async (req, res) => { }) // nao funciona -> somente o usuario logado deve conseguir deletar sua conta -app.post("/deletar", async (req, res) => { +router.post("/deletar", async (req, res) => { }) -module.exports = app; \ No newline at end of file +module.exports = router; \ No newline at end of file diff --git a/Back/src/middleware/auth.js b/Back/src/middleware/auth.js new file mode 100644 index 0000000..058fc4f --- /dev/null +++ b/Back/src/middleware/auth.js @@ -0,0 +1,15 @@ +const jwt = require('jsonwebtoken'); + +module.exports = (req, res, next) => { + try { + const token = req.body.token; + const decode = jwt.verify(token, "INVEXTGFOURD62ST92Y7A6V7K5C6W9ZU6W8KS3"); + req.usuario = decode; + next(); + } catch (error) { + return res.status(401).json({ + erro: true, + message: "Falha na autenticação!" + }); + } +} \ No newline at end of file diff --git a/Back/src/models/Ativo.js b/Back/src/models/Ativo.js new file mode 100755 index 0000000..0e0e290 --- /dev/null +++ b/Back/src/models/Ativo.js @@ -0,0 +1,72 @@ +const Sequelize = require('sequelize'); +const db = require('./db'); + +const Ativo = db.define('ativo', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + allowNull: false, + primaryKey: true + }, + id_usuario: { + type: Sequelize.INTEGER, + allowNull: false, + primaryKey: true + }, + nomeAtivo: { + type: Sequelize.STRING, + allowNull: false, + validate: { + notEmpty: { + msg: "Esse campo nao pode ser vazio" + } + } + }, + sigla: { + type: Sequelize.STRING, + allowNull: false, + validate: { + notEmpty: { + msg: "Esse campo nao pode ser vazio" + } + } + }, + preco: { + type: Sequelize.FLOAT, + allowNull: false, + validate: { + notEmpty: { + msg: "Esse campo nao pode ser vazio" + } + } + }, + quantidade: { + type: Sequelize.INTEGER, + allowNull: false, + validate: { + notEmpty: { + msg: "Esse campo nao pode ser vazio" + } + } + }, + data: { + type: Sequelize.STRING, + allowNull: false, + validate: { + notEmpty: { + msg: "Esse campo nao pode ser vazio" + } + } + }, + execucao: { + type: Sequelize.STRING, + allowNull: false + } +}); + +Ativo.sync() + +// verifica se existe alteração na model que não está no BD +// Acao.sync({ alter: true }) + +module.exports = Ativo; \ No newline at end of file diff --git a/Back/src/models/AtivosB3.js b/Back/src/models/AtivosB3.js new file mode 100755 index 0000000..0dae2e1 --- /dev/null +++ b/Back/src/models/AtivosB3.js @@ -0,0 +1,31 @@ +const Sequelize = require('sequelize'); +const db = require('./db'); + +const AtivosB3 = db.define('b3_ativo', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + allowNull: false, + primaryKey: true + }, + data_pregao: { + type: Sequelize.STRING, + allowNull: false, + }, + codigo_acao: { + type: Sequelize.STRING, + allowNull: false, + }, + nome_empresa: { + type: Sequelize.STRING, + allowNull: false, + }, + valor_fechamento: { + type: Sequelize.FLOAT, + allowNull: false, + }, +}); + +AtivosB3.sync() + +module.exports = AtivosB3; \ No newline at end of file diff --git a/Back/src/models/User.js b/Back/src/models/User.js old mode 100644 new mode 100755 diff --git a/Back/src/models/db.js b/Back/src/models/db.js old mode 100644 new mode 100755 diff --git a/Back/src/server.js b/Back/src/server.js old mode 100644 new mode 100755 diff --git a/Back/src/util/AtivosB3Util.js b/Back/src/util/AtivosB3Util.js new file mode 100644 index 0000000..fe55e7a --- /dev/null +++ b/Back/src/util/AtivosB3Util.js @@ -0,0 +1,204 @@ +const Axios = require("axios"); +const AtivosB3 = require("../models/AtivosB3") +const Ativo = require("../models/Ativo"); +const sequelize = require('sequelize'); + +exports.updateAtivosB3 = async function () { + let dt_ultimo_pregao; + + // Pega a data do ultimo pregao dispnivel na API da B3 + await Axios.get("https://api-cotacao-b3.labdo.it/api/sysinfo", { + }).then(function(res){ + const { data } = res; + dt_ultimo_pregao = data.dt_ultimo_pregao; + }).catch(function(err){ + console.log(err); + }); + + // Seleciona todas as informacoes disponiveis sobre todos os ativos nesse dia + // e atualiza o banco de dados com as novas contacoes do dia + await Axios.get(`https://api-cotacao-b3.labdo.it/api/cotacao/dt/${dt_ultimo_pregao}/02`, { + }).then(async function(res){ + const { data } = res; + var lista = []; + // deleta todos os registros do banco primeiro + await AtivosB3.destroy({ + where: {}, + truncate: true + }).then(async () => { + for (let ativo of data) { + const novo_ativo = { + data_pregao: ativo.dt_pregao, + codigo_acao: ativo.cd_acao, + nome_empresa: ativo.nm_empresa_rdz, + valor_fechamento: ativo.vl_fechamento + } + lista.push(novo_ativo); + } + await AtivosB3.bulkCreate(lista); // adiciona todos os ativos no banco de uma vez + }); + }).catch(function(err){ + console.log(err); + }); +} + +exports.calculaPatrimonio = async function (siglas, id_usuario) { + var lista = []; + var vTotal = 0; + for (let sigla of siglas) { + await Ativo.findAll({ + attributes: [ + "nomeAtivo", + "sigla", + "preco", + "quantidade", + "data", + "execucao", + ], + where: { + "id_usuario": id_usuario, + "sigla": sigla + }, + }).then(async (res) => { + let pPrecoMedio = 0, pQuantidade = 0, pTotal = 0, pDiferenca = 0, pTotalatt = 0, pTotalPM = 0, pQuantidadePM = 0; + let pNomeAtivo, pSigla; + const precoAtual = await AtivosB3.findAll({ + attributes: ['valor_fechamento'], + where: { + "codigo_acao": sigla + } + }); + precoAtt = precoAtual[0].valor_fechamento; + for (let result of res) { + const { nomeAtivo } = result; + const { sigla } = result; + const { preco } = result; + const { quantidade } = result; + const { execucao } = result; + const { data } = result; + + pNomeAtivo = nomeAtivo; + pSigla = sigla; + if (execucao === "compra") { + pTotalPM += parseFloat(preco) * parseInt(quantidade); + pTotal += parseFloat(preco) * parseInt(quantidade); + pTotalatt += parseFloat(precoAtt) * parseInt(quantidade); + pQuantidadePM += parseInt(quantidade); + pQuantidade += parseInt(quantidade); + } else { + //pTotal += (-1) * parseFloat(preco) * parseInt(quantidade); + pTotalatt += (-1) * parseFloat(preco) * parseInt(quantidade); + pQuantidade += (-1) * parseInt(quantidade); + } + } + vTotal += pTotalatt; + pPrecoMedio = pTotalPM / pQuantidadePM; + + //console.log(pTotalatt); + //console.log(pTotal); + //console.log(vTotal); + + const patrimonio = { + nomeAtivo: pNomeAtivo, + sigla: pSigla, + porcentagem: ((pTotalatt / vTotal)*100).toFixed(2), + quantidade: pQuantidade, + precoAtual: precoAtual[0].valor_fechamento, + precoMedio: parseFloat(pPrecoMedio.toFixed(2)), + diferenca: (precoAtual[0].valor_fechamento*pQuantidade - pTotal).toFixed(2), + valorTotal: (precoAtual[0].valor_fechamento*pQuantidade).toFixed(2) + } + + lista.push(patrimonio); + + }); + } + // Percorre os valores da lista e calcula a porcentagem. + for (let x of lista) { + x.porcentagem = ((x.valorTotal / vTotal)*100).toFixed(2); + } + + return lista; +} + +exports.calculaRentabilidade = async function (datas, id_usuario) { + console.log(datas); + const sc = new sequelize("usuario", "root", "12345678", { + host: 'localhost', + dialect: 'mysql' + }); + + let lista = []; + for (let data of datas) { + let lucro = 0, gastoTotal = 0; + await sc.query(`SELECT DISTINCT(sigla) FROM ativos WHERE data LIKE '${data}%' AND id_usuario = ${id_usuario}`).then(async (res) => { + for (let ativo of res[0]) { + await sc.query(`SELECT * FROM ativos WHERE data LIKE '${data}%' AND id_usuario = ${id_usuario} AND sigla = '${ativo.sigla}'`).then(async (res2) => { + let pTotal = 0, precoMedio = 0, sigla; + let qtCompras = 0, qtVendida = 0, qtComprada = 0, qtTotal = 0; + for (let result of res2[0]) { + // console.log(result, i); + sigla = result.sigla; + const { preco } = result; + const { quantidade } = result; + const { execucao } = result; + + qtTotal += quantidade; + if (execucao === "compra") { + qtCompras++; + qtComprada += quantidade; + pTotal += (-1) * parseFloat(preco) * parseInt(quantidade); + gastoTotal += parseFloat(preco) * parseInt(quantidade); + precoMedio += parseFloat(preco) * parseInt(quantidade); + } else { + qtVendida += quantidade; + pTotal += parseFloat(preco) * parseInt(quantidade); + } + } + + // cria a string da data do pregao do mes + const pregao = `${data.split("-")[0]}${data.split("-")[1]}` + // descobre qual o preco de fechamento do ultimo dia do mes em especifico que teve pregao + const precoAtual = await calculaPrecoAtual(pregao, sigla); + + precoMedio = precoMedio / qtComprada; + if (qtCompras === res2[0].length) { // somente compra + console.log(1); + lucro += qtComprada * (precoAtual - precoMedio); // somente a valorizacao do ativo + } else { + if (qtVendida === qtComprada) { // venda total + console.log(2); + lucro += pTotal; + } else { // venda parcial + lucro += ((qtComprada - qtVendida) * precoAtual) + pTotal; + } + } + }); + } + + const rentabilidade = { + data: data, + valor: ((lucro/gastoTotal)*100).toFixed(2), + } + + lista.push(rentabilidade); + }); + } + return lista; +} + +async function calculaPrecoAtual(data, sigla) { + return new Promise((resolve, reject) => { + Axios.get(`https://api-cotacao-b3.labdo.it/api/cotacao/cd_acao/${sigla}/100`, { // devolve os ultimos 100 pregoes desse ativo + }).then(function(res) { + for (let pregao of res.data) { + const { dt_pregao } = pregao; + if (JSON.stringify(dt_pregao).includes(data)) { + return resolve(pregao.vl_fechamento); + } + } + }).catch(function(err) { + console.log(err); + }); + }); +} \ No newline at end of file diff --git a/Back/src/util/IndicesUtil.js b/Back/src/util/IndicesUtil.js new file mode 100644 index 0000000..c91a944 --- /dev/null +++ b/Back/src/util/IndicesUtil.js @@ -0,0 +1,27 @@ +const Axios = require("axios"); + +exports.indiceSelic = async function () { + var lista = []; + // Pega os dados da taxa selic + await Axios.get("http://api.bcb.gov.br/dados/serie/bcdata.sgs.432/dados?formato=json", { + }).then(function(res){ + lista.push(res.data); + return lista; + }).catch(function(err){ + console.log(err); + return lista; + }); +} + +exports.indiceCDI = async function () { + var lista = []; + // Pega os dados da taxa selic + await Axios.get("http://api.bcb.gov.br/dados/serie/bcdata.sgs.12/dados?formato=json", { + }).then(function(res){ + lista.push(res.data); + return lista; + }).catch(function(err){ + console.log(err); + return lista; + }); +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f74daba..840fd84 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,22 +12,19 @@ Caso encontre um bug ou tenha alguma sugestão de melhoria ao projeto, siga os p 1. Defina as labels que são pertinentes ao problema ou sugestão; 1. Se aplicável, defina os responsáveis pela issue, o milestone e o projeto. 1. Retire dúvidas através da issue. -## Gitflow + Para contribuir com o projeto, observe as políticas adotadas em relação a padronização e organização de código e documentação. -Documentação -Regras: +Documentação: [Acesso a documentação](https://fga-eps-mds.github.io/GFour-Invext/#/pages/DocumentoDeArquiteturaDeSoftware) -1. Novas branchs devem ser criadas a partir da main; -1. Depois de fazer modificações na branch, submete-a por pull request para integrar a branch principal (main); -1. Após aprovado ou recusado o pull request, apague a branch. +Issues: [Acesso as issues](https://github.com/fga-eps-mds/GFour-Invext/issues) + +## Política de Branches -## Código 1. Novas branchs devem ser criadas a partir da dev; 1. Depois de fazer modificações na branch, submete-a por pull request para integrar a branch secundária (Develop); 1. Após aprovado ou recusado o pull request, apague a branch. -## Política de Branches ### **main:** main é a branch de produção, onde se encontra a versão que estará disponível para utilização no mercado. @@ -35,16 +32,28 @@ main é a branch de produção, onde se encontra a versão que estará disponív develop é a branch de homologação, onde se encontra a versão mais atualizada do projeto. ### **Nome das Branches** -Crie a branch com a seguinte estrutura: +Para criar novas branchs crie com a seguinte estrutura: [número-da-issue]- ## Política de Commits -Para commits individuais, use: git commit -m "Mensagem". Para commits em pares, digite git commit e atribua os co-authoreds na mensagem: +Para realizar commits, utilize o template abaixo: + git commit -m "tipo: Exemplo de commit" - Mensagem do commit: - Co-authored-by: Nome e sobrenome do parceiro(a) +- Os commits devem utilizar o tempo presente. Exemplo: "Adiciona funcionalidade" e não "Adicionada a funcionalidade". + +- Escreva o commit de maneira objetiva, descrevendo brevemente o que foi implementado, alterado, etc. + +- Utilize os comentários da issue para detalhar mais sobre o que etá sendo implementado. + +Os tipos de commits podem ser: +- **feat** (novo recurso) +- **fix** (correção de bug) +- **refactor** (refatorando o código de produção) +- **style** (formatação, falta de ponto e vírgula, etc; sem alteração de código) +- **docs** (alterações na documentação) +- **teste** (adicionando ou refatorando testes; sem alteração do código de produção) ## Política de Merges e Pull Requests ### Pull Requests: @@ -55,7 +64,11 @@ Pull requests serão realizados para controle de estabilidade das branches: Quando disponível uma nova release ou funcionalidade, esta será integrada através de pull request na branch main. -Durante a criação de um pull request, deve-se observar o template definido no repositório. +Durante a criação de um pull request, deve-se observar o template definido no repositório e adicionar o scrum team como reviewer. + +Todos os merges devem ser realizados pelos scrum teams, com excessão de quando o scrum team é quem fez o codigo. Nesse caso o código deve ser revisado por alguém do outro time. + +Após a revisão do código e aceitação do pull request, deve ser realizado o merge. ## Code Review Na revisão de código de pull request, observe os pontos abaixo: diff --git a/Front/package.json b/Front/package.json index feb4a2f..816678e 100644 --- a/Front/package.json +++ b/Front/package.json @@ -9,15 +9,26 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.10.4", + "@emotion/styled": "^11.10.4", + "@mui/material": "^5.10.3", + "@mui/x-data-grid": "^5.16.0", "axios": "^0.27.2", "date-fns": "^2.29.1", + "module-alias": "^2.2.2", "react": "^18.2.0", + "react-bootstrap": "^2.5.0", "react-dom": "^18.2.0", "react-icons": "^4.4.0", "react-imask": "^6.4.2", - "react-router-dom": "^6.3.0" + "react-router-dom": "^6.3.0", + "react-select": "^5.4.0", + "react-window": "^1.8.7", + "react-windowed-select": "^5.1.0", + "victory": "^36.6.6" }, "devDependencies": { + "@types/module-alias": "^2.0.1", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", "@vitejs/plugin-react": "^2.0.0", diff --git a/Front/src/App.css b/Front/src/App.css index 6798013..aed8d42 100644 --- a/Front/src/App.css +++ b/Front/src/App.css @@ -1,5 +1,7 @@ +@import url('https://fonts.googleapis.com/css2?family=Lato&display=swap'); .App { font-family: 'Roboto Flex'; font-style: normal; font-weight: 400; } + diff --git a/Front/src/App.tsx b/Front/src/App.tsx index 7cb18aa..2b689a3 100644 --- a/Front/src/App.tsx +++ b/Front/src/App.tsx @@ -1,20 +1,45 @@ import './App.css' -import CadastroUsuario from './pages/CadastroUsuario/Cadastro' -import LoginUsuario from './pages/LoginUsuario/Login' -import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; +import CadastroUsuario from './pages/CadastroUsuario/Cadastro'; +import LoginUsuario from './pages/LoginUsuario/Login'; +import { HistoricoDeAcoes } from './pages/HistoricoDeAcoes/historico'; +import { Route, Routes } from "react-router-dom"; +import { CadastroAcoes } from './pages/Cadastro de Ações/Açoes'; +import { AuthProvider } from './services/Provider'; +import { RequireAuth } from './services/requireAuth'; +import { Sidebar } from './pages/Sidebar/Sidebar'; +import { PublicRoute } from './services/publicRoute'; +import { Rentabilidade } from './pages/Rentabilidade/rentabilidade'; +import { Patrimonio } from './pages/Patrimonio/Patrimonio'; +import { PageNotFound } from './components/PageNotFound/PageNotFound'; function App() { return (
- + - } /> - } /> + + }> + } /> + } /> + + + + + + }> + } /> + } /> + } /> + } /> + + + } /> - +
- ) + ); } -export default App +export default App; diff --git a/Front/src/components/BuscaAtivos/Busca.css b/Front/src/components/BuscaAtivos/Busca.css new file mode 100644 index 0000000..5608140 --- /dev/null +++ b/Front/src/components/BuscaAtivos/Busca.css @@ -0,0 +1,28 @@ +.select{ + text-align: center; + color:black; + font-size: 1.5em; + text-align: center; + font-weight: bold; + +} +.react-select__control{ + border-radius: 40px !important; + padding: 0.25em; +} + +.react-select__value-container{ + margin-left: 10%; + width: 90% !important; + cursor: text; +} + +.react-select__indicators{ + width: 10% !important; + +} +.react-select__indicator{ + display: flex; + justify-content: center; + align-items: center; +} diff --git a/Front/src/components/BuscaAtivos/Busca.tsx b/Front/src/components/BuscaAtivos/Busca.tsx new file mode 100644 index 0000000..c2a20c6 --- /dev/null +++ b/Front/src/components/BuscaAtivos/Busca.tsx @@ -0,0 +1,69 @@ +import axios from 'axios'; +import { useState, useEffect } from 'react'; +import WindowedSelect, { createFilter } from "react-windowed-select"; +import './Busca.css'; + +interface Assets { + nome: string, + sigla: string +} +interface Option { + value: Assets, + label: string +} +interface Props { + value: Assets, + setValue: Function +} + +// Fazer o props para pegar o assets para pesquisa +export const BuscaAtivo = (props: Props) => { + const [options, setOptions] = useState(); + + useEffect(() => { + axios.get("/ativo/buscaativos") + .then(function (response) { + const ativos: Assets[] = response.data.lista; + var array = ativos.map((ativos) => ({ value: ativos, label: ativos.nome.concat(' - ', ativos.sigla) })); + setOptions(array); + + }).catch(function (error) { + console.log(error); + }); + + }, []); + + const handleChange = (e: unknown) => { + if (e) { + const option = e as Option; + props.setValue(option.value); + + } else { + props.setValue(''); + } + } + return ( + "Ativo não encontrado"} + isClearable + components={{ DropdownIndicator: () => null, IndicatorSeparator: () => null }} + onChange={handleChange} + value={props.value ? + { + value: props.value, + label: props.value.nome.concat(' - ', props.value.sigla) + } + : + null + } + /> + ); +} + + diff --git a/Front/src/components/PageNotFound/PageNotFound.tsx b/Front/src/components/PageNotFound/PageNotFound.tsx new file mode 100644 index 0000000..e765864 --- /dev/null +++ b/Front/src/components/PageNotFound/PageNotFound.tsx @@ -0,0 +1,15 @@ + +export const PageNotFound = () => { + + return ( +
+

Página não encontrada!

+
+ ); +} \ No newline at end of file diff --git a/Front/src/components/PopoverEditarAtivo/editarAtivo.css b/Front/src/components/PopoverEditarAtivo/editarAtivo.css new file mode 100644 index 0000000..717eadf --- /dev/null +++ b/Front/src/components/PopoverEditarAtivo/editarAtivo.css @@ -0,0 +1,77 @@ +.MuiDialogContent-root{ + background-color: #060b26; + display: flex; + flex-direction: column; + +} + +.modalEdit{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: white; + gap:1.25em; + padding: 2.5% 5%; + padding-bottom: 5%; +} +.modalEdit h2{ + font-family: 'Roboto Flex'; + width: 100%; + text-align: center; + font-size: 2em; + color: white; + font-weight: bold; +} + + +.modalEdit-linebox { + width: 100%; + display: flex; + justify-content: space-between; +} +.modalEdit input{ + width: 45%; + background-color:white; + color:black; + border: none; + border-radius: 40px; + font-size: 1.2em; + text-align: center; + font-weight: bold; + padding: 0.5em 0; + +} + +.modalEdit input::placeholder{ + color:gray; + font-family: 'Roboto'; + font-size: 1em; +} + +.modalEdit input:disabled{ + background-color: lightgray; +} + +.busca-ativo{ + width: 100% !important; +} + +.data-input{ + font-size: 0.5em; +} + +.modalEdit button{ + color:white; + border: none; + border-radius:70px; + font-size: 1em; + text-align: center; + padding: 0.5em 1.5em; + cursor: pointer; + transition: 0.25s; +} +.modalEdit button:hover{ + background: white; + color: black; +} diff --git a/Front/src/components/PopoverEditarAtivo/editarAtivo.tsx b/Front/src/components/PopoverEditarAtivo/editarAtivo.tsx new file mode 100644 index 0000000..714cc8f --- /dev/null +++ b/Front/src/components/PopoverEditarAtivo/editarAtivo.tsx @@ -0,0 +1,130 @@ +import { Dialog, DialogContent, Button } from "@mui/material" +import axios from "axios"; +import { useState } from "react"; +import { IMaskInput } from "react-imask"; +import { Register } from "../../pages/HistoricoDeAcoes/historico"; +import { returnToken } from "../../services/authToken"; +import './editarAtivo.css' + +interface Props { + isOpen: boolean, + setIsOpen: (isOpen: boolean) => void + initialValues: Register + callback: () => void +} +export const EditarAtivo = (p: Props) => { + + const token = returnToken(); + const [error, setError] = useState(""); + + const [stockPrice, setStockPrice] = useState(p.initialValues.preco.toString()); //preço das ações + const [date, setDate] = useState(p.initialValues.data); + const [quantity, setQuantity] = useState(p.initialValues.quantidade.toString()); + + //para a formatar a data + const [inputType, setInputType] = useState("text"); + + const closeModal = () => { + p.setIsOpen(false); + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + setError(""); + if (parseInt(quantity) <= 0) { + setError("É necessário inserir uma quantidade válida"); + + } else if (parseInt(stockPrice) <= 0) { + setError("É necessário inserir um valor válido"); + + } else { + axios.post("/ativo/editar", + { + token: token, + id: p.initialValues.id, + data: date, + preco: stockPrice, + quantidade: quantity + + }).then(function (response) { + alert(response.data.message); + p.callback(); + closeModal(); + + }).catch(function (error) { + const message = error.response.data.message; + setError(message); + }) + } + } + + return ( + + +
+

Editar Ativo

+ + + +
+ ) => + setStockPrice(e.target.value)} + /> + ) => + setQuantity(e.target.value)} + /> +
+
+ ) => + setDate(e.currentTarget.value)} + onFocus={() => setInputType("date")} + onBlur={() => setInputType("text")} + /> + + +
+
+ + +
+ {error &&

{error}

} +
+
+ +
+ ) +} + diff --git a/Front/src/main.tsx b/Front/src/main.tsx index 238fccb..86a5146 100644 --- a/Front/src/main.tsx +++ b/Front/src/main.tsx @@ -1,10 +1,15 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' +import { BrowserRouter } from "react-router-dom"; import './index.css' +import axios from 'axios'; +axios.defaults.baseURL = 'http://localhost:3000'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + ) diff --git "a/Front/src/pages/Cadastro de A\303\247\303\265es/A\303\247oes.tsx" "b/Front/src/pages/Cadastro de A\303\247\303\265es/A\303\247oes.tsx" new file mode 100644 index 0000000..6aa1392 --- /dev/null +++ "b/Front/src/pages/Cadastro de A\303\247\303\265es/A\303\247oes.tsx" @@ -0,0 +1,134 @@ +//talvez podemos começar a checar os subtitulos igual no prototipo usando uma nova div e mexendo apenas no p. +//lembrar de alterar o fundo +import './Ações.css'; +import { IMaskInput } from "react-imask"; +import { useState } from "react"; +import Axios from "axios"; +import { useAuth } from '../../services/Provider'; +import { useNavigate } from 'react-router-dom'; +import { BuscaAtivo } from '../../components/BuscaAtivos/Busca'; + +interface Assets { + nome: string, + sigla: string +} +export const CadastroAcoes = () => { + + const [error, setError] = useState(""); + const [assets, setAssets] = useState(); //Assets é os ativos + const [stockPrice, setStockPrice] = useState(""); //preço das ações + const [date, setDate] = useState(""); + const [quantity, setQuantity] = useState(""); + + // Define qual tipo de operação será efetuada no request (compra/venda) + const [requestType, setRequestType] = useState(""); + + const navigate = useNavigate(); + const auth = useAuth(); + const token = auth.getToken(); + + const handleSubmit = async (e: React.FormEvent) => { + + e.preventDefault(); + + setError(""); + if (!assets) { + setError("Selecione um ativo") + + } else if (parseInt(quantity) <= 0) { + setError("É necessário inserir uma quantidade válida") + + } else if (parseInt(stockPrice) <= 0) { + setError("É necessário inserir um valor válido") + + } else { + Axios.post("/ativo/" + requestType, + { + token: token, + nomeAtivo: assets.nome, + sigla: assets.sigla, + preco: stockPrice, + quantidade: quantity, + data: date + }).then(function (response) { + alert(response.data.message); + navigate("../historico"); + + }).catch(function (error) { + const message = error.response.data.message; + setError(message); + }) + } + } + + //para a formatar a data + const [inputType, setInputType] = useState("text"); + + return ( +
+

Compra/Venda de Ativos

+
+
+ + +
+ ) => + setStockPrice(e.target.value)} + /> + ) => + setQuantity(e.target.value)} + /> +
+
+ ) => + setDate(e.currentTarget.value)} + onFocus={() => setInputType("date")} + onBlur={() => setInputType("text")} + /> +
+
+ + +
+ {error &&

{error}

} + +
+
+ ); +} + diff --git "a/Front/src/pages/Cadastro de A\303\247\303\265es/A\303\247\303\265es.css" "b/Front/src/pages/Cadastro de A\303\247\303\265es/A\303\247\303\265es.css" new file mode 100644 index 0000000..2a7ec93 --- /dev/null +++ "b/Front/src/pages/Cadastro de A\303\247\303\265es/A\303\247\303\265es.css" @@ -0,0 +1,108 @@ +.background-img { + display: flex; + flex-direction: column; + justify-content: center; + position: absolute; + height: 100vh; + width:100vw; + background-image: url('../../assets/background.jpeg'); + background-color: #232239; + background-size: cover; + background-repeat: no-repeat; +} +.div-acoes{ + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(10, 17, 79, 0.8); + border-radius: 40px; + width: 65%; +} +.div-acoes div{ + width: 100%; +} +.form-acoes{ + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: white; + gap:1.25em; + padding: 5%; + padding-bottom: 7.5%; +} + +.titulo-acoes { + text-align: center; + font-size: 2.5em; + margin-bottom: 0.5em; + color: white; + font-weight: bold; +} +.linebox { + display: flex; + justify-content: space-between; +} +.form-acoes input{ + background-color:white; + color:black; + width: 45%; + border: none; + border-radius: 40px; + font-size: 1.5em; + text-align: center; + font-weight: bold; + padding: 0.5em; + +} + +.form-acoes input::placeholder{ + color:gray; + font-family: 'Roboto'; + font-size: 1em; +} + + +.div-acoes button{ + color:white; + border: none; + border-radius:70px; + font-size: 1.5em; + text-align: center; + padding: 0.5em 1.5em; + cursor: pointer; + transition: 0.25s; +} +.div-acoes button:hover{ + float: left; + background: white; + color: black; + align-items: left; + justify-items: left; +} +.buy-button { + background-color:#47B802; +} + +.sell-button { + background-color:#9F0303; + padding: 0.5em 2em; +} + +.error{ + color:#721c24; + background-color: #F8D7D4; + border: 1px solid #F5C6CB; + padding: 0.5em 1.5em; + border-radius: 5px; + +} + +.buttonBox{ + display: flex; + justify-content: center; + align-items: center; + gap: 2.5em; + margin-top: 1.75em; +} diff --git a/Front/src/pages/CadastroUsuario/Cadastro.css b/Front/src/pages/CadastroUsuario/Cadastro.css index 73499bc..a135ee1 100644 --- a/Front/src/pages/CadastroUsuario/Cadastro.css +++ b/Front/src/pages/CadastroUsuario/Cadastro.css @@ -1,6 +1,3 @@ -body { - margin: 0; -} .background-img { display: flex; align-items: center; @@ -16,23 +13,33 @@ body { .div-cadastro{ display: flex; flex-direction: column; - padding: 2%; - width: 80%; + padding: 2.5%; + width: 60%; background-color: rgba(10, 17, 79, 0.8); border-radius: 40px; color: white; +} + +.form-cadastro{ + display:flex ; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 1em; } -.titulo { +.titulo-cadastro{ text-align: center; - font-size: 2.5em; - margin-bottom: 0.5em; + font-size: 2.5em; + font-weight: bold; } .icone-voltar{ color: white; - font-size: 2em; + font-size: 2.5em; cursor: pointer; + position: absolute; + z-index: 2; } .icone-voltar:hover{ @@ -43,16 +50,8 @@ body { transform: scale(1.1); } -.form-cadastro{ - display: flex; - flex-direction: column; - justify-content: center; - align-content: center; - align-items: center; - gap: 1em; -} -input{ +.form-cadastro input{ background-color:rgba(169, 178, 194, 0.7); color: white; width: 50%; @@ -64,7 +63,7 @@ input{ font-weight: bold; } -input::placeholder{ +.form-cadastro input::placeholder{ color: rgba(255, 255, 255, 0.7); font-family: 'Roboto'; } diff --git a/Front/src/pages/CadastroUsuario/Cadastro.tsx b/Front/src/pages/CadastroUsuario/Cadastro.tsx index 5b705a5..6edfef3 100644 --- a/Front/src/pages/CadastroUsuario/Cadastro.tsx +++ b/Front/src/pages/CadastroUsuario/Cadastro.tsx @@ -9,7 +9,7 @@ import Axios from "axios"; const CadastroUsuario = () => { // Vai redirecionar a pagina para o login - let navigate = useNavigate(); + const navigate = useNavigate(); const [displayName, setDisplayName] = useState(""); const [birth, setBirth] = useState(""); @@ -42,7 +42,7 @@ const CadastroUsuario = () => { setError("As senhas precisam ser iguais"); } else { - Axios.post("http://localhost:3000/usuario/cadastrar", { + Axios.post("/usuario/cadastrar", { nomeCompleto: displayName, dataNascimento: birth, telefone: phone, @@ -71,18 +71,18 @@ const CadastroUsuario = () => { const [inputType, setInputType] = useState("text"); return ( - -
+ +
- - + + -

Cadastrar

+

Cadastrar

{ setConfirmPassword(e.target.value)} /> - {error &&

{error}

} +
- + ) } diff --git a/Front/src/pages/HistoricoDeAcoes/excluirAtivo.tsx b/Front/src/pages/HistoricoDeAcoes/excluirAtivo.tsx new file mode 100644 index 0000000..48af30d --- /dev/null +++ b/Front/src/pages/HistoricoDeAcoes/excluirAtivo.tsx @@ -0,0 +1,20 @@ +import axios from "axios" +import { returnToken } from "../../services/authToken" + +export const excluirAtivo = (id:string) => { + const token = returnToken(); + + if(confirm('Deseja realmente excluir esse ativo?')){ + + axios.post('/ativo/excluir', { + token: token, + id: id + }).then(function(response){ + alert(response.data.message); + + }).catch(function(error){ + alert(error.response.data.message); + }) + } + +} \ No newline at end of file diff --git a/Front/src/pages/HistoricoDeAcoes/historico.css b/Front/src/pages/HistoricoDeAcoes/historico.css new file mode 100644 index 0000000..74e49d9 --- /dev/null +++ b/Front/src/pages/HistoricoDeAcoes/historico.css @@ -0,0 +1,38 @@ +.background-img { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + height: 100vh; + width:100vw; + background-image: url('../../assets/background.jpeg'); + background-size: cover; + background-repeat: no-repeat; +} + +.titulo-historico{ + text-align: center; + font-size: 2.5em; + margin-bottom: 0.5em; + color: white; + font-weight: bold; + margin-top: 2.5%; +} + +.div-historico ul { + color: #000000; + padding-right: 10px; + display: flex; +} +.div-historico li{ + padding: 10px; +} +.div-historico { + display: flex; + flex-direction: column; + padding: 2%; + width: 72%; + background-color: #cacacaf1; + border-radius: 0.5cm; + color: white; +} \ No newline at end of file diff --git a/Front/src/pages/HistoricoDeAcoes/historico.tsx b/Front/src/pages/HistoricoDeAcoes/historico.tsx new file mode 100644 index 0000000..7f8e0d6 --- /dev/null +++ b/Front/src/pages/HistoricoDeAcoes/historico.tsx @@ -0,0 +1,129 @@ +import './historico.css'; +import { BiEdit } from 'react-icons/bi'; +import { RiDeleteBinLine } from 'react-icons/ri' +import { DataGrid, GridActionsCellItem, GridColDef } from '@mui/x-data-grid'; +import { useAuth } from '../../services/Provider'; +import { useEffect, useState } from 'react'; +import Axios from 'axios'; +import { format } from 'date-fns'; +import { excluirAtivo } from './excluirAtivo'; +import { parse } from 'date-fns/esm'; +import { EditarAtivo } from '../../components/PopoverEditarAtivo/editarAtivo'; + + + +// Registro que sera mostrado para o usuario +export interface Register { + id: number, + nomeAtivo: string, + sigla: string, + execucao: string, + quantidade: number, + data: string, + preco: string +} + +export const HistoricoDeAcoes = () => { + + const auth = useAuth(); + const token = auth.getToken(); + // Todo o historico de compra e venda de ativos + const [historic, setHistoric] = useState(); + // Dita quando o historico deve ser puxado do backend + const [renderData, setRenderData] = useState(true); + + const [openEditModal, setEditModalOpen] = useState(false); + const [selectedRow, setSelectedRow] = useState(); + + // Pega o historico do usuario do backend + useEffect(() => { + if (renderData) { + setRenderData(false); + Axios.post("/ativo/historico", { + token: token + } + ).then(function (response) { + setHistoric(response.data.historico); + }).catch(function (error) { + console.log(error); + }) + } + }, [renderData]) + + + const columns: GridColDef[] = [ + { field: 'id', headerName: 'ID', width: 70 }, + { field: 'nomeAtivo', headerName: 'Ativo', width: 130 }, + { field: 'sigla', headerName: 'Sigla', width: 130 }, + { field: 'execucao', headerName: 'Ordem', width: 130 }, + { field: 'quantidade', headerName: 'Quantidade', width: 130 }, + { + field: 'data', + headerName: 'Negociação', + width: 130, + valueFormatter: (params) => format(parse(params.value, 'yyyy-MM-dd', new Date), "dd/MM/yyyy") + }, + { + field: 'preco', + type: 'number', + headerName: 'Valor', + width: 130, + valueFormatter: (params) => `R$ ${params.value.toFixed(2)}` + }, + { + field: 'edit-column', + headerName: ' ', + sortable: false, + disableColumnMenu: true, + width: 60, + renderCell: (params) => [ +
+ } + label='Editar' + onClick={() => { + setSelectedRow(params.row); + setEditModalOpen(true); + }} + /> + } + label='Deletar' + onClick={() => { + excluirAtivo(params.id.toString()); + setRenderData(true); + }} + /> +
+ ], + }, + ]; + + return ( + <> +
+

Histórico de Ativos

+
+
+ {historic ? + false} + /> + : null} +
+
+
+ {openEditModal && + setRenderData(true)} + />} + + ) +} diff --git a/Front/src/pages/LoginUsuario/Login.tsx b/Front/src/pages/LoginUsuario/Login.tsx index f84835d..4e364a0 100644 --- a/Front/src/pages/LoginUsuario/Login.tsx +++ b/Front/src/pages/LoginUsuario/Login.tsx @@ -1,6 +1,7 @@ import './Login.css'; import { useState } from "react"; -import { Link } from 'react-router-dom'; +import { Link, useLocation, useNavigate} from 'react-router-dom'; +import { useAuth } from '../../services/Provider'; import Axios from "axios"; const LoginUsuario = () => { @@ -8,26 +9,27 @@ const LoginUsuario = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); - const [user, setUser] = useState([]); + // Variaveis para encaminhamento de usuario logado/nao logado + let navigate = useNavigate(); + let location = useLocation(); + let auth = useAuth(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(""); - const user = { - email, - password, - } - - Axios.post("http://localhost:3000/usuario/login", { + Axios.post("/usuario/login", { email: email, senha: password }).then(function (response) { - const message = response.data.message; - const token = response.data.token; - alert(message); + const token = response.data.token; + + auth.login(token, () => { + navigate("/patrimonio", { replace: true }); + }); + }).catch(function (response) { // Caso caia nesse catch, o usuario nao eh gravado no banco e retorna um erro const message = response.response.data.message; diff --git a/Front/src/pages/Patrimonio/Patrimonio.css b/Front/src/pages/Patrimonio/Patrimonio.css new file mode 100644 index 0000000..01ecf47 --- /dev/null +++ b/Front/src/pages/Patrimonio/Patrimonio.css @@ -0,0 +1,55 @@ +.background-img-patrimonio { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: absolute; + height: 100vh; + width:100vw; + background-image: url('../../assets/background.jpeg'); + background-size: cover; + background-repeat: no-repeat; + font-family: 'Roboto Flex'; + +} + +.div-patrimonio{ + display: flex; + flex-direction: column; + padding: 1.0%; + width: 70%; + height: 70%; + background-color: rgba(10, 17, 79, 0.904); + border-radius: 40px; +} + +.titulo-patrimonio{ + text-align: center; + margin-top: 1.0em; + font-size: 2.5em; + margin-bottom: 0.5em; + color: white; + font-weight: bold; +} + +.div-ativos-patrimonio ul { + margin-top: auto; + color: #000000; + padding-right: 5px; + display: flex; +} +.div-ativos-patrimonio li{ + padding: 5px; +} + +.div-grid-patrimonio { + border-color: #ffff; + height: 60%; + width: 85%; + margin-left: 50%; + transform: translate(-50%); + padding: 2.0%; + height: 25em; + background-color: #dcdcdc; + border-radius: 25px; +} diff --git a/Front/src/pages/Patrimonio/Patrimonio.tsx b/Front/src/pages/Patrimonio/Patrimonio.tsx new file mode 100644 index 0000000..acace82 --- /dev/null +++ b/Front/src/pages/Patrimonio/Patrimonio.tsx @@ -0,0 +1,92 @@ +import "./Patrimonio.css"; +import { useState, useEffect } from "react"; +import Axios from "axios"; +import { DataGrid, GridColDef } from "@mui/x-data-grid"; +import { useAuth } from "../../services/Provider"; + +//base da tabela...estou usando tudo que tava no prototipo +const columns: GridColDef[] = [ + //{ field: "id", headerName: "ID", width: 35 }, + { field: "nomeAtivo", headerName: "Ações", width: 120 }, + { field: "sigla", headerName: "Sigla", width: 80}, + { field: "quantidade", headerName: "Quant.", width: 80 }, + { field: "precoAtual", headerName: "Preço atual", width: 100, type: 'number',valueFormatter: (params) => `R$ ${params.value.toFixed(2)}`, align: 'center'}, + { field: "precoMedio", headerName: "Preço médio", width: 100, type: 'number', valueFormatter: (params) => `R$ ${params.value.toFixed(2)}`, align: 'left'}, + { field: "diferenca", headerName: "Diferença", width: 80 }, + { field: "porcentagem", headerName: "Porc.", width: 80 }, + { field: "valorTotal", headerName: "Valor total", width: 100, type: 'number', valueFormatter: (params) => `R$ ${params.value}`, align:'left' }, +]; + +//passo tudo que esta em field para essa interface...verificar se esta ok os nomes e os tipos. +interface Ativo { + id: number; + nomeAtivo: string; + sigla: string; + porcentagem: number; + quantidade: number; + precoAtual: number; + precoMedio: number; + diferenca: number; + valorTotal: number; +} + +export const Patrimonio = () => { + const auth = useAuth(); + const token = auth.getToken(); + const [patrimonio, setPatrimonio] = useState(); + + //vamos ter que mexer na forma como esses dados vem do banco...estou um pouco incerto dessa parte + + + + const getPatrimonio = () => { + //estou puxando os mesmos dados do historico + Axios.post("/ativo/patrimonio", { + token: token, + }) + .then(function (response) { + setPatrimonio(response.data.ativos.map((ativo:Ativo, index:number) => ({ + id: index, + nomeAtivo: ativo.nomeAtivo, + sigla: ativo.sigla, + porcentagem: ativo.porcentagem, + quantidade: ativo.quantidade, + precoAtual: ativo.precoAtual, + precoMedio: ativo.precoMedio, + diferenca: ativo.diferenca, + valorTotal: ativo.valorTotal, + }))); + console.log(response.data.ativos) + }) + .catch(function (error) { + console.log(error); + }); + + }; + + useEffect(() => { + getPatrimonio(); + }, []); + + return ( +
+ +

Patrimônio

+
+
+
+ {patrimonio ? ( + + ) : null} + +
+
+
+
+ ); +}; diff --git a/Front/src/pages/Rentabilidade/rentabilidade.css b/Front/src/pages/Rentabilidade/rentabilidade.css new file mode 100644 index 0000000..f948471 --- /dev/null +++ b/Front/src/pages/Rentabilidade/rentabilidade.css @@ -0,0 +1,42 @@ +.background-img-rentabilidade { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: absolute; + height: 100vh; + width:100vw; + background-image: url('../../assets/background.jpeg'); + background-size: cover; + background-repeat: no-repeat; + font-family: 'Roboto Flex'; +} + +.div-rentabilidade{ + display: flex; + justify-content: space-between; + padding: 2%; + width: 70%; + height: 65%; + background-color: #A9A9A9; + border-radius: 1cm; +} + +.titulo-rentabilidade{ + text-align: center; + font-size: 2.5em; + margin-bottom: 0.3em; + color: white; + font-weight: bold; + margin-top: 5%; +} + +.div-chart-rentabilidade{ + width: 100%; + height: -25em; + padding: 2.0%; + background-color: #dcdcdcb0; + border: 1px solid; + border-color:#fff; + border-radius: 1cm; +} \ No newline at end of file diff --git a/Front/src/pages/Rentabilidade/rentabilidade.tsx b/Front/src/pages/Rentabilidade/rentabilidade.tsx new file mode 100644 index 0000000..0691ae6 --- /dev/null +++ b/Front/src/pages/Rentabilidade/rentabilidade.tsx @@ -0,0 +1,89 @@ +import "./rentabilidade.css"; +import { VictoryAxis, VictoryChart, VictoryLabel, VictoryLine, VictoryScatter, VictoryTheme, VictoryZoomContainer } from 'victory'; +import { useAuth } from "../../services/Provider"; +import { useEffect, useState } from "react"; +import Axios from "axios"; +import { format, parse } from "date-fns"; +import ptBR from 'date-fns/locale/pt-BR'; + +interface Rentabilidade { + x: Date, + y: number +} + +export const Rentabilidade = () => { + + const token = useAuth().getToken(); + const [data, setData] = useState([]); + + console.log(data); + const getData = () => { + + Axios.post('/ativo/rentabilidade', { + token: token + }).then(function (response) { + console.log(response.data) + setData(response.data.rentabilidade.map( + (item: any) => ({ + x: parse(item.data, 'yyyy-MM', new Date(), { locale: ptBR }), + y: Number(item.valor) + + })) + ); + + }).catch(function (error) { + console.log(error); + }) + } + + useEffect(() => { + getData(); + + }, []); + + return ( +
+

Rentabilidade

+
+
+ + + + + + item.x)} + tickFormat={(x) => { + return format(x, "MMM/yyyy", { locale: ptBR }); + }} + /> + + item.y)),[0])} + tickFormat={(y) => { + return `${y}%` + }} + style={{axisLabel: {padding: 50} }} + /> + +
+
+
+ ); +} diff --git a/Front/src/pages/Sidebar/Sidebar.css b/Front/src/pages/Sidebar/Sidebar.css new file mode 100644 index 0000000..c665a8a --- /dev/null +++ b/Front/src/pages/Sidebar/Sidebar.css @@ -0,0 +1,91 @@ +.container{ + position: absolute; + z-index: 100; + width: 100%; +} +.sidebar { + background-color: #060b26; + height: 60px; + display: flex; + justify-content: start; + align-items: center; +} + +.menu-bars { + margin-left: 2rem; + font-size: 2rem; + background: none; +} + +.nav-menu { + background-color: #060b26; + width: 250px; + height: 100vh; + display: flex; + justify-content: center; + position: fixed; + top: 0; + left: -100%; + transition: 850ms; +} + +.nav-menu.active { + left: 0; + transition: 350ms; +} + +.nav-text { + display: flex; + justify-content: start ; + align-items: center; + padding: 8px 0px 8px 16px; + list-style: none; + height: 60px; +} +.nav-link { + text-decoration: none; + color: #f5f5f5; + font-size: 18px; + width: 95%; + height: 100%; + display: flex; + align-items: center; + padding: 0 16px; + border-radius: 4px; + cursor: pointer; +} +.nav-link:hover { + background-color: #1a83ff; +} + +.nav-link.disabled{ + pointer-events: none; + color: gray; +} + +.nav-menu-items { + width: 100%; + display: flex; + flex-direction: column; +} + +.sidebar-toggle { + background-color: #060b26; + width: 100%; + height: 80px; + display: flex; + justify-content: start; + align-items: center; +} + +.container span { + margin-left: 16px; +} + +.icon-hover:hover{ + -webkit-transform: scale(1.1); + -moz-transform: scale(1.1); + -o-transform: scale(1.1); + -ms-transform: scale(1.1); + transform: scale(1.1); +} \ No newline at end of file diff --git a/Front/src/pages/Sidebar/Sidebar.tsx b/Front/src/pages/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..91a9005 --- /dev/null +++ b/Front/src/pages/Sidebar/Sidebar.tsx @@ -0,0 +1,66 @@ +import { useState } from "react"; +import * as FaIcons from "react-icons/fa"; +import * as AiIcons from "react-icons/ai"; +import { Link, Outlet } from "react-router-dom"; +import { SidebarData } from "./SidebarData"; +import "./Sidebar.css"; +import { IconContext } from "react-icons"; +import { useAuth } from "../../services/Provider"; +import { useNavigate } from "react-router-dom"; + + +export const Sidebar = () => { + const [navbar, setNavbar] = useState(false); + + const showNavbar = () => setNavbar(!navbar); + + const auth = useAuth(); + const navigate = useNavigate(); + + const logout = () => { + auth.logout(() => { + navigate("/"); + }); + }; + + return ( + <> +
+ +
+ + + +
+ +
+
+ + + ); +}; diff --git a/Front/src/pages/Sidebar/SidebarData.tsx b/Front/src/pages/Sidebar/SidebarData.tsx new file mode 100644 index 0000000..beff4d5 --- /dev/null +++ b/Front/src/pages/Sidebar/SidebarData.tsx @@ -0,0 +1,48 @@ +import * as FaIcons from 'react-icons/fa' +import * as AiIcons from 'react-icons/ai' +import * as IoIcons from 'react-icons/io' + +export const SidebarData = [ + { + title:'Patrimônio', + path:'patrimonio', + icon: , + className: "nav-link" + }, + { + title:'Rentabilidade', + path:'rentabilidade', + icon: , + className: "nav-link" + }, + { + title:'Ações', + path:'acoes', + icon: , + className: "nav-link" + }, + { + title:'Histórico', + path:'historico', + icon: , + className: "nav-link" + }, + { + title:'Perfil', + path:'perfil', + icon: , + className: "nav-link disabled" + }, + { + title:'Sair', + path:'sair', + icon: , + className: "nav-link" + }, + { + title:'Ajuda', + path:'ajuda', + icon: , + className: "nav-link disabled" + }, +]; \ No newline at end of file diff --git a/Front/src/services/Provider.tsx b/Front/src/services/Provider.tsx new file mode 100644 index 0000000..34b81d6 --- /dev/null +++ b/Front/src/services/Provider.tsx @@ -0,0 +1,44 @@ +import { createContext, useContext,useState, useMemo } from "react"; +import { returnToken, setarToken, destroyToken } from "./authToken"; +// import { useLocalStorage } from "./useLocalStorage"; + +// Criação de um Context para saber se o usuario estah logad o ou nao +interface AuthContextType { + getToken: () => any; + login: (token: string, callback: VoidFunction) => void; + logout: (callback: VoidFunction) => void; +} + +const AuthContext = createContext(null!); + +export const AuthProvider = ({ children }: { children: JSX.Element }) => { + + // call this function when you want to authenticate the user + const login = (token: string, callback: VoidFunction) => { + return setarToken(token, () => { + callback(); + }); + + + }; + + // call this function to sign out logged in user + const logout = (callback: VoidFunction) => { + return destroyToken(() => { + callback(); + }); + + }; + + const getToken = () =>{ + return returnToken(); + }; + + let value = { getToken, login, logout }; + + return {children}; +}; + +export const useAuth = () => { + return useContext(AuthContext); +}; \ No newline at end of file diff --git a/Front/src/services/authToken.tsx b/Front/src/services/authToken.tsx new file mode 100644 index 0000000..d7b7dd7 --- /dev/null +++ b/Front/src/services/authToken.tsx @@ -0,0 +1,17 @@ + +export const TOKEN_KEY = "@invext-Token"; + +export const returnToken = () => localStorage.getItem(TOKEN_KEY); + +export const setarToken = (token: string, callback : VoidFunction) => { + localStorage.setItem(TOKEN_KEY, token); + callback(); +}; + +export const destroyToken = (callback: VoidFunction) => { + localStorage.removeItem(TOKEN_KEY,); + callback(); +}; + + + diff --git a/Front/src/services/publicRoute.tsx b/Front/src/services/publicRoute.tsx new file mode 100644 index 0000000..83c5071 --- /dev/null +++ b/Front/src/services/publicRoute.tsx @@ -0,0 +1,16 @@ +import { useLocation , Navigate, Outlet} from "react-router-dom"; +import { useAuth } from "./Provider"; + +export const PublicRoute = ( ) => { + let auth = useAuth(); + let location = useLocation(); + + + // Se o usuario não estiver logado, ele tem acesso a parte publica das rotas + if (!auth.getToken()) { + return ; + } + // Caso ele esteja logado, ele é redirecionado para o ultimo local + // que ele estava antes de tentar deslogar + return ; + } \ No newline at end of file diff --git a/Front/src/services/requireAuth.tsx b/Front/src/services/requireAuth.tsx new file mode 100644 index 0000000..4e4583f --- /dev/null +++ b/Front/src/services/requireAuth.tsx @@ -0,0 +1,18 @@ +import { useAuth } from "./Provider"; +import { useLocation, Navigate } from "react-router-dom"; + + + +export const RequireAuth = ( { children }: { children: JSX.Element } ) => { + let auth = useAuth(); + let location = useLocation(); + + if (!auth.getToken()) { + // Redirect them to the /login page, but save the current location they were + // trying to go to when they were redirected. This allows us to send them + // along to that page after they login, which is a nicer user experience + // than dropping them off on the home page. + return ; + } + return children; + } \ No newline at end of file diff --git a/Front/tsconfig.json b/Front/tsconfig.json index 3d0a51a..9a5ba3f 100644 --- a/Front/tsconfig.json +++ b/Front/tsconfig.json @@ -3,7 +3,7 @@ "target": "ESNext", "useDefineForClassFields": true, "lib": ["DOM", "DOM.Iterable", "ESNext"], - "allowJs": false, + "allowJs": true, "skipLibCheck": true, "esModuleInterop": false, "allowSyntheticDefaultImports": true, @@ -18,4 +18,4 @@ }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] -} +} \ No newline at end of file diff --git a/Front/vite.config.ts b/Front/vite.config.ts index b1b5f91..3ac8361 100644 --- a/Front/vite.config.ts +++ b/Front/vite.config.ts @@ -1,7 +1,8 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import module from 'module-alias/register'; -// https://vitejs.dev/config/ +//https://vitejs.dev/config/ export default defineConfig({ plugins: [react()] }) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b46c28f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "GFour-Invext", + "lockfileVersion": 2, + "requires": true, + "packages": {} +}