├── .gitignore ├── LICENSE ├── README.md ├── client ├── .env.example ├── .gitignore ├── .prettierrc ├── dev │ ├── add_comments.js │ └── copy_to_src_total.js ├── eslint.config.js ├── index.html ├── package.json ├── public │ └── images │ │ ├── img-1.jpg │ │ ├── img-2.jpg │ │ ├── img-3.jpg │ │ ├── img-4.jpg │ │ └── img-5.jpg ├── src │ ├── App.tsx │ ├── components │ │ ├── Page │ │ │ └── Page.tsx │ │ ├── charts │ │ │ ├── BarChart.tsx │ │ │ ├── ChartThemeConfig.ts │ │ │ ├── PieChart.tsx │ │ │ └── StackedBarChart.tsx │ │ ├── layouts │ │ │ ├── AppLayout.tsx │ │ │ ├── AppNavbar.tsx │ │ │ ├── AppSidebar.tsx │ │ │ └── AuthLayout.tsx │ │ ├── timeline │ │ │ ├── EnhancedTimeline.tsx │ │ │ └── TimelineVisualization.tsx │ │ └── ui │ │ │ ├── ErrorBoundary.tsx │ │ │ └── Table.tsx │ ├── contexts │ │ └── ThemeContext.tsx │ ├── features │ │ ├── activities │ │ │ ├── components │ │ │ │ ├── ActivityCard.tsx │ │ │ │ ├── FinishActivityDialog.tsx │ │ │ │ ├── ReportErrorDialog.tsx │ │ │ │ └── StartActivityDialog.tsx │ │ │ └── routes │ │ │ │ └── Activity.tsx │ │ ├── auth │ │ │ ├── components │ │ │ │ └── AuthStatus.tsx │ │ │ └── routes │ │ │ │ └── Login.tsx │ │ ├── dashboard │ │ │ ├── components │ │ │ │ ├── ProductionCharts.tsx │ │ │ │ └── StatusCards.tsx │ │ │ └── routes │ │ │ │ └── Dashboard.tsx │ │ ├── fieldActivities │ │ │ ├── components │ │ │ │ ├── CampoMap.tsx │ │ │ │ ├── FieldActivitySidebar.tsx │ │ │ │ └── FieldFeaturePopup.tsx │ │ │ └── routes │ │ │ │ └── FieldActivities.tsx │ │ ├── grid │ │ │ ├── components │ │ │ │ ├── Grid.tsx │ │ │ │ └── GridCard.tsx │ │ │ └── routes │ │ │ │ └── Grids.tsx │ │ ├── lot │ │ │ └── routes │ │ │ │ └── Lot.tsx │ │ ├── map │ │ │ ├── components │ │ │ │ ├── FeaturePopup.tsx │ │ │ │ ├── LayerControl.tsx │ │ │ │ ├── MapControls.tsx │ │ │ │ ├── MapLegend.tsx │ │ │ │ └── MapVisualization.tsx │ │ │ ├── routes │ │ │ │ └── Maps.tsx │ │ │ └── utils │ │ │ │ └── mapStyles.ts │ │ ├── microControl │ │ │ └── routes │ │ │ │ └── MicroControl.tsx │ │ ├── pit │ │ │ └── routes │ │ │ │ └── PIT.tsx │ │ └── subphases │ │ │ └── routes │ │ │ ├── SubphaseSituation.tsx │ │ │ ├── Subphases.tsx │ │ │ └── UserActivities.tsx │ ├── hooks │ │ ├── useActivities.ts │ │ ├── useAuth.ts │ │ ├── useDashboard.ts │ │ ├── useFieldActivities.ts │ │ ├── useGrid.ts │ │ ├── useLot.ts │ │ ├── useMap.ts │ │ ├── useMicroControl.ts │ │ ├── usePIT.ts │ │ └── useSubphases.ts │ ├── index.css │ ├── lib │ │ ├── axios.ts │ │ ├── queryClient.ts │ │ └── theme.ts │ ├── main.tsx │ ├── routes │ │ ├── ErrorBoundaryRoute.tsx │ │ ├── NotFound.tsx │ │ ├── Unauthorized.tsx │ │ └── index.tsx │ ├── services │ │ ├── activityMonitoringService.ts │ │ ├── activityService.ts │ │ ├── authService.ts │ │ ├── dashboardService.ts │ │ ├── fieldActivitiesService.ts │ │ ├── gridService.ts │ │ ├── lotService.ts │ │ ├── mapService.ts │ │ ├── pitService.ts │ │ └── subphaseService.ts │ ├── stores │ │ ├── authStore.ts │ │ ├── fieldActivitiesStore.ts │ │ └── mapStore.ts │ ├── types │ │ ├── activity.ts │ │ ├── api.ts │ │ ├── auth.ts │ │ ├── dashboard.ts │ │ ├── fieldActivities.ts │ │ ├── grid.ts │ │ ├── lot.ts │ │ ├── map.ts │ │ ├── microControl.ts │ │ ├── pit.ts │ │ └── subphase.ts │ ├── utils │ │ ├── apiErrorHandler.ts │ │ ├── dateFormatters.ts │ │ └── formatters.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── create_build.js ├── create_config.js ├── er ├── acompanhamento.sql ├── campo.sql ├── dgeo.sql ├── dominio.sql ├── macrocontrole.sql ├── metadado.sql ├── permissao.sql ├── recurso_humano.sql └── versao.sql ├── er_microcontrole ├── microcontrole.sql ├── permissao.sql └── versao.sql ├── package-lock.json ├── package.json ├── server ├── package-lock.json ├── package.json └── src │ ├── acompanhamento │ ├── acompanhamento_ctrl.js │ ├── acompanhamento_route.js │ ├── acompanhamento_schema.js │ └── index.js │ ├── authentication │ ├── authenticate_user.js │ ├── get_usuarios.js │ ├── index.js │ └── verify_server.js │ ├── campo │ ├── campo_ctrl.js │ ├── campo_route.js │ ├── campo_schema.js │ └── index.js │ ├── config.js │ ├── database │ ├── database_version.js │ ├── database_version_microcontrole.js │ ├── db.js │ ├── disable_triggers.js │ ├── index.js │ ├── manage_permissions.js │ ├── sql │ │ ├── revoke.sql │ │ └── revoke_all_users.sql │ ├── sql_file.js │ └── temporary_login.js │ ├── gerencia │ ├── gerencia_ctrl.js │ ├── gerencia_route.js │ ├── gerencia_schema.js │ ├── index.js │ └── qgis_project.js │ ├── gerenciador_fme │ ├── check_connection.js │ ├── index.js │ └── validate_parameters.js │ ├── index.js │ ├── login │ ├── index.js │ ├── login_ctrl.js │ ├── login_route.js │ ├── login_schema.js │ ├── validate_token.js │ ├── verify_admin.js │ └── verify_login.js │ ├── main.js │ ├── metadados │ ├── index.js │ ├── metadados_ctrl.js │ ├── metadados_route.js │ └── metadados_schema.js │ ├── microcontrole │ ├── index.js │ ├── microcontrole_ctrl.js │ ├── microcontrole_route.js │ └── microcontrole_schema.js │ ├── perigo │ ├── index.js │ ├── perigo_ctrl.js │ ├── perigo_route.js │ └── perigo_schema.js │ ├── producao │ ├── index.js │ ├── prepared_statements.js │ ├── producao_ctrl.js │ ├── producao_docs.js │ ├── producao_route.js │ ├── producao_schema.js │ └── sql │ │ ├── calcula_fila.sql │ │ ├── calcula_fila_pausada.sql │ │ ├── calcula_fila_prioritaria.sql │ │ ├── calcula_fila_prioritaria_grupo.sql │ │ └── retorna_dados_producao.sql │ ├── projeto │ ├── index.js │ ├── projeto_ctrl.js │ ├── projeto_route.js │ └── projeto_schema.js │ ├── rh │ ├── index.js │ ├── rh_ctrl.js │ ├── rh_route.js │ └── rh_schema.js │ ├── routes.js │ ├── server │ ├── app.js │ ├── index.js │ ├── start_server.js │ └── swagger_options.js │ ├── templates │ ├── .gitkeep │ └── sap_config_template.qgs │ ├── usuario │ ├── index.js │ ├── usuario_ctrl.js │ ├── usuario_route.js │ └── usuario_schema.js │ └── utils │ ├── app_error.js │ ├── async_handler.js │ ├── async_handler_with_queue.js │ ├── error_handler.js │ ├── http_code.js │ ├── index.js │ ├── logger.js │ ├── schema_validation.js │ └── send_json_and_log.js └── version_update ├── update_210_220.sql ├── update_220_221.sql ├── update_221_222.sql └── update_222_223.sql /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | 3 | # Logs 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Optional npm cache directory 9 | .npm 10 | 11 | # Dependency directories 12 | node_modules 13 | /jspm_packages 14 | /bower_components 15 | /.pnp 16 | .pnp.js 17 | 18 | # testing 19 | /coverage 20 | 21 | # Yarn Integrity file 22 | .yarn-integrity 23 | 24 | # Optional eslint cache 25 | .eslintcache 26 | 27 | #Build generated 28 | dist/ 29 | build/ 30 | js_docs/ 31 | 32 | # Serverless generated files 33 | .serverless/ 34 | 35 | ### SublimeText ### 36 | # cache files for sublime text 37 | *.tmlanguage.cache 38 | *.tmPreferences.cache 39 | *.stTheme.cache 40 | 41 | # workspace files are user-specific 42 | *.sublime-workspace 43 | 44 | # project files should be checked into the repository, unless a significant 45 | # proportion of contributors will probably not be using SublimeText 46 | # *.sublime-project 47 | 48 | 49 | ### VisualStudioCode ### 50 | .vscode/* 51 | !.vscode/settings.json 52 | !.vscode/tasks.json 53 | !.vscode/launch.json 54 | !.vscode/extensions.json 55 | 56 | ### Vim ### 57 | *.sw[a-p] 58 | 59 | ### WebStorm/IntelliJ ### 60 | /.idea 61 | modules.xml 62 | *.ipr 63 | 64 | 65 | ### System Files ### 66 | *.DS_Store 67 | 68 | # Windows thumbnail cache files 69 | Thumbs.db 70 | ehthumbs.db 71 | ehthumbs_vista.db 72 | 73 | # Folder config file 74 | Desktop.ini 75 | 76 | # Recycle Bin used on file shares 77 | $RECYCLE.BIN/ 78 | 79 | # Thumbnails 80 | ._* 81 | 82 | # Files that might appear in the root of a volume 83 | .DocumentRevisions-V100 84 | .fseventsd 85 | .Spotlight-V100 86 | .TemporaryItems 87 | .Trashes 88 | .VolumeIcon.icns 89 | .com.apple.timemachine.donotpresent 90 | 91 | #Log files 92 | *.log 93 | *.log.gz 94 | .audit.json 95 | 96 | # APIDOC 97 | src/apidoc 98 | logs 99 | log 100 | .vscode 101 | 102 | sap_config.qgs 103 | sap_config.qgs~ 104 | 105 | 106 | config.env 107 | config_test.env 108 | 109 | sslcert 110 | 111 | test_request.http 112 | 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present 1ºCGEO / DSG / Exército Brasileiro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sistema de Apoio a Produção (SAP) 2 | 3 | Serviço em Node.js para gerenciamento da produção cartográfica. 4 | 5 | As vantagens são: 6 | * Distribuição automática de atividades aos operadores 7 | * Configuração de camadas, estilos, menus, regras, rotinas que o operador irá receber no QGIS de acordo com a peculiaridade da subfase de produção 8 | * Distribuição de insumos para a atividade 9 | * Controle de permissões do banco de dados PostgreSQL 10 | * Possibilidade de configuração de uma fila de atividades conforme as necessidades do projeto e as habilitações do operador 11 | * Centralização das informações de produção, como data de ínicio, data de fim e quem realizou a atividade 12 | * Acompanhamento gráfico da produção 13 | * Monitoramento da produção por feição, apontamentos ou tela (microcontrole da produção) 14 | * Possibilidade de trabalho em banco de dados geoespaciais contínuo, apresentando somente o subconjunto necessário dos dados para o operador 15 | * Geração de metadados compatíveis com a ET-PCDG 16 | * Integração com [Gerenciador do FME](https://github.com/1cgeo/gerenciador_fme) 17 | * Integração com [DSGTools](https://github.com/dsgoficial/DsgTools) 18 | 19 | Para sua utilização é necessária a utilização do [Serviço de Autenticação](https://github.com/1cgeo/auth_server) 20 | 21 | Para acesso ao cliente QGIS do usuário verifique [SAP Operador](https://github.com/dsgoficial/SAP_Operador) 22 | 23 | Para acesso ao cliente QGIS do gerente verifique [SAP Gerente](https://github.com/dsgoficial/SAP_Gerente) 24 | -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | # .env.example 2 | 3 | # API Configuration 4 | VITE_API_URL=http://localhost:3013 -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # .gitignore 2 | 3 | # Dependências 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # Testes 9 | /coverage 10 | 11 | # Produção 12 | /build 13 | /dist 14 | 15 | # Misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | .env 22 | 23 | # Logs 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | pnpm-debug.log* 28 | lerna-debug.log* 29 | 30 | # Editor directories and files 31 | .vscode/* 32 | !.vscode/extensions.json 33 | !.vscode/settings.json 34 | .idea 35 | .DS_Store 36 | *.suo 37 | *.ntvs* 38 | *.njsproj 39 | *.sln 40 | *.sw? 41 | 42 | # TypeScript 43 | *.tsbuildinfo 44 | 45 | package-lock.json -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "arrowParens": "avoid", 8 | "endOfLine": "auto", 9 | "bracketSpacing": true, 10 | "jsxBracketSameLine": false 11 | } -------------------------------------------------------------------------------- /client/dev/add_comments.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import ignore from 'ignore'; 4 | import { fileURLToPath } from 'url'; 5 | import { dirname } from 'path'; 6 | 7 | // Define the source folder name 8 | const SRC_FOLDER_NAME = 'src'; 9 | 10 | // Lista de pastas para ignorar (mantenha como antes) 11 | const FOLDERS_TO_IGNORE = ['.git', 'node_modules', 'vendors', 'images', 'assets']; 12 | 13 | function readGitignore(projectRoot) { 14 | const gitignorePath = path.join(projectRoot, '.gitignore'); 15 | if (fs.existsSync(gitignorePath)) { 16 | const content = fs.readFileSync(gitignorePath, 'utf8'); 17 | return ignore().add(content.split('\n')); 18 | } 19 | return ignore(); 20 | } 21 | 22 | function shouldIgnore(item, relativePath, ig) { 23 | // Verifica se o item está na lista de pastas para ignorar 24 | if (FOLDERS_TO_IGNORE.includes(item)) { 25 | return true; 26 | } 27 | // Verifica se o item deve ser ignorado pelo .gitignore 28 | return ig.ignores(relativePath); 29 | } 30 | 31 | // Function to process .ts and .tsx files and add comment 32 | function processTsFiles(startDir, currentDir, ig) { 33 | const items = fs.readdirSync(currentDir); 34 | 35 | for (const item of items) { 36 | const fullPath = path.join(currentDir, item); 37 | const relativePath = path.relative(startDir, fullPath); 38 | 39 | if (shouldIgnore(item, relativePath, ig)) continue; 40 | 41 | const stats = fs.statSync(fullPath); 42 | 43 | if (stats.isDirectory()) { 44 | processTsFiles(startDir, fullPath, ig); // Recursive call for directories 45 | } else { 46 | if (item.endsWith('.ts') || item.endsWith('.tsx')) { 47 | // Process only .ts and .tsx files 48 | addCommentToFile(startDir, fullPath, relativePath); 49 | } 50 | } 51 | } 52 | } 53 | 54 | function addCommentToFile(srcDir, filePath, relativePath) { 55 | try { 56 | let content = fs.readFileSync(filePath, 'utf8'); 57 | const comment = `// Path: ${relativePath}\n`; 58 | const lines = content.split('\n'); 59 | let firstLine = lines[0]; 60 | 61 | if (firstLine.startsWith('//')) { 62 | // First line is already a comment, replace it 63 | lines[0] = comment.trim(); // Replace the first line with the new comment, trim to remove extra newline 64 | content = lines.join('\n'); 65 | fs.writeFileSync(filePath, content, 'utf8'); 66 | console.log(`Comment updated in: ${relativePath}`); 67 | } else { 68 | // First line is not a comment, prepend the comment 69 | content = comment + content; 70 | fs.writeFileSync(filePath, content, 'utf8'); 71 | console.log(`Comment added to: ${relativePath}`); 72 | } 73 | } catch (error) { 74 | console.error(`Error processing file ${relativePath}: ${error.message}`); 75 | } 76 | } 77 | 78 | function main() { 79 | const __filename = fileURLToPath(import.meta.url); 80 | const __dirname = dirname(__filename); 81 | const scriptDirPath = __dirname; 82 | const projectRoot = path.dirname(scriptDirPath); // Go up one level to project root 83 | const srcDirPath = path.join(projectRoot, SRC_FOLDER_NAME); 84 | 85 | if (!fs.existsSync(srcDirPath)) { 86 | console.error(`Error: Folder '${SRC_FOLDER_NAME}' not found at: ${srcDirPath}`); 87 | return; 88 | } 89 | 90 | console.log(`Running script from src folder: ${srcDirPath}`); 91 | 92 | const ig = readGitignore(projectRoot); // Gitignore from project root 93 | processTsFiles(srcDirPath, srcDirPath, ig); // Start processing files from src folder 94 | 95 | console.log("Finished processing .ts and .tsx files."); 96 | } 97 | 98 | main(); -------------------------------------------------------------------------------- /client/dev/copy_to_src_total.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import ignore from 'ignore'; 4 | import { fileURLToPath } from 'url'; 5 | import { dirname } from 'path'; 6 | 7 | // Define folder names 8 | const SRC_FOLDER_NAME = 'src'; 9 | const DEST_FOLDER_NAME = 'src_total'; 10 | 11 | // Lista de pastas para ignorar 12 | const FOLDERS_TO_IGNORE = ['.git', 'node_modules', 'vendors', 'images', 'assets']; 13 | 14 | function readGitignore(projectRoot) { 15 | const gitignorePath = path.join(projectRoot, '.gitignore'); 16 | if (fs.existsSync(gitignorePath)) { 17 | const content = fs.readFileSync(gitignorePath, 'utf8'); 18 | return ignore().add(content.split('\n')); 19 | } 20 | return ignore(); 21 | } 22 | 23 | function shouldIgnore(item, relativePath, ig) { 24 | // Verifica se o item está na lista de pastas para ignorar 25 | if (FOLDERS_TO_IGNORE.includes(item)) { 26 | return true; 27 | } 28 | // Verifica se o item deve ser ignorado pelo .gitignore 29 | return ig.ignores(relativePath); 30 | } 31 | 32 | // Function to copy and rename files from src to src_total 33 | function copyFilesToDestination(srcDir, destDir, currentDir, ig) { 34 | const items = fs.readdirSync(currentDir); 35 | 36 | for (const item of items) { 37 | const fullPath = path.join(currentDir, item); 38 | const relativePath = path.relative(srcDir, fullPath); 39 | 40 | if (shouldIgnore(item, relativePath, ig)) continue; 41 | 42 | const stats = fs.statSync(fullPath); 43 | 44 | if (stats.isDirectory()) { 45 | // Recursive call for directories 46 | copyFilesToDestination(srcDir, destDir, fullPath, ig); 47 | } else { 48 | // Process the file 49 | const pathParts = relativePath.split(path.sep); 50 | const newFileName = pathParts.join('_'); 51 | const destPath = path.join(destDir, newFileName); 52 | 53 | // Copy the file with the new name 54 | fs.copyFileSync(fullPath, destPath); 55 | console.log(`Copied: ${relativePath} -> ${newFileName}`); 56 | } 57 | } 58 | } 59 | 60 | function ensureDirectoryExists(dirPath) { 61 | if (!fs.existsSync(dirPath)) { 62 | fs.mkdirSync(dirPath, { recursive: true }); 63 | console.log(`Created directory: ${dirPath}`); 64 | } 65 | } 66 | 67 | function main() { 68 | const __filename = fileURLToPath(import.meta.url); 69 | const __dirname = dirname(__filename); 70 | const scriptDirPath = __dirname; 71 | const projectRoot = path.dirname(scriptDirPath); // Go up one level to project root 72 | const srcDirPath = path.join(projectRoot, SRC_FOLDER_NAME); 73 | const destDirPath = path.join(projectRoot, DEST_FOLDER_NAME); 74 | 75 | if (!fs.existsSync(srcDirPath)) { 76 | console.error(`Error: Source folder '${SRC_FOLDER_NAME}' not found at: ${srcDirPath}`); 77 | return; 78 | } 79 | 80 | // Ensure destination directory exists 81 | ensureDirectoryExists(destDirPath); 82 | 83 | console.log(`Copying files from ${srcDirPath} to ${destDirPath}`); 84 | 85 | const ig = readGitignore(projectRoot); // Gitignore from project root 86 | copyFilesToDestination(srcDirPath, destDirPath, srcDirPath, ig); 87 | 88 | console.log("Finished copying and renaming files."); 89 | } 90 | 91 | main(); -------------------------------------------------------------------------------- /client/eslint.config.js: -------------------------------------------------------------------------------- 1 | import reactPlugin from 'eslint-plugin-react'; 2 | import hooksPlugin from 'eslint-plugin-react-hooks'; 3 | 4 | export default [ 5 | { 6 | files: ['src/**/*.{ts,tsx}'], 7 | languageOptions: { 8 | ecmaVersion: 'latest', 9 | sourceType: 'module', 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true 13 | } 14 | } 15 | }, 16 | plugins: { 17 | 'react': reactPlugin, 18 | 'react-hooks': hooksPlugin 19 | }, 20 | rules: { 21 | 'react/react-in-jsx-scope': 'off', 22 | 'react-hooks/rules-of-hooks': 'error', 23 | 'react-hooks/exhaustive-deps': 'warn', 24 | 'no-unused-vars': 'warn', 25 | 'no-undef': 'error' 26 | } 27 | } 28 | ]; -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | SAP - Sistema de Apoio à Produção 11 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sap-client-modern", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "type-check": "tsc -p tsconfig.json --noEmit", 11 | "lint": "eslint \"src/**/*.{ts,tsx}\" --fix", 12 | "analyze": "vite-bundle-visualizer", 13 | "knip": "knip", 14 | "format": "prettier --write \"src/**/*.{ts,tsx,css,md}\"" 15 | }, 16 | "dependencies": { 17 | "@emotion/react": "^11.14.0", 18 | "@emotion/styled": "^11.14.0", 19 | "@hookform/resolvers": "^4.1.2", 20 | "@mui/icons-material": "^6.4.6", 21 | "@mui/material": "^6.4.6", 22 | "@tanstack/react-query": "^5.66.11", 23 | "@tanstack/react-query-devtools": "^5.66.11", 24 | "axios": "^1.8.1", 25 | "d3": "^7.9.0", 26 | "eslint-plugin-react": "^7.37.4", 27 | "maplibre-gl": "^5.2.0", 28 | "notistack": "^3.0.2", 29 | "react": "^19.0.0", 30 | "react-dom": "^19.0.0", 31 | "react-hook-form": "^7.54.2", 32 | "react-map-gl": "^8.0.1", 33 | "react-router-dom": "^7.2.0", 34 | "recharts": "^2.15.1", 35 | "zod": "^3.24.2", 36 | "zustand": "^5.0.3" 37 | }, 38 | "devDependencies": { 39 | "@types/d3": "^7.4.3", 40 | "@types/node": "^22.13.8", 41 | "@types/react": "^19.0.10", 42 | "@types/react-dom": "^19.0.4", 43 | "@vitejs/plugin-react": "^4.3.4", 44 | "eslint": "^9.21.0", 45 | "eslint-plugin-react-hooks": "^5.2.0", 46 | "knip": "^5.45.0", 47 | "prettier": "^3.5.2", 48 | "typescript": "^5.8.2", 49 | "vite": "^6.2.0", 50 | "vite-bundle-visualizer": "^1.2.1", 51 | "vite-plugin-compression": "^0.5.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/public/images/img-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsgoficial/sap/d4a251bed651e687f5f39431551b2c0774b5a722/client/public/images/img-1.jpg -------------------------------------------------------------------------------- /client/public/images/img-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsgoficial/sap/d4a251bed651e687f5f39431551b2c0774b5a722/client/public/images/img-2.jpg -------------------------------------------------------------------------------- /client/public/images/img-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsgoficial/sap/d4a251bed651e687f5f39431551b2c0774b5a722/client/public/images/img-3.jpg -------------------------------------------------------------------------------- /client/public/images/img-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsgoficial/sap/d4a251bed651e687f5f39431551b2c0774b5a722/client/public/images/img-4.jpg -------------------------------------------------------------------------------- /client/public/images/img-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsgoficial/sap/d4a251bed651e687f5f39431551b2c0774b5a722/client/public/images/img-5.jpg -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | // Path: App.tsx 2 | import { QueryClientProvider } from '@tanstack/react-query'; 3 | import { CssBaseline } from '@mui/material'; 4 | import { SnackbarProvider } from 'notistack'; 5 | import queryClient from './lib/queryClient'; 6 | import router from './routes'; // Import the router configuration 7 | import ErrorBoundary from './components/ui/ErrorBoundary'; 8 | import { RouterProvider } from 'react-router-dom'; 9 | import { ThemeProvider } from './contexts/ThemeContext'; 10 | 11 | const App = () => { 12 | return ( 13 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /client/src/components/Page/Page.tsx: -------------------------------------------------------------------------------- 1 | // Path: components\Page\Page.tsx 2 | import React, { ReactNode } from 'react'; 3 | import { Box } from '@mui/material'; 4 | 5 | interface PageProps { 6 | children: ReactNode; 7 | title?: string; 8 | description?: string; 9 | meta?: React.DetailedHTMLProps< 10 | React.MetaHTMLAttributes, 11 | HTMLMetaElement 12 | >[]; 13 | } 14 | 15 | const Page = ({ children, title = '', description = '', meta }: PageProps) => { 16 | return ( 17 | <> 18 | 19 | {title ? `${title} | SAP` : 'SAP - Sistema de Apoio à Produção'} 20 | 21 | {description && } 22 | {/* Additional meta tags */} 23 | {meta && meta.map((item, index) => )} 24 | {children} 25 | 26 | ); 27 | }; 28 | 29 | export default Page; 30 | -------------------------------------------------------------------------------- /client/src/components/charts/ChartThemeConfig.ts: -------------------------------------------------------------------------------- 1 | // Path: components\charts\ChartThemeConfig.ts 2 | import { useTheme } from '@mui/material/styles'; 3 | 4 | /** 5 | * Hook to provide theme-aware colors for charts and data visualizations 6 | * Used to make charts respond to light/dark mode changes 7 | */ 8 | export const useChartColors = () => { 9 | const theme = useTheme(); 10 | const isDark = theme.palette.mode === 'dark'; 11 | 12 | return { 13 | // Base colors for charts 14 | primary: theme.palette.primary.main, 15 | secondary: theme.palette.secondary.main, 16 | success: theme.palette.success.main, 17 | error: theme.palette.error.main, 18 | warning: theme.palette.warning.main, 19 | info: theme.palette.info.main, 20 | 21 | // Text colors 22 | textPrimary: theme.palette.text.primary, 23 | textSecondary: theme.palette.text.secondary, 24 | 25 | // Background colors 26 | background: theme.palette.background.default, 27 | paper: theme.palette.background.paper, 28 | 29 | // Status colors (for different chart elements) 30 | completed: isDark ? '#AAF27F' : '#54D62C', 31 | running: isDark ? '#74CAFF' : '#1890FF', 32 | notStarted: isDark ? '#919EAB' : '#637381', 33 | 34 | // Series colors for bar charts, pie charts, etc. 35 | seriesColors: [ 36 | isDark ? '#76B0F1' : '#2065D1', // primary blue 37 | isDark ? '#FF9777' : '#FF4842', // error 38 | isDark ? '#FFD666' : '#FFC107', // warning 39 | isDark ? '#AAF27F' : '#54D62C', // success 40 | isDark ? '#CE93D8' : '#9C27B0', // purple 41 | isDark ? '#90CAF9' : '#1976D2', // blue 42 | isDark ? '#FFAB91' : '#F4511E', // orange 43 | isDark ? '#B39DDB' : '#673AB7', // deep purple 44 | ], 45 | 46 | // Grid and axis colors 47 | grid: isDark ? 'rgba(145, 158, 171, 0.24)' : 'rgba(145, 158, 171, 0.16)', 48 | axis: isDark ? 'rgba(145, 158, 171, 0.4)' : 'rgba(145, 158, 171, 0.32)', 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /client/src/components/layouts/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | // Path: components\layouts\AppLayout.tsx 2 | import { useState } from 'react'; 3 | import { Outlet } from 'react-router-dom'; 4 | import { styled } from '@mui/material/styles'; 5 | import { Box, useMediaQuery, useTheme } from '@mui/material'; 6 | import DashboardSidebar from './AppSidebar'; 7 | import DashboardNavbar from './AppNavbar'; 8 | 9 | // Constants 10 | const APP_BAR_MOBILE = 64; 11 | const APP_BAR_DESKTOP = 92; 12 | const DRAWER_WIDTH = 310; 13 | 14 | // Styled components 15 | const RootStyle = styled(Box)(({ theme: _theme }) => ({ 16 | display: 'flex', 17 | minHeight: '100%', 18 | overflow: 'hidden', 19 | })); 20 | 21 | const MainStyle = styled(Box)<{ open?: boolean }>(({ theme, open }) => ({ 22 | flexGrow: 1, 23 | overflow: 'auto', 24 | minHeight: '100%', 25 | paddingTop: APP_BAR_MOBILE + 24, 26 | paddingBottom: theme.spacing(10), 27 | paddingLeft: theme.spacing(2), 28 | paddingRight: theme.spacing(2), 29 | [theme.breakpoints.up('lg')]: { 30 | paddingTop: APP_BAR_DESKTOP + 24, 31 | width: open ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%', 32 | transition: theme.transitions.create(['width'], { 33 | easing: theme.transitions.easing.sharp, 34 | duration: theme.transitions.duration.enteringScreen, 35 | }), 36 | }, 37 | [theme.breakpoints.down('sm')]: { 38 | paddingTop: APP_BAR_MOBILE + 16, 39 | paddingLeft: theme.spacing(1), 40 | paddingRight: theme.spacing(1), 41 | paddingBottom: theme.spacing(6), 42 | }, 43 | })); 44 | 45 | const DashboardLayout = () => { 46 | const [mobileOpen, setMobileOpen] = useState(false); 47 | const [desktopOpen, setDesktopOpen] = useState(true); 48 | const theme = useTheme(); 49 | const isMobile = useMediaQuery(theme.breakpoints.down('lg')); 50 | 51 | // Toggle for mobile drawer 52 | const handleMobileDrawerToggle = () => { 53 | setMobileOpen(!mobileOpen); 54 | }; 55 | 56 | // Toggle for desktop drawer 57 | const handleDesktopDrawerToggle = () => { 58 | setDesktopOpen(!desktopOpen); 59 | }; 60 | 61 | return ( 62 | 63 | 68 | 69 | 76 | 77 | 78 | 79 | 80 | 81 | ); 82 | }; 83 | 84 | export default DashboardLayout; 85 | -------------------------------------------------------------------------------- /client/src/components/layouts/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | // Path: components\layouts\AuthLayout.tsx 2 | import { ReactNode } from 'react'; 3 | import { Box, Container, Typography, alpha } from '@mui/material'; 4 | import { styled } from '@mui/material/styles'; 5 | import { useThemeMode } from '@/contexts/ThemeContext'; 6 | 7 | interface AuthLayoutProps { 8 | children: ReactNode; 9 | title?: string; 10 | backgroundImageNumber?: number; 11 | maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; 12 | } 13 | 14 | // Generate a random number between 1 and 5 for background image if not provided 15 | const defaultImageNumber = Math.floor(Math.random() * 5) + 1; 16 | 17 | const BackgroundBox = styled(Box)(({ theme }) => ({ 18 | minHeight: '100vh', 19 | width: '100vw', 20 | display: 'flex', 21 | flexDirection: 'column', 22 | alignItems: 'center', 23 | justifyContent: 'center', 24 | backgroundSize: 'cover', 25 | backgroundPosition: 'center', 26 | backgroundRepeat: 'no-repeat', 27 | backgroundAttachment: 'fixed', 28 | position: 'relative', 29 | // Set background color as fallback 30 | backgroundColor: 31 | theme.palette.mode === 'dark' 32 | ? theme.palette.background.default 33 | : '#f5f5f5', 34 | '&::before': { 35 | content: '""', 36 | position: 'absolute', 37 | top: 0, 38 | left: 0, 39 | right: 0, 40 | bottom: 0, 41 | // Add appropriate overlay for each theme 42 | backgroundColor: 43 | theme.palette.mode === 'dark' 44 | ? alpha(theme.palette.common.black, 0.4) // Darker overlay for dark mode 45 | : alpha(theme.palette.common.white, 0.1), // Lighter overlay for light mode 46 | zIndex: 1, 47 | }, 48 | })); 49 | 50 | const ContentContainer = styled(Container)(({ theme }) => ({ 51 | position: 'relative', 52 | zIndex: 2, 53 | padding: theme.spacing(4), 54 | [theme.breakpoints.down('sm')]: { 55 | padding: theme.spacing(2), 56 | }, 57 | })); 58 | 59 | export const AuthLayout = ({ 60 | children, 61 | title, 62 | backgroundImageNumber = defaultImageNumber, 63 | maxWidth = 'sm', 64 | }: AuthLayoutProps) => { 65 | const { isDarkMode } = useThemeMode(); 66 | 67 | return ( 68 | 73 | 74 | {title && ( 75 | 83 | {title} 84 | 85 | )} 86 | 87 | {children} 88 | 89 | 90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /client/src/components/timeline/TimelineVisualization.tsx: -------------------------------------------------------------------------------- 1 | // Path: components\timeline\TimelineVisualization.tsx 2 | import React from 'react'; 3 | import { 4 | EnhancedTimeline, 5 | TimelineGroup, 6 | TimelineItem, 7 | } from './EnhancedTimeline'; 8 | import { Box } from '@mui/material'; 9 | 10 | interface VisavailDataset { 11 | measure: string; 12 | data: Array<[string, string, string]>; // [start_date, status, end_date] 13 | } 14 | 15 | interface VisavailOptions { 16 | title: { 17 | text: string; 18 | }; 19 | id_div_container: string; 20 | id_div_graph: string; 21 | date_in_utc?: boolean; 22 | line_spacing?: number; 23 | tooltip?: { 24 | height?: number; 25 | position?: string; 26 | left_spacing?: number; 27 | only_first_date?: boolean; 28 | date_plus_time?: boolean; 29 | }; 30 | responsive?: { 31 | enabled?: boolean; 32 | }; 33 | [key: string]: any; 34 | } 35 | 36 | interface TimelineVisualizationProps { 37 | idContainer?: string; 38 | idBar?: string; 39 | options: VisavailOptions; 40 | dataset: VisavailDataset[]; 41 | } 42 | 43 | export const TimelineVisualization: React.FC = ({ 44 | options, 45 | dataset, 46 | }) => { 47 | const transformedGroups: TimelineGroup[] = dataset.map(item => { 48 | const timelineItems: TimelineItem[] = item.data.map( 49 | ([startDateStr, status, endDateStr]) => { 50 | // Parse dates 51 | const startDate = new Date(startDateStr); 52 | const endDate = new Date(endDateStr); 53 | 54 | return { 55 | startDate, 56 | endDate, 57 | status, // Keep status as is - the EnhancedTimeline now handles both string and number formats 58 | label: `${startDateStr} - ${endDateStr}`, 59 | }; 60 | }, 61 | ); 62 | 63 | return { 64 | title: item.measure, 65 | data: timelineItems, 66 | }; 67 | }); 68 | 69 | return ( 70 | 71 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /client/src/contexts/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | // Path: contexts\ThemeContext.tsx 2 | import React, { 3 | createContext, 4 | useContext, 5 | useState, 6 | useEffect, 7 | useMemo, 8 | ReactNode, 9 | } from 'react'; 10 | import { ThemeProvider as MUIThemeProvider } from '@mui/material/styles'; 11 | import { lightTheme, darkTheme } from '@/lib/theme'; 12 | 13 | // Type for the theme context 14 | interface ThemeContextType { 15 | isDarkMode: boolean; 16 | toggleTheme: () => void; 17 | } 18 | 19 | // Create theme context with default values 20 | const ThemeContext = createContext({ 21 | isDarkMode: false, 22 | toggleTheme: () => {}, 23 | }); 24 | 25 | // Custom hook to use the theme context 26 | export const useThemeMode = () => useContext(ThemeContext); 27 | 28 | interface ThemeProviderProps { 29 | children: ReactNode; 30 | } 31 | 32 | // Theme provider component 33 | export const ThemeProvider: React.FC = ({ children }) => { 34 | // Initialize theme from localStorage or system preference 35 | const [isDarkMode, setIsDarkMode] = useState(() => { 36 | // First check localStorage 37 | const savedMode = localStorage.getItem('sap-theme-mode'); 38 | if (savedMode !== null) { 39 | return savedMode === 'dark'; 40 | } 41 | 42 | // Fallback to system preference 43 | return ( 44 | window.matchMedia && 45 | window.matchMedia('(prefers-color-scheme: dark)').matches 46 | ); 47 | }); 48 | 49 | // Toggle theme function 50 | const toggleTheme = () => { 51 | setIsDarkMode(prevMode => !prevMode); 52 | }; 53 | 54 | // Save theme preference to localStorage when it changes 55 | useEffect(() => { 56 | localStorage.setItem('sap-theme-mode', isDarkMode ? 'dark' : 'light'); 57 | 58 | // Update meta theme-color for mobile browsers 59 | const metaThemeColor = document.querySelector('meta[name="theme-color"]'); 60 | if (metaThemeColor) { 61 | metaThemeColor.setAttribute( 62 | 'content', 63 | isDarkMode ? '#212B36' : '#2065D1', 64 | ); 65 | } 66 | }, [isDarkMode]); 67 | 68 | // Listen for system preference changes 69 | useEffect(() => { 70 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 71 | 72 | const handleChange = (e: MediaQueryListEvent) => { 73 | // Only update if user hasn't explicitly set a preference 74 | if (localStorage.getItem('sap-theme-mode') === null) { 75 | setIsDarkMode(e.matches); 76 | } 77 | }; 78 | 79 | // Add event listener 80 | if (mediaQuery.addEventListener) { 81 | mediaQuery.addEventListener('change', handleChange); 82 | } else { 83 | // For older browsers 84 | mediaQuery.addListener(handleChange); 85 | } 86 | 87 | // Cleanup 88 | return () => { 89 | if (mediaQuery.removeEventListener) { 90 | mediaQuery.removeEventListener('change', handleChange); 91 | } else { 92 | // For older browsers 93 | mediaQuery.removeListener(handleChange); 94 | } 95 | }; 96 | }, []); 97 | 98 | // Memoize context value to prevent unnecessary re-renders 99 | const contextValue = useMemo( 100 | () => ({ 101 | isDarkMode, 102 | toggleTheme, 103 | }), 104 | [isDarkMode], 105 | ); 106 | 107 | return ( 108 | 109 | 110 | {children} 111 | 112 | 113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /client/src/features/activities/components/FinishActivityDialog.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\activities\components\FinishActivityDialog.tsx 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogContentText, 7 | DialogActions, 8 | Button, 9 | } from '@mui/material'; 10 | 11 | interface FinishActivityDialogProps { 12 | open: boolean; 13 | onClose: () => void; 14 | onConfirm: () => void; 15 | isSubmitting: boolean; 16 | } 17 | 18 | export const FinishActivityDialog = ({ 19 | open, 20 | onClose, 21 | onConfirm, 22 | isSubmitting, 23 | }: FinishActivityDialogProps) => { 24 | return ( 25 | 26 | Atenção 27 | 28 | Deseja finalizar a atividade? 29 | 30 | 31 | 34 | 42 | 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /client/src/features/activities/components/StartActivityDialog.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\activities\components\StartActivityDialog.tsx 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogContentText, 7 | DialogActions, 8 | Button, 9 | } from '@mui/material'; 10 | 11 | interface StartActivityDialogProps { 12 | open: boolean; 13 | onClose: () => void; 14 | onConfirm: () => void; 15 | isSubmitting: boolean; 16 | } 17 | 18 | export const StartActivityDialog = ({ 19 | open, 20 | onClose, 21 | onConfirm, 22 | isSubmitting, 23 | }: StartActivityDialogProps) => { 24 | return ( 25 | 26 | Atenção 27 | 28 | 29 | Deseja iniciar a próxima atividade? 30 | 31 | 32 | 33 | 36 | 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /client/src/features/activities/routes/Activity.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\activities\routes\Activity.tsx 2 | import { Container, Typography, Box } from '@mui/material'; 3 | import { ActivityCard } from '../components/ActivityCard'; 4 | 5 | export const Activity = () => { 6 | return ( 7 | 8 | 9 | Atividades do Usuário 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /client/src/features/auth/components/AuthStatus.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\auth\components\AuthStatus.tsx 2 | import { useMemo } from 'react'; 3 | import { Box, Chip, styled } from '@mui/material'; 4 | import { useIsAuthenticated, useUser } from '@/stores/authStore'; 5 | 6 | interface AuthStatusProps { 7 | showRole?: boolean; 8 | vertical?: boolean; 9 | } 10 | 11 | const StatusContainer = styled(Box, { 12 | shouldForwardProp: prop => prop !== 'vertical', 13 | })<{ vertical: boolean }>(({ theme, vertical }) => ({ 14 | display: 'flex', 15 | flexDirection: vertical ? 'column' : 'row', 16 | alignItems: 'center', 17 | gap: theme.spacing(1), 18 | })); 19 | 20 | export const AuthStatus = ({ 21 | showRole = true, 22 | vertical = false, 23 | }: AuthStatusProps) => { 24 | // Use custom hooks for better performance 25 | const isAuthenticated = useIsAuthenticated(); 26 | const user = useUser(); 27 | 28 | const roleColor = useMemo(() => { 29 | if (!user?.role) return 'default'; 30 | return user.role === 'ADMIN' ? 'primary' : 'success'; 31 | }, [user?.role]); 32 | 33 | const roleName = useMemo(() => { 34 | if (!user?.role) return 'Não autenticado'; 35 | return user.role === 'ADMIN' ? 'Administrador' : 'Usuário'; 36 | }, [user?.role]); 37 | 38 | if (!isAuthenticated) { 39 | return ( 40 | 41 | 42 | 43 | ); 44 | } 45 | 46 | return ( 47 | 48 | 49 | {showRole && ( 50 | 56 | )} 57 | 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /client/src/features/dashboard/components/StatusCards.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\dashboard\components\StatusCards.tsx 2 | import { 3 | Grid, 4 | Paper, 5 | Typography, 6 | Box, 7 | LinearProgress, 8 | useTheme, 9 | useMediaQuery, 10 | } from '@mui/material'; 11 | import { styled } from '@mui/material/styles'; 12 | import { DashboardSummary } from '@/types/dashboard'; 13 | 14 | interface StatusCardsProps { 15 | data: DashboardSummary; 16 | isLoading: boolean; 17 | } 18 | 19 | const StyledPaper = styled(Paper)(({ theme }) => ({ 20 | padding: theme.spacing(3), 21 | display: 'flex', 22 | flexDirection: 'column', 23 | minHeight: 150, 24 | height: 'auto', 25 | justifyContent: 'center', 26 | alignItems: 'center', 27 | [theme.breakpoints.down('sm')]: { 28 | padding: theme.spacing(2), 29 | minHeight: 120, 30 | }, 31 | })); 32 | 33 | const ProgressContainer = styled(Box)(({ theme }) => ({ 34 | width: '100%', 35 | marginTop: theme.spacing(2), 36 | [theme.breakpoints.down('sm')]: { 37 | marginTop: theme.spacing(1), 38 | }, 39 | })); 40 | 41 | export const StatusCards = ({ data, isLoading }: StatusCardsProps) => { 42 | const theme = useTheme(); 43 | const isMobile = useMediaQuery(theme.breakpoints.down('sm')); 44 | 45 | return ( 46 | 47 | 48 | 49 | 55 | Total de Produtos 56 | 57 | {isLoading ? ( 58 | 59 | 60 | 61 | ) : ( 62 | 63 | {data.totalProducts.toLocaleString()} 64 | 65 | )} 66 | 67 | 68 | 69 | 70 | 71 | 77 | Porcentagem Concluída 78 | 79 | {isLoading ? ( 80 | 81 | 82 | 83 | ) : ( 84 | <> 85 | 90 | {data.progressPercentage.toFixed(2)}% 91 | 92 | 93 | 101 | 102 | 103 | )} 104 | 105 | 106 | 107 | 108 | 109 | 115 | Produtos Concluídos 116 | 117 | {isLoading ? ( 118 | 119 | 120 | 121 | ) : ( 122 | 123 | {data.completedProducts.toLocaleString()} 124 | 125 | )} 126 | 127 | 128 | 129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /client/src/features/dashboard/routes/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\dashboard\routes\Dashboard.tsx 2 | import { 3 | Container, 4 | Typography, 5 | Box, 6 | Alert, 7 | CircularProgress, 8 | } from '@mui/material'; 9 | import { StatusCards } from '../components/StatusCards'; 10 | import { ProductionCharts } from '../components/ProductionCharts'; 11 | import { useDashboard } from '@/hooks/useDashboard'; 12 | 13 | export const Dashboard = () => { 14 | const { dashboardData, isLoading, isError, error } = useDashboard(); 15 | 16 | // Show loading state 17 | if (isLoading) { 18 | return ( 19 | 20 | 21 | Dashboard 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | // Show error state 31 | if (isError) { 32 | return ( 33 | 34 | 35 | Dashboard 36 | 37 | 38 | Erro ao carregar dados do dashboard:{' '} 39 | {error?.message || 'Tente novamente.'} 40 | 41 | 42 | ); 43 | } 44 | 45 | // If no data but no error either 46 | if (!dashboardData) { 47 | return ( 48 | 49 | 50 | Dashboard 51 | 52 | 53 | Nenhum dado disponível para exibição. 54 | 55 | 56 | ); 57 | } 58 | 59 | // Render dashboard with data 60 | return ( 61 | 62 | 63 | Dashboard 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /client/src/features/fieldActivities/routes/FieldActivities.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\fieldActivities\routes\FieldActivities.tsx 2 | import React from 'react'; 3 | import { Box, Typography, Paper, Alert } from '@mui/material'; 4 | import CampoMap from '../components/CampoMap'; 5 | import FieldActivitySidebar from '../components/FieldActivitySidebar'; 6 | import { useFieldActivities } from '@/hooks/useFieldActivities'; 7 | import Page from '@/components/Page/Page'; 8 | 9 | export const FieldActivities: React.FC = () => { 10 | const { error } = useFieldActivities(); 11 | 12 | return ( 13 | 14 | 15 | 16 | Atividades de Campo 17 | 18 | 19 | {error && ( 20 | 21 | Erro ao carregar dados: {error.message} 22 | 23 | )} 24 | 25 | 26 | 27 | 28 | 29 | {/* Sidebar component rendered outside Paper to allow proper drawer behavior */} 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default FieldActivities; 37 | -------------------------------------------------------------------------------- /client/src/features/grid/routes/Grids.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\grid\routes\Grids.tsx 2 | import { 3 | Container, 4 | Box, 5 | Typography, 6 | Alert, 7 | CircularProgress, 8 | } from '@mui/material'; 9 | import Page from '@/components/Page/Page'; 10 | import { useGridStatistics } from '@/hooks/useGrid'; 11 | import { GridCard } from '../components/GridCard'; 12 | import { GridData } from '@/types/grid'; 13 | 14 | export const Grids = () => { 15 | const { data, isLoading, error } = useGridStatistics(); 16 | if (isLoading) { 17 | return ( 18 | 19 | 20 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | if (error) { 35 | return ( 36 | 37 | 38 | 39 | Erro ao carregar dados da grade. Por favor, tente novamente. 40 | 41 | 42 | 43 | ); 44 | } 45 | 46 | return ( 47 | 48 | 49 | 50 | Grade de Acompanhamento 51 | 52 | 53 | 62 | {data && 63 | data.map((grid: GridData, idx: number) => ( 64 | 65 | ))} 66 | 67 | {data && data.length === 0 && ( 68 | 69 | Nenhuma grade de acompanhamento disponível. 70 | 71 | )} 72 | 73 | 74 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /client/src/features/map/components/LayerControl.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\map\components\LayerControl.tsx 2 | import React, { useCallback } from 'react'; 3 | import { 4 | List, 5 | ListItem, 6 | ListItemButton, 7 | ListItemIcon, 8 | ListItemText, 9 | Checkbox, 10 | Typography, 11 | useTheme, 12 | Tooltip, 13 | alpha, 14 | } from '@mui/material'; 15 | 16 | interface LayerInfo { 17 | id: string; 18 | name: string; 19 | } 20 | 21 | interface LayerControlProps { 22 | layers: LayerInfo[]; 23 | visibility: Record; 24 | onToggle: (layerId: string) => void; 25 | } 26 | 27 | const LayerControl: React.FC = ({ 28 | layers, 29 | visibility, 30 | onToggle, 31 | }) => { 32 | const theme = useTheme(); 33 | const isDarkMode = theme.palette.mode === 'dark'; 34 | 35 | // Memoize toggle handler to prevent recreation on each render 36 | const handleToggle = useCallback( 37 | (layerId: string) => { 38 | // Prevent event bubbling 39 | return (e: React.MouseEvent) => { 40 | e.stopPropagation(); 41 | onToggle(layerId); 42 | }; 43 | }, 44 | [onToggle], 45 | ); 46 | 47 | if (layers.length === 0) { 48 | return ( 49 | 50 | Nenhuma camada disponível 51 | 52 | ); 53 | } 54 | 55 | return ( 56 |
57 | 62 | Camadas 63 | 64 | 65 | {layers.map(layer => ( 66 | 67 | 80 | 81 | 98 | 99 | 100 | 113 | 114 | 115 | 116 | ))} 117 | 118 |
119 | ); 120 | }; 121 | 122 | export default React.memo(LayerControl); 123 | -------------------------------------------------------------------------------- /client/src/features/map/components/MapControls.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\map\components\MapControls.tsx 2 | import React from 'react'; 3 | import { Button, IconButton, useTheme, styled } from '@mui/material'; 4 | import InfoIcon from '@mui/icons-material/Info'; 5 | import LayersIcon from '@mui/icons-material/Layers'; 6 | import ChevronRightIcon from '@mui/icons-material/ChevronRight'; 7 | 8 | interface MapControlsProps { 9 | onToggleLegend: () => void; 10 | onToggleDrawer?: () => void; 11 | onToggleSidebar?: () => void; 12 | showLegend: boolean; 13 | isMobile: boolean; 14 | sidebarOpen: boolean; 15 | } 16 | 17 | const ControlButton = styled(Button)(({ theme }) => ({ 18 | position: 'absolute', 19 | zIndex: 1, 20 | color: theme.palette.getContrastText( 21 | theme.palette.mode === 'dark' 22 | ? theme.palette.primary.dark 23 | : theme.palette.primary.main, 24 | ), 25 | backgroundColor: 26 | theme.palette.mode === 'dark' 27 | ? theme.palette.primary.dark 28 | : theme.palette.primary.main, 29 | '&:hover': { 30 | backgroundColor: 31 | theme.palette.mode === 'dark' 32 | ? theme.palette.primary.main 33 | : theme.palette.primary.dark, 34 | }, 35 | fontWeight: 'bold', 36 | boxShadow: theme.shadows[2], 37 | })); 38 | 39 | const MapControls: React.FC = ({ 40 | onToggleLegend, 41 | onToggleDrawer, 42 | onToggleSidebar, 43 | showLegend, 44 | isMobile, 45 | sidebarOpen, 46 | }) => { 47 | const theme = useTheme(); 48 | 49 | return ( 50 | <> 51 | {/* Legend toggle button - at bottom left */} 52 | } 55 | size="small" 56 | variant="contained" 57 | color="secondary" 58 | sx={{ 59 | bottom: 10, 60 | left: 10, 61 | }} 62 | > 63 | {showLegend ? 'Ocultar Legenda' : 'Mostrar Legenda'} 64 | 65 | 66 | {/* Mobile layers button */} 67 | {isMobile && onToggleDrawer && ( 68 | 85 | 86 | 87 | )} 88 | 89 | {/* Sidebar toggle button for desktop */} 90 | {!isMobile && !sidebarOpen && onToggleSidebar && ( 91 | 107 | )} 108 | 109 | ); 110 | }; 111 | 112 | export default React.memo(MapControls); 113 | -------------------------------------------------------------------------------- /client/src/features/map/components/MapLegend.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\map\components\MapLegend.tsx 2 | import React from 'react'; 3 | import { Box, Typography, useTheme } from '@mui/material'; 4 | import { LegendItem } from '@/types/map'; 5 | 6 | interface MapLegendProps { 7 | items: LegendItem[]; 8 | } 9 | 10 | const MapLegend: React.FC = ({ items }) => { 11 | const theme = useTheme(); 12 | 13 | if (items.length === 0) { 14 | return null; 15 | } 16 | 17 | return ( 18 | 19 | 24 | Legenda 25 | 26 | 27 | {items.map((item, index) => ( 28 | 29 | 41 | 48 | {item.label} 49 | 50 | 51 | ))} 52 | 53 | 54 | ); 55 | }; 56 | 57 | export default React.memo(MapLegend); 58 | -------------------------------------------------------------------------------- /client/src/features/pit/routes/PIT.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\pit\routes\PIT.tsx 2 | import { 3 | Container, 4 | Box, 5 | Typography, 6 | Alert, 7 | CircularProgress, 8 | } from '@mui/material'; 9 | import Page from '@/components/Page/Page'; 10 | import { usePITData, PitViewModel } from '@/hooks/usePIT'; 11 | import { Table } from '@/components/ui/Table'; 12 | 13 | const MONTHS = [ 14 | { label: 'Jan', id: 'jan' }, 15 | { label: 'Fev', id: 'fev' }, 16 | { label: 'Mar', id: 'mar' }, 17 | { label: 'Abr', id: 'abr' }, 18 | { label: 'Mai', id: 'mai' }, 19 | { label: 'Jun', id: 'jun' }, 20 | { label: 'Jul', id: 'jul' }, 21 | { label: 'Ago', id: 'ago' }, 22 | { label: 'Set', id: 'set' }, 23 | { label: 'Out', id: 'out' }, 24 | { label: 'Nov', id: 'nov' }, 25 | { label: 'Dez', id: 'dez' }, 26 | ]; 27 | 28 | export const PIT = () => { 29 | const { data, isLoading, error } = usePITData(); 30 | 31 | if (isLoading) { 32 | return ( 33 | 34 | 35 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | 48 | if (error) { 49 | return ( 50 | 51 | 52 | 53 | Erro ao carregar dados de PIT. Por favor, tente novamente. 54 | 55 | 56 | 57 | ); 58 | } 59 | 60 | return ( 61 | 62 | 63 | 64 | PIT 65 | 66 | 67 | 76 | {data && 77 | data.map((item: PitViewModel, idx: number) => ( 78 | 79 | ({ 90 | id: m.id, 91 | label: m.label, 92 | align: 'center' as 'center', 93 | minWidth: 50, 94 | maxWidth: 70, 95 | })), 96 | { 97 | id: 'count', 98 | label: 'Quantitativo', 99 | align: 'center' as 'center', 100 | minWidth: 100, 101 | }, 102 | { 103 | id: 'percent', 104 | label: '(%)', 105 | align: 'center' as 'center', 106 | minWidth: 80, 107 | }, 108 | ]} 109 | rows={item.rows} 110 | rowKey={row => `${item.project}-${row.lot}`} 111 | /> 112 | 113 | ))} 114 | 115 | {data && data.length === 0 && ( 116 | Nenhum dado de PIT disponível. 117 | )} 118 | 119 | 120 | 121 | ); 122 | }; 123 | -------------------------------------------------------------------------------- /client/src/features/subphases/routes/SubphaseSituation.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\subphases\routes\SubphaseSituation.tsx 2 | import { 3 | Container, 4 | Box, 5 | Typography, 6 | Alert, 7 | CircularProgress, 8 | } from '@mui/material'; 9 | import Page from '../../../components/Page/Page'; 10 | import { useSubphaseSituation } from '@/hooks/useSubphases'; 11 | import { StackedBarChart } from '@/components/charts/StackedBarChart'; 12 | import { ChartGroup } from '@/types/subphase'; 13 | 14 | export const SubphaseSituation = () => { 15 | const { data, isLoading, error } = useSubphaseSituation(); 16 | 17 | if (isLoading) { 18 | return ( 19 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | if (error) { 35 | return ( 36 | 37 | 38 | 39 | Erro ao carregar dados de situação de subfases. Por favor, tente 40 | novamente. 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | return ( 48 | 49 | 50 | 51 | Situação Subfase 52 | 53 | 54 | 62 | {data && 63 | data.map((item: ChartGroup, idx: number) => ( 64 | ({ 68 | name: dataPoint.label, 69 | completed: dataPoint.y, 70 | notStarted: item.dataPointB[index]?.y || 0, 71 | }))} 72 | series={[ 73 | { 74 | dataKey: 'completed', 75 | name: 'Finalizadas', 76 | color: '#9bbb59', 77 | }, 78 | { 79 | dataKey: 'notStarted', 80 | name: 'Não Finalizadas', 81 | color: '#7f7f7f', 82 | }, 83 | ]} 84 | stacked100={true} 85 | /> 86 | ))} 87 | 88 | {data && data.length === 0 && ( 89 | 90 | Nenhuma situação de subfase disponível. 91 | 92 | )} 93 | 94 | 95 | 96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /client/src/features/subphases/routes/Subphases.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\subphases\routes\Subphases.tsx 2 | import { 3 | Container, 4 | Box, 5 | Typography, 6 | Alert, 7 | CircularProgress, 8 | Paper, 9 | useTheme, 10 | } from '@mui/material'; 11 | import Page from '@/components/Page/Page'; 12 | import { useActivitySubphase } from '@/hooks/useSubphases'; 13 | import { TimelineVisualization } from '@/components/timeline/TimelineVisualization'; 14 | 15 | export const Subphases = () => { 16 | const { data, isLoading, error } = useActivitySubphase(); 17 | const theme = useTheme(); 18 | 19 | if (isLoading) { 20 | return ( 21 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | if (error) { 37 | return ( 38 | 39 | 40 | 41 | Erro ao carregar dados de atividades por subfase. Por favor, tente 42 | novamente. 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | return ( 50 | 51 | 52 | 53 | Atividade por Subfase 54 | 55 | 56 | 64 | {data && 65 | data.map((graph, idx) => ( 66 | 81 | 87 | 88 | ))} 89 | 90 | {(!data || data.length === 0) && ( 91 | 92 | Nenhuma atividade por subfase disponível. 93 | 94 | )} 95 | 96 | 97 | 98 | ); 99 | }; 100 | -------------------------------------------------------------------------------- /client/src/features/subphases/routes/UserActivities.tsx: -------------------------------------------------------------------------------- 1 | // Path: features\subphases\routes\UserActivities.tsx 2 | import { 3 | Container, 4 | Box, 5 | Typography, 6 | Alert, 7 | CircularProgress, 8 | Paper, 9 | useTheme, 10 | } from '@mui/material'; 11 | import Page from '@/components/Page/Page'; 12 | import { useUserActivities } from '@/hooks/useSubphases'; 13 | import { TimelineVisualization } from '@/components/timeline/TimelineVisualization'; 14 | 15 | export const UserActivities = () => { 16 | const { data, isLoading, error } = useUserActivities(); 17 | const theme = useTheme(); 18 | 19 | if (isLoading) { 20 | return ( 21 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | if (error) { 37 | return ( 38 | 39 | 40 | 41 | Erro ao carregar dados de atividades por usuário. Por favor, tente 42 | novamente. 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | return ( 50 | 51 | 52 | 53 | Atividades por Usuário 54 | 55 | 56 | 70 | {data && 71 | data.map((graph, idx) => ( 72 | 79 | ))} 80 | 81 | {(!data || data.length === 0) && ( 82 | 83 | Nenhuma atividade por usuário disponível. 84 | 85 | )} 86 | 87 | 88 | 89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /client/src/hooks/useGrid.ts: -------------------------------------------------------------------------------- 1 | // Path: hooks\useGrid.ts 2 | import { useQuery } from '@tanstack/react-query'; 3 | import { getStatisticsGrid } from '@/services/gridService'; 4 | import { 5 | createQueryKey, 6 | STALE_TIMES, 7 | standardizeError, 8 | } from '@/lib/queryClient'; 9 | import { GridData } from '@/types/grid'; 10 | import { ApiResponse } from '@/types/api'; 11 | import { useEffect, useRef, useCallback } from 'react'; 12 | import { createCancelToken } from '@/utils/apiErrorHandler'; 13 | import axios from 'axios'; 14 | 15 | // Define query keys 16 | const QUERY_KEYS = { 17 | GRID_STATISTICS: createQueryKey('gridStatistics'), 18 | }; 19 | 20 | export const useGridStatistics = () => { 21 | // Token de cancelamento para requisição 22 | const cancelTokenRef = useRef(createCancelToken()); 23 | 24 | // Limpeza quando componente desmontar 25 | useEffect(() => { 26 | return () => { 27 | cancelTokenRef.current.cancel('Component unmounted'); 28 | }; 29 | }, []); 30 | 31 | // Função memoizada para transformar os dados 32 | const transformGridData = useCallback( 33 | (data: ApiResponse): GridData[] => { 34 | return data.dados.sort((a, b) => { 35 | const dateA = a.data_inicio ? new Date(a.data_inicio).getTime() : 0; 36 | const dateB = b.data_inicio ? new Date(b.data_inicio).getTime() : 0; 37 | return dateB - dateA; 38 | }); 39 | }, 40 | [], 41 | ); 42 | 43 | // Query com tipagem adequada 44 | const query = useQuery, unknown, GridData[]>({ 45 | queryKey: QUERY_KEYS.GRID_STATISTICS, 46 | queryFn: () => getStatisticsGrid(cancelTokenRef.current), 47 | staleTime: STALE_TIMES.FREQUENT_DATA, 48 | select: transformGridData, 49 | retry: (failureCount, error) => { 50 | // Não tentar novamente requisições canceladas 51 | if (axios.isCancel(error)) return false; 52 | return failureCount < 2; 53 | }, 54 | }); 55 | 56 | return { 57 | data: query.data, 58 | isLoading: query.isLoading, 59 | isError: query.isError, 60 | error: query.error ? standardizeError(query.error) : null, 61 | refetch: query.refetch, 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | /* src/index.css */ 2 | /* Estilos globais para toda a aplicação */ 3 | 4 | * { 5 | margin: 0; 6 | padding: 0; 7 | box-sizing: border-box; 8 | } 9 | 10 | html, 11 | body, 12 | #root { 13 | height: 100%; 14 | width: 100%; 15 | } 16 | 17 | body { 18 | font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | background-color: #f4f6f8; 22 | } 23 | 24 | a { 25 | text-decoration: none; 26 | color: inherit; 27 | } 28 | 29 | ::-webkit-scrollbar { 30 | width: 8px; 31 | height: 8px; 32 | } 33 | 34 | ::-webkit-scrollbar-track { 35 | background: #f1f1f1; 36 | } 37 | 38 | ::-webkit-scrollbar-thumb { 39 | background: #888; 40 | border-radius: 4px; 41 | } 42 | 43 | ::-webkit-scrollbar-thumb:hover { 44 | background: #555; 45 | } 46 | 47 | /* Estilos para o componente de grid (compatibilidade com visavail) */ 48 | div.tooltip-donut { 49 | position: absolute; 50 | text-align: center; 51 | padding: 0.5rem; 52 | background: white; 53 | color: #313639; 54 | border: 1px solid #313639; 55 | border-radius: 8px; 56 | pointer-events: none; 57 | font-size: 12px; 58 | } 59 | -------------------------------------------------------------------------------- /client/src/lib/axios.ts: -------------------------------------------------------------------------------- 1 | // Path: lib\axios.ts 2 | import axios, { AxiosInstance, AxiosError } from 'axios'; 3 | import { ApiError } from '../types/api'; 4 | import { logoutAndRedirect } from '../stores/authStore'; 5 | 6 | // Token storage key 7 | const TOKEN_KEY = '@sap_web-Token'; 8 | 9 | // Create axios instance with default config 10 | const apiClient: AxiosInstance = axios.create({ 11 | baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3013', 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | }, 15 | timeout: 30000, // 30 second timeout 16 | }); 17 | 18 | // Request interceptor to add auth token 19 | apiClient.interceptors.request.use( 20 | config => { 21 | const token = localStorage.getItem(TOKEN_KEY); 22 | if (token && config.headers) { 23 | config.headers.Authorization = `Bearer ${token}`; 24 | } 25 | return config; 26 | }, 27 | error => { 28 | return Promise.reject(error); 29 | }, 30 | ); 31 | 32 | // Response interceptor to handle errors 33 | apiClient.interceptors.response.use( 34 | response => response, 35 | (error: unknown) => { 36 | // Se for um erro de cancelamento, apenas propaga 37 | if (axios.isCancel(error)) { 38 | return Promise.reject(error); 39 | } 40 | 41 | // Cast para AxiosError para trabalhar com a tipagem correta 42 | const axiosError = error as AxiosError; 43 | 44 | // Handle 401 and 403 errors (unauthorized/forbidden) 45 | if ( 46 | axiosError.response?.status === 401 || 47 | axiosError.response?.status === 403 48 | ) { 49 | // Use the centralized logout and redirect function 50 | logoutAndRedirect(); 51 | } 52 | 53 | // Criar um erro padronizado 54 | const apiError: ApiError = { 55 | message: axiosError.message || 'Ocorreu um erro inesperado', 56 | status: axiosError.response?.status, 57 | data: axiosError.response?.data, 58 | }; 59 | 60 | return Promise.reject(apiError); 61 | }, 62 | ); 63 | 64 | export default apiClient; 65 | -------------------------------------------------------------------------------- /client/src/lib/queryClient.ts: -------------------------------------------------------------------------------- 1 | // Path: lib\queryClient.ts 2 | import { QueryClient } from '@tanstack/react-query'; 3 | import { ApiError } from '../types/api'; 4 | 5 | // Define default stale times based on data type 6 | export const STALE_TIMES = { 7 | REFERENCE_DATA: 1000 * 60 * 30, // 30 minutes 8 | USER_DATA: 1000 * 60 * 5, // 5 minutes 9 | FREQUENT_DATA: 1000 * 60 * 1, // 1 minute 10 | }; 11 | 12 | // Create a client 13 | const queryClient = new QueryClient({ 14 | defaultOptions: { 15 | queries: { 16 | refetchOnWindowFocus: false, 17 | retry: 1, 18 | staleTime: STALE_TIMES.USER_DATA, // Default to 5 minutes 19 | gcTime: 1000 * 60 * 10, // 10 minutes 20 | // Consistent error handling 21 | throwOnError: false, 22 | }, 23 | mutations: { 24 | // Don't retry mutations by default 25 | retry: 0, 26 | }, 27 | }, 28 | }); 29 | 30 | // Helper function to standardize error responses 31 | export const standardizeError = (error: unknown): ApiError => { 32 | if (error && typeof error === 'object' && 'message' in error) { 33 | return error as ApiError; 34 | } 35 | 36 | return { 37 | message: 38 | error instanceof Error ? error.message : 'An unexpected error occurred', 39 | status: undefined, 40 | }; 41 | }; 42 | 43 | // Helper function for creating consistent query keys 44 | // Using a proper type union for the entityId parameter 45 | export const createQueryKey = ( 46 | entityType: string, 47 | entityId?: string | number, 48 | subResource?: string, 49 | ): (string | number)[] => { 50 | const key: (string | number)[] = [entityType]; 51 | if (entityId !== undefined) key.push(entityId); 52 | if (subResource !== undefined) key.push(subResource); 53 | return key; 54 | }; 55 | 56 | export default queryClient; 57 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | // Path: main.tsx 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import App from './App'; 5 | import './index.css'; 6 | 7 | ReactDOM.createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | , 11 | ); 12 | -------------------------------------------------------------------------------- /client/src/routes/ErrorBoundaryRoute.tsx: -------------------------------------------------------------------------------- 1 | // Path: routes\ErrorBoundaryRoute.tsx 2 | import { isRouteErrorResponse, useRouteError, Link } from 'react-router-dom'; 3 | import { Box, Typography, Button, Container, Paper } from '@mui/material'; 4 | import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; 5 | import HomeIcon from '@mui/icons-material/Home'; 6 | 7 | export const ErrorBoundaryRoute = () => { 8 | const error = useRouteError(); 9 | 10 | let errorMessage = 'Ocorreu um erro inesperado.'; 11 | let statusCode = 500; 12 | let errorDetails = ''; 13 | 14 | if (isRouteErrorResponse(error)) { 15 | statusCode = error.status; 16 | errorMessage = error.data?.message || error.statusText; 17 | } else if (error instanceof Error) { 18 | errorMessage = error.message; 19 | errorDetails = error.stack || ''; 20 | } else if (typeof error === 'string') { 21 | errorMessage = error; 22 | } 23 | 24 | return ( 25 | 26 | 35 | 36 | 37 | 38 | {statusCode === 404 ? 'Página não encontrada' : 'Erro no aplicativo'} 39 | 40 | 41 | 47 | {statusCode === 404 48 | ? 'A página que você está procurando não existe ou foi movida.' 49 | : errorMessage} 50 | 51 | 52 | {errorDetails && ( 53 | 63 | 64 | Detalhes do erro: 65 | 66 | 71 | {errorDetails} 72 | 73 | 74 | )} 75 | 76 | 77 | 85 | 86 | 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /client/src/routes/NotFound.tsx: -------------------------------------------------------------------------------- 1 | // Path: routes\NotFound.tsx 2 | import { Link as RouterLink } from 'react-router-dom'; 3 | import { Box, Button, Container, Typography } from '@mui/material'; 4 | import { styled } from '@mui/material/styles'; 5 | 6 | const ContentBox = styled(Box)(({ theme }) => ({ 7 | maxWidth: 480, 8 | margin: 'auto', 9 | minHeight: '80vh', 10 | display: 'flex', 11 | justifyContent: 'center', 12 | flexDirection: 'column', 13 | padding: theme.spacing(12, 0), 14 | })); 15 | 16 | export const NotFound = () => { 17 | return ( 18 | 19 | 20 | 21 | 404 22 | 23 | 24 | Página não encontrada 25 | 26 | 27 | A página que você está procurando não existe ou foi movida. 28 | 29 | 30 | 33 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /client/src/routes/Unauthorized.tsx: -------------------------------------------------------------------------------- 1 | // Path: routes\Unauthorized.tsx 2 | import { Link as RouterLink } from 'react-router-dom'; 3 | import { Box, Button, Container, Typography } from '@mui/material'; 4 | import { styled } from '@mui/material/styles'; 5 | 6 | const ContentBox = styled(Box)(({ theme }) => ({ 7 | maxWidth: 480, 8 | margin: 'auto', 9 | minHeight: '80vh', 10 | display: 'flex', 11 | justifyContent: 'center', 12 | flexDirection: 'column', 13 | padding: theme.spacing(12, 0), 14 | })); 15 | 16 | export const Unauthorized = () => { 17 | return ( 18 | 19 | 20 | 21 | 403 22 | 23 | 24 | Acesso não autorizado 25 | 26 | 27 | Você não tem permissão para acessar esta página. 28 | 29 | 30 | 33 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /client/src/services/activityMonitoringService.ts: -------------------------------------------------------------------------------- 1 | // Path: services\activityMonitoringService.ts 2 | import axios from 'axios'; 3 | import apiClient from '../lib/axios'; 4 | import { ApiResponse } from '../types/api'; 5 | import { RunningActivity, CompletedActivity } from '../types/microControl'; 6 | import { handleApiError, createCancelToken } from '@/utils/apiErrorHandler'; 7 | 8 | /** 9 | * Get activities currently in progress 10 | * @param cancelToken Token para possível cancelamento da requisição 11 | */ 12 | export const getRunningActivities = async ( 13 | cancelToken?: ReturnType, 14 | ): Promise> => { 15 | try { 16 | const response = await apiClient.get>( 17 | '/api/acompanhamento/atividades_em_execucao', 18 | cancelToken ? { cancelToken: cancelToken.token } : undefined, 19 | ); 20 | return response.data; 21 | } catch (error) { 22 | // Se a requisição foi cancelada, apenas propaga o erro 23 | if (axios.isCancel(error)) { 24 | throw error; 25 | } 26 | 27 | throw handleApiError( 28 | error, 29 | 'Erro ao buscar atividades em execução', 30 | 'getRunningActivities', 31 | ); 32 | } 33 | }; 34 | 35 | /** 36 | * Get most recently completed activities 37 | * @param cancelToken Token para possível cancelamento da requisição 38 | */ 39 | export const getLastCompletedActivities = async ( 40 | cancelToken?: ReturnType, 41 | ): Promise> => { 42 | try { 43 | const response = await apiClient.get>( 44 | '/api/acompanhamento/ultimas_atividades_finalizadas', 45 | cancelToken ? { cancelToken: cancelToken.token } : undefined, 46 | ); 47 | return response.data; 48 | } catch (error) { 49 | // Se a requisição foi cancelada, apenas propaga o erro 50 | if (axios.isCancel(error)) { 51 | throw error; 52 | } 53 | 54 | throw handleApiError( 55 | error, 56 | 'Erro ao buscar atividades finalizadas recentemente', 57 | 'getLastCompletedActivities', 58 | ); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /client/src/services/authService.ts: -------------------------------------------------------------------------------- 1 | // Path: services\authService.ts 2 | import apiClient from '../lib/axios'; 3 | import { ApiResponse } from '../types/api'; 4 | import { LoginRequest, LoginResponse } from '../types/auth'; 5 | import { handleApiError, createCancelToken } from '@/utils/apiErrorHandler'; 6 | import axios from 'axios'; 7 | 8 | const APLICACAO = 'sap_web'; 9 | const TOKEN_EXPIRY_KEY = '@sap_web-Token-Expiry'; 10 | 11 | /** 12 | * Login user with username and password 13 | * @param credentials Credenciais de login 14 | * @param cancelToken Token para possível cancelamento da requisição 15 | */ 16 | export const login = async ( 17 | credentials: LoginRequest, 18 | cancelToken?: ReturnType, 19 | ): Promise> => { 20 | try { 21 | const response = await apiClient.post>( 22 | '/api/login', 23 | { 24 | usuario: credentials.usuario, 25 | senha: credentials.senha, 26 | aplicacao: APLICACAO, 27 | cliente: 'sap', 28 | }, 29 | cancelToken ? { cancelToken: cancelToken.token } : undefined, 30 | ); 31 | 32 | // If login is successful, store token expiry (assuming token valid for 24 hours) 33 | if (response.data.success && response.data.dados.token) { 34 | const expiryTime = new Date(); 35 | expiryTime.setHours(expiryTime.getHours() + 24); 36 | localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toISOString()); 37 | } 38 | 39 | return response.data; 40 | } catch (error) { 41 | // Se a requisição foi cancelada, apenas propaga o erro 42 | if (axios.isCancel(error)) { 43 | throw error; 44 | } 45 | 46 | throw handleApiError(error, 'Erro durante o login', 'login'); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /client/src/services/dashboardService.ts: -------------------------------------------------------------------------------- 1 | // Path: services\dashboardService.ts 2 | import axios from 'axios'; 3 | import apiClient from '../lib/axios'; 4 | import { ApiResponse } from '../types/api'; 5 | import { 6 | DashboardQuantityItem, 7 | DashboardFinishedItem, 8 | DashboardRunningItem, 9 | PitItem, 10 | } from '../types/dashboard'; 11 | import { handleApiError, createCancelToken } from '@/utils/apiErrorHandler'; 12 | 13 | /** 14 | * Get dashboard data (all APIs in one request) 15 | * @param cancelToken Token para possível cancelamento da requisição 16 | */ 17 | export const getDashboardData = async ( 18 | cancelToken?: ReturnType, 19 | ) => { 20 | try { 21 | const year = new Date().getFullYear(); 22 | const config = cancelToken ? { cancelToken: cancelToken.token } : undefined; 23 | 24 | const [quantityResponse, finishedResponse, runningResponse, pitResponse] = 25 | await Promise.all([ 26 | apiClient.get>( 27 | `/api/acompanhamento/dashboard/quantidade/${year}`, 28 | config, 29 | ), 30 | apiClient.get>( 31 | `/api/acompanhamento/dashboard/finalizadas/${year}`, 32 | config, 33 | ), 34 | apiClient.get>( 35 | `/api/acompanhamento/dashboard/execucao`, 36 | config, 37 | ), 38 | apiClient.get>( 39 | `/api/acompanhamento/pit/${year}`, 40 | config, 41 | ), 42 | ]); 43 | 44 | return { 45 | quantityData: quantityResponse.data.dados || [], 46 | finishedData: finishedResponse.data.dados || [], 47 | runningData: runningResponse.data.dados || [], 48 | pitData: pitResponse.data.dados || [], 49 | }; 50 | } catch (error) { 51 | // Se a requisição foi cancelada, apenas propaga o erro 52 | if (axios.isCancel(error)) { 53 | throw error; 54 | } 55 | 56 | throw handleApiError( 57 | error, 58 | 'Erro ao carregar dados do dashboard', 59 | 'getDashboardData', 60 | ); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /client/src/services/gridService.ts: -------------------------------------------------------------------------------- 1 | // Path: services\gridService.ts 2 | import axios from 'axios'; 3 | import apiClient from '../lib/axios'; 4 | import { ApiResponse } from '../types/api'; 5 | import { GridData } from '../types/grid'; 6 | import { handleApiError, createCancelToken } from '@/utils/apiErrorHandler'; 7 | 8 | /** 9 | * Get statistics grid data 10 | * @param cancelToken Token para possível cancelamento da requisição 11 | */ 12 | export const getStatisticsGrid = async ( 13 | cancelToken?: ReturnType, 14 | ): Promise> => { 15 | try { 16 | const response = await apiClient.get>( 17 | '/api/acompanhamento/grade_acompanhamento/', 18 | cancelToken ? { cancelToken: cancelToken.token } : undefined, 19 | ); 20 | return response.data; 21 | } catch (error) { 22 | // Se a requisição foi cancelada, apenas propaga o erro 23 | if (axios.isCancel(error)) { 24 | throw error; 25 | } 26 | 27 | throw handleApiError( 28 | error, 29 | 'Erro ao carregar dados da grade estatística', 30 | 'getStatisticsGrid', 31 | ); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /client/src/services/lotService.ts: -------------------------------------------------------------------------------- 1 | // Path: services\lotService.ts 2 | import axios from 'axios'; 3 | import apiClient from '../lib/axios'; 4 | import { ApiResponse } from '../types/api'; 5 | import { LotSubphaseData } from '../types/lot'; 6 | import { handleApiError, createCancelToken } from '@/utils/apiErrorHandler'; 7 | 8 | /** 9 | * Get lot statistics 10 | * @param cancelToken Token para possível cancelamento da requisição 11 | */ 12 | export const getLots = async ( 13 | cancelToken?: ReturnType, 14 | ): Promise> => { 15 | try { 16 | const year = new Date().getFullYear(); 17 | const response = await apiClient.get>( 18 | `/api/acompanhamento/pit/subfase/${year}`, 19 | cancelToken ? { cancelToken: cancelToken.token } : undefined, 20 | ); 21 | return response.data; 22 | } catch (error) { 23 | // Se a requisição foi cancelada, apenas propaga o erro 24 | if (axios.isCancel(error)) { 25 | throw error; 26 | } 27 | 28 | throw handleApiError(error, 'Erro ao carregar dados de lotes', 'getLots'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/services/mapService.ts: -------------------------------------------------------------------------------- 1 | // Path: services\mapService.ts 2 | import axios from 'axios'; 3 | import apiClient from '../lib/axios'; 4 | import { ApiResponse } from '../types/api'; 5 | import { handleApiError, createCancelToken } from '@/utils/apiErrorHandler'; 6 | 7 | /** 8 | * Get map views 9 | * @param cancelToken Token para possível cancelamento da requisição 10 | */ 11 | export const getViews = async ( 12 | cancelToken?: ReturnType, 13 | ) => { 14 | try { 15 | const response = await apiClient.get>( 16 | '/api/gerencia/view_acompanhamento?em_andamento_projeto=true', 17 | cancelToken ? { cancelToken: cancelToken.token } : undefined, 18 | ); 19 | return response.data; 20 | } catch (error) { 21 | // Se a requisição foi cancelada, apenas propaga o erro 22 | if (axios.isCancel(error)) { 23 | throw error; 24 | } 25 | 26 | throw handleApiError( 27 | error, 28 | 'Erro ao carregar visualizações do mapa', 29 | 'getViews', 30 | ); 31 | } 32 | }; 33 | 34 | /** 35 | * Get GeoJSON data for a lot 36 | * @param lotName Nome do lote 37 | * @param cancelToken Token para possível cancelamento da requisição 38 | */ 39 | export const getLotGeoJSON = async ( 40 | lotName: string, 41 | cancelToken?: ReturnType, 42 | ) => { 43 | try { 44 | const response = await apiClient.get>( 45 | `/api/acompanhamento/mapa/${lotName}`, 46 | cancelToken ? { cancelToken: cancelToken.token } : undefined, 47 | ); 48 | return response.data; 49 | } catch (error) { 50 | // Se a requisição foi cancelada, apenas propaga o erro 51 | if (axios.isCancel(error)) { 52 | throw error; 53 | } 54 | 55 | throw handleApiError( 56 | error, 57 | `Erro ao carregar GeoJSON para o lote ${lotName}`, 58 | `getLotGeoJSON:${lotName}`, 59 | ); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /client/src/services/pitService.ts: -------------------------------------------------------------------------------- 1 | // Path: services\pitService.ts 2 | import axios from 'axios'; 3 | import apiClient from '../lib/axios'; 4 | import { ApiResponse } from '../types/api'; 5 | import { PitItem } from '../types/pit'; // Using the correct type 6 | import { handleApiError, createCancelToken } from '@/utils/apiErrorHandler'; 7 | 8 | /** 9 | * Get PIT (Plan of Integrated Tasks) data 10 | * @param cancelToken Token para possível cancelamento da requisição 11 | */ 12 | export const getPIT = async ( 13 | cancelToken?: ReturnType, 14 | ): Promise> => { 15 | try { 16 | const year = new Date().getFullYear(); 17 | const response = await apiClient.get>( 18 | `/api/acompanhamento/pit/${year}`, 19 | cancelToken ? { cancelToken: cancelToken.token } : undefined, 20 | ); 21 | return response.data; 22 | } catch (error) { 23 | // Se a requisição foi cancelada, apenas propaga o erro 24 | if (axios.isCancel(error)) { 25 | throw error; 26 | } 27 | 28 | throw handleApiError(error, 'Erro ao carregar dados do PIT', 'getPIT'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/services/subphaseService.ts: -------------------------------------------------------------------------------- 1 | // Path: services\subphaseService.ts 2 | import axios from 'axios'; 3 | import apiClient from '../lib/axios'; 4 | import { ApiResponse } from '../types/api'; 5 | import { SubphaseData } from '../types/subphase'; 6 | import { handleApiError, createCancelToken } from '@/utils/apiErrorHandler'; 7 | 8 | /** 9 | * Get activity subphase data 10 | * @param cancelToken Token para possível cancelamento da requisição 11 | */ 12 | export const getActivitySubphase = async ( 13 | cancelToken?: ReturnType, 14 | ): Promise> => { 15 | try { 16 | const response = await apiClient.get>( 17 | '/api/acompanhamento/atividade_subfase', 18 | cancelToken ? { cancelToken: cancelToken.token } : undefined, 19 | ); 20 | return response.data; 21 | } catch (error) { 22 | // Se a requisição foi cancelada, apenas propaga o erro 23 | if (axios.isCancel(error)) { 24 | throw error; 25 | } 26 | 27 | throw handleApiError( 28 | error, 29 | 'Erro ao buscar dados de atividade por subfase', 30 | 'getActivitySubphase', 31 | ); 32 | } 33 | }; 34 | 35 | /** 36 | * Get subphases situation data 37 | * @param cancelToken Token para possível cancelamento da requisição 38 | */ 39 | export const getSubphasesSituation = async ( 40 | cancelToken?: ReturnType, 41 | ): Promise> => { 42 | try { 43 | const response = await apiClient.get>( 44 | '/api/acompanhamento/situacao_subfase', 45 | cancelToken ? { cancelToken: cancelToken.token } : undefined, 46 | ); 47 | return response.data; 48 | } catch (error) { 49 | // Se a requisição foi cancelada, apenas propaga o erro 50 | if (axios.isCancel(error)) { 51 | throw error; 52 | } 53 | 54 | throw handleApiError( 55 | error, 56 | 'Erro ao buscar dados de situação de subfase', 57 | 'getSubphasesSituation', 58 | ); 59 | } 60 | }; 61 | 62 | /** 63 | * Get user activities data 64 | * @param cancelToken Token para possível cancelamento da requisição 65 | */ 66 | export const getUserActivities = async ( 67 | cancelToken?: ReturnType, 68 | ): Promise> => { 69 | try { 70 | const response = await apiClient.get>( 71 | '/api/acompanhamento/atividade_usuario', 72 | cancelToken ? { cancelToken: cancelToken.token } : undefined, 73 | ); 74 | return response.data; 75 | } catch (error) { 76 | // Se a requisição foi cancelada, apenas propaga o erro 77 | if (axios.isCancel(error)) { 78 | throw error; 79 | } 80 | 81 | throw handleApiError( 82 | error, 83 | 'Erro ao buscar atividades de usuário', 84 | 'getUserActivities', 85 | ); 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /client/src/stores/mapStore.ts: -------------------------------------------------------------------------------- 1 | // Path: stores\mapStore.ts 2 | import { create } from 'zustand'; 3 | import { MapLayer } from '@/types/map'; 4 | 5 | // Define state type 6 | interface MapState { 7 | layers: MapLayer[]; 8 | visibleLayers: Record; 9 | } 10 | 11 | // Define actions type 12 | interface MapActions { 13 | setLayers: (layers: MapLayer[]) => void; 14 | toggleLayerVisibility: (layerId: string) => void; 15 | setInitialVisibility: () => void; 16 | setLayerVisibility: (layerId: string, isVisible: boolean) => void; 17 | } 18 | 19 | // Create store with separated state and actions 20 | const useMapStoreBase = create( 21 | (set, get) => ({ 22 | // State 23 | layers: [], 24 | visibleLayers: {}, 25 | 26 | // Actions 27 | actions: { 28 | // Optimize setLayers to avoid unnecessary state updates 29 | setLayers: layers => { 30 | // Compare new layers with existing ones to prevent unnecessary updates 31 | const currentLayers = get().layers; 32 | 33 | // Only update if layer ids have changed 34 | const shouldUpdate = 35 | layers.length !== currentLayers.length || 36 | layers.some((layer, index) => currentLayers[index]?.id !== layer.id); 37 | 38 | if (shouldUpdate) { 39 | set({ layers }); 40 | 41 | const { visibleLayers } = get(); 42 | const hasAnyVisibleLayers = Object.keys(visibleLayers).length > 0; 43 | 44 | if (!hasAnyVisibleLayers) { 45 | get().actions.setInitialVisibility(); 46 | } 47 | } 48 | }, 49 | 50 | // Optimize toggleLayerVisibility to avoid object spreading which creates a new object 51 | toggleLayerVisibility: layerId => { 52 | set(state => { 53 | // Create a shallow copy only when needed 54 | const newVisibleLayers = { ...state.visibleLayers }; 55 | newVisibleLayers[layerId] = !newVisibleLayers[layerId]; 56 | 57 | return { visibleLayers: newVisibleLayers }; 58 | }); 59 | }, 60 | 61 | setLayerVisibility: (layerId, isVisible) => { 62 | set(state => { 63 | // Only update if the visibility actually changes 64 | if (state.visibleLayers[layerId] === isVisible) { 65 | return state; // Return the current state unchanged 66 | } 67 | 68 | // Create a shallow copy and update 69 | const newVisibleLayers = { ...state.visibleLayers }; 70 | newVisibleLayers[layerId] = isVisible; 71 | 72 | return { visibleLayers: newVisibleLayers }; 73 | }); 74 | }, 75 | 76 | setInitialVisibility: () => { 77 | const { layers } = get(); 78 | 79 | set(state => { 80 | if ( 81 | Object.keys(state.visibleLayers).length === layers.length && 82 | layers.every(layer => state.visibleLayers[layer.id] === true) 83 | ) { 84 | return state; // No changes needed 85 | } 86 | 87 | const initialVisibility: Record = {}; 88 | 89 | layers.forEach(layer => { 90 | initialVisibility[layer.id] = true; 91 | }); 92 | 93 | return { visibleLayers: initialVisibility }; 94 | }); 95 | }, 96 | }, 97 | }), 98 | ); 99 | 100 | // Custom hooks for selectors (atomic selectors) 101 | export const useLayers = () => useMapStoreBase(state => state.layers); 102 | export const useVisibleLayers = () => 103 | useMapStoreBase(state => state.visibleLayers); 104 | export const useMapActions = () => useMapStoreBase(state => state.actions); 105 | 106 | // If you need to check if a specific layer is visible 107 | export const useLayerVisibility = (layerId: string) => 108 | useMapStoreBase(state => state.visibleLayers[layerId]); 109 | -------------------------------------------------------------------------------- /client/src/types/activity.ts: -------------------------------------------------------------------------------- 1 | // Path: types\activity.ts 2 | /** 3 | * Activity entity 4 | * This interface represents the core domain model for activities 5 | * Currently used for documentation and future reference 6 | */ 7 | interface Activity { 8 | id: string; 9 | nome: string; 10 | projeto?: string; 11 | lote?: string; 12 | fase?: string; 13 | subfase?: string; 14 | etapa?: string; 15 | bloco?: string; 16 | data_inicio?: string; 17 | data_fim?: string; 18 | usuario?: string; 19 | dado_producao: { 20 | tipo_dado_producao_id: number; 21 | }; 22 | } 23 | 24 | /** 25 | * Current activity response 26 | */ 27 | export interface CurrentActivityResponse { 28 | atividade: Activity | null; 29 | } 30 | 31 | /** 32 | * Error report for an activity 33 | */ 34 | export interface ErrorReport { 35 | atividade_id: string; 36 | tipo_problema_id: number; 37 | descricao: string; 38 | } 39 | 40 | /** 41 | * Error type definition 42 | */ 43 | export interface ErrorType { 44 | tipo_problema_id: number; 45 | tipo_problema: string; 46 | } 47 | -------------------------------------------------------------------------------- /client/src/types/api.ts: -------------------------------------------------------------------------------- 1 | // Path: types\api.ts 2 | /** 3 | * Generic API response structure 4 | */ 5 | export interface ApiResponse { 6 | success: boolean; 7 | message: string; 8 | dados: T; 9 | } 10 | 11 | /** 12 | * Error response from API 13 | */ 14 | export interface ApiError { 15 | message: string; 16 | status?: number; 17 | data?: any; 18 | isCancelled?: boolean; 19 | } 20 | -------------------------------------------------------------------------------- /client/src/types/auth.ts: -------------------------------------------------------------------------------- 1 | // Path: types\auth.ts 2 | /** 3 | * User role enum 4 | */ 5 | export enum UserRole { 6 | ADMIN = 'ADMIN', 7 | USER = 'USER', 8 | } 9 | 10 | /** 11 | * User authentication data 12 | */ 13 | export interface User { 14 | uuid: string; 15 | role: UserRole; 16 | token: string; 17 | username?: string; 18 | } 19 | 20 | /** 21 | * Login request payload 22 | */ 23 | export interface LoginRequest { 24 | usuario: string; 25 | senha: string; 26 | } 27 | 28 | /** 29 | * Login response from the API 30 | */ 31 | export interface LoginResponse { 32 | token: string; 33 | administrador: boolean; 34 | uuid: string; 35 | username?: string; 36 | } 37 | -------------------------------------------------------------------------------- /client/src/types/dashboard.ts: -------------------------------------------------------------------------------- 1 | // Path: types\dashboard.ts 2 | export interface DashboardQuantityItem { 3 | lote: string; 4 | quantidade: number; 5 | } 6 | 7 | export interface DashboardFinishedItem { 8 | lote: string; 9 | finalizadas: number; 10 | } 11 | 12 | export interface DashboardRunningItem { 13 | lote: string; 14 | count: number; 15 | } 16 | 17 | export interface PitItem { 18 | projeto: string; 19 | lote: string; 20 | month: number; 21 | finalizadas: number; 22 | meta?: number; 23 | } 24 | 25 | export interface DashboardSummary { 26 | totalProducts: number; 27 | completedProducts: number; 28 | runningProducts: number; 29 | progressPercentage: number; 30 | } 31 | 32 | export interface DashboardData { 33 | summary: DashboardSummary; 34 | lotProgressData: { 35 | name: string; 36 | completed: number; 37 | running: number; 38 | notStarted: number; 39 | }[]; 40 | monthlyData: { 41 | month: string; 42 | [key: string]: any; 43 | }[]; 44 | } 45 | -------------------------------------------------------------------------------- /client/src/types/fieldActivities.ts: -------------------------------------------------------------------------------- 1 | // Path: types\fieldActivities.ts 2 | /** 3 | * Campo (Field) entity 4 | */ 5 | export interface Campo { 6 | id: string; 7 | nome: string; 8 | descricao?: string; 9 | situacao_id: number; 10 | categoria_id?: number; 11 | data_criacao?: string; 12 | orgao?: string; 13 | pit?: string; 14 | qtd_fotos?: number; 15 | qtd_track?: number; 16 | situacao?: string; 17 | geometry?: GeoJSON.Geometry; 18 | // Campos específicos para dados do GeoJSON feature properties 19 | inicio?: string; 20 | fim?: string; 21 | } 22 | 23 | /** 24 | * Foto (Photo) entity 25 | */ 26 | export interface Foto { 27 | id: string; 28 | campo_id: string; 29 | descricao?: string; 30 | data_imagem?: string; 31 | nome?: string; 32 | url?: string; 33 | imagem_bin?: any; // Binary data for image 34 | } 35 | 36 | /** 37 | * Track entity 38 | */ 39 | export interface Track { 40 | id: string; 41 | campo_id: string; 42 | nome?: string; 43 | descricao?: string; 44 | track_id_garmin?: string; 45 | min_t?: string; 46 | max_t?: string; 47 | data?: any; // Track data 48 | chefe_vtr?: string; // Chefe da viatura 49 | motorista?: string; // Motorista 50 | placa_vtr?: string; // Placa da viatura 51 | dia?: string; 52 | } 53 | 54 | /** 55 | * Situação (Status) entity 56 | */ 57 | export interface Situacao { 58 | id: number; 59 | nome: string; 60 | cor?: string; // Color for visualization 61 | } 62 | 63 | /** 64 | * Categoria (Category) entity 65 | */ 66 | export interface Categoria { 67 | id: number; 68 | nome: string; 69 | } 70 | 71 | /** 72 | * GeoJSON response for map 73 | */ 74 | export interface CamposGeoJSONResponse 75 | extends GeoJSON.FeatureCollection { 76 | // Garantir que seja compatível com GeoJSON.FeatureCollection exato 77 | type: 'FeatureCollection'; 78 | } 79 | 80 | /** 81 | * API responses 82 | */ 83 | export interface CamposResponse { 84 | dados: Campo[]; 85 | success: boolean; 86 | message: string; 87 | } 88 | 89 | export interface FotosResponse { 90 | dados: Foto[]; 91 | success: boolean; 92 | message: string; 93 | } 94 | 95 | export interface TracksResponse { 96 | dados: Track[]; 97 | success: boolean; 98 | message: string; 99 | } 100 | 101 | export interface SituacoesResponse { 102 | dados: Situacao[]; 103 | success: boolean; 104 | message: string; 105 | } 106 | 107 | export interface CategoriasResponse { 108 | dados: Categoria[]; 109 | success: boolean; 110 | message: string; 111 | } 112 | 113 | export interface CamposGeoJSONApiResponse { 114 | dados: CamposGeoJSONResponse; 115 | success: boolean; 116 | message: string; 117 | } 118 | -------------------------------------------------------------------------------- /client/src/types/grid.ts: -------------------------------------------------------------------------------- 1 | // Path: types\grid.ts 2 | /** 3 | * Grid item representing a cell in the grid visualization 4 | */ 5 | export interface GridItem { 6 | i: number; 7 | j: number; 8 | visited: boolean; 9 | data_atualizacao?: string; 10 | } 11 | 12 | /** 13 | * Grid data representing the entire grid structure with metadata 14 | */ 15 | export interface GridData { 16 | grade: GridItem[]; 17 | data_inicio?: string; 18 | usuario?: string; 19 | projeto?: string; 20 | lote?: string; 21 | fase?: string; 22 | bloco?: string; 23 | subfase?: string; 24 | etapa?: string; 25 | selectedItem?: GridItem; // For storing the currently selected item 26 | } 27 | -------------------------------------------------------------------------------- /client/src/types/lot.ts: -------------------------------------------------------------------------------- 1 | // Path: types\lot.ts 2 | export interface LotSubphaseData { 3 | lote: string; 4 | subfase: string; 5 | month: number; 6 | count: number; 7 | } 8 | -------------------------------------------------------------------------------- /client/src/types/map.ts: -------------------------------------------------------------------------------- 1 | // Path: types\map.ts 2 | 3 | export interface MapLayer { 4 | id: string; 5 | name: string; 6 | geojson: GeoJSON.FeatureCollection; 7 | visible?: boolean; 8 | } 9 | 10 | export interface LegendItem { 11 | label: string; 12 | color: string; 13 | border: boolean; 14 | } 15 | -------------------------------------------------------------------------------- /client/src/types/microControl.ts: -------------------------------------------------------------------------------- 1 | // Path: types\microControl.ts 2 | export interface Duration { 3 | days?: number; 4 | hours?: number; 5 | minutes?: number; 6 | seconds?: number; 7 | } 8 | 9 | export interface RunningActivity { 10 | projeto_nome: string; 11 | lote: string; 12 | fase_nome: string; 13 | subfase_nome: string; 14 | etapa_nome: string; 15 | bloco: string; 16 | atividade_id: string; 17 | usuario: string; 18 | data_inicio: string; 19 | duracao: Duration; 20 | } 21 | 22 | export interface CompletedActivity { 23 | projeto_nome: string; 24 | lote: string; 25 | fase_nome: string; 26 | subfase_nome: string; 27 | etapa_nome: string; 28 | bloco: string; 29 | atividade_id: string; 30 | usuario: string; 31 | data_inicio: string; 32 | data_fim: string; 33 | } 34 | 35 | export interface FormattedRunningActivity 36 | extends Omit { 37 | duration: string; 38 | data_inicio: string; 39 | } 40 | 41 | export interface FormattedCompletedActivity 42 | extends Omit { 43 | data_inicio: string; 44 | data_fim: string; 45 | } 46 | -------------------------------------------------------------------------------- /client/src/types/pit.ts: -------------------------------------------------------------------------------- 1 | // Path: types\pit.ts 2 | export interface PitItem { 3 | projeto: string; 4 | lote: string; 5 | month: number; 6 | finalizadas: number; 7 | meta?: number; 8 | } 9 | -------------------------------------------------------------------------------- /client/src/types/subphase.ts: -------------------------------------------------------------------------------- 1 | // Path: types\subphase.ts 2 | /** 3 | * Subphase data for timeline visualization 4 | */ 5 | export interface SubphaseData { 6 | lote: string; 7 | subfase: string; 8 | data: Array<[string, string, string]>; // [start_date, status, end_date] 9 | } 10 | 11 | /** 12 | * Timeline group data for visavail visualization 13 | */ 14 | export interface TimelineGroup { 15 | idContainer: string; 16 | idBar: string; 17 | title: string; 18 | options: { 19 | title: { 20 | text: string; 21 | }; 22 | id_div_container: string; 23 | id_div_graph: string; 24 | date_in_utc: boolean; 25 | line_spacing: number; 26 | tooltip: { 27 | height: number; 28 | position: string; 29 | left_spacing: number; 30 | only_first_date: boolean; 31 | date_plus_time: boolean; 32 | }; 33 | responsive: { 34 | enabled: boolean; 35 | }; 36 | }; 37 | dataset: { 38 | measure: string; 39 | data: Array<[string, string, string]>; 40 | }[]; 41 | } 42 | 43 | /** 44 | * Chart point for bar/pie chart visualizations 45 | * Used internally within ChartGroup 46 | */ 47 | interface ChartPoint { 48 | label: string; 49 | y: number; 50 | } 51 | 52 | /** 53 | * Chart group for stacked bar charts 54 | */ 55 | export interface ChartGroup { 56 | title: string; 57 | dataPointA: ChartPoint[]; 58 | dataPointB: ChartPoint[]; 59 | } 60 | -------------------------------------------------------------------------------- /client/src/utils/apiErrorHandler.ts: -------------------------------------------------------------------------------- 1 | // Path: utils\apiErrorHandler.ts 2 | import axios, { AxiosError, CancelTokenSource } from 'axios'; 3 | import { ApiError } from '@/types/api'; 4 | import { logoutAndRedirect } from '@/stores/authStore'; 5 | 6 | /** 7 | * Centraliza o tratamento de erros de API 8 | * @param error Erro capturado do try/catch 9 | * @param defaultMessage Mensagem padrão caso o erro não tenha uma mensagem 10 | * @param logLabel Identificador para o log de erro (opcional) 11 | * @returns Um objeto ApiError padronizado 12 | */ 13 | export const handleApiError = ( 14 | error: unknown, 15 | defaultMessage: string, 16 | logLabel?: string, 17 | ): ApiError => { 18 | // Se for um erro do Axios 19 | if (axios.isAxiosError(error)) { 20 | const axiosError = error as AxiosError; 21 | 22 | // Verificar se é um erro de cancelamento - não é tratado como erro real 23 | if (axios.isCancel(error)) { 24 | return { 25 | message: 'Requisição cancelada', 26 | status: 499, // Código não-padrão para "Client Closed Request" 27 | isCancelled: true, 28 | }; 29 | } 30 | 31 | // Verificar autenticação 32 | if ( 33 | axiosError.response?.status === 401 || 34 | axiosError.response?.status === 403 35 | ) { 36 | // Use a função centralizada para logout e redirecionamento 37 | logoutAndRedirect(); 38 | } 39 | 40 | // Tentar extrair mensagem de erro da resposta 41 | let errorMessage = defaultMessage; 42 | if ( 43 | axiosError.response?.data && 44 | typeof axiosError.response.data === 'object' 45 | ) { 46 | const data = axiosError.response.data; 47 | if ('message' in data && typeof data.message === 'string') { 48 | errorMessage = data.message; 49 | } 50 | } else if (axiosError.message && axiosError.message !== 'Network Error') { 51 | errorMessage = axiosError.message; 52 | } 53 | 54 | const apiError: ApiError = { 55 | message: errorMessage, 56 | status: axiosError.response?.status, 57 | data: axiosError.response?.data, 58 | }; 59 | 60 | // Log do erro 61 | if (logLabel) { 62 | console.error(`API Error [${logLabel}]:`, apiError); 63 | } else { 64 | console.error('API Error:', apiError); 65 | } 66 | 67 | return apiError; 68 | } 69 | 70 | // Se for um ApiError, apenas retorná-lo 71 | if (error && typeof error === 'object' && 'message' in error) { 72 | return error as ApiError; 73 | } 74 | 75 | // Para outros tipos de erro 76 | const errorMessage = error instanceof Error ? error.message : defaultMessage; 77 | const apiError: ApiError = { 78 | message: errorMessage, 79 | status: undefined, 80 | }; 81 | 82 | if (logLabel) { 83 | console.error(`Error [${logLabel}]:`, error); 84 | } else { 85 | console.error('Error:', error); 86 | } 87 | 88 | return apiError; 89 | }; 90 | 91 | /** 92 | * Cria um token de cancelamento para requisições Axios 93 | * Usado para cancelar requisições quando componentes são desmontados 94 | */ 95 | export const createCancelToken = (): CancelTokenSource => { 96 | return axios.CancelToken.source(); 97 | }; 98 | 99 | /** 100 | * Verifica se um erro é um erro de cancelamento 101 | */ 102 | export const isRequestCancelled = (error: unknown): boolean => { 103 | return axios.isCancel(error); 104 | }; 105 | -------------------------------------------------------------------------------- /client/src/utils/dateFormatters.ts: -------------------------------------------------------------------------------- 1 | // Path: utils\dateFormatters.ts 2 | /** 3 | * Formata um timestamp com manipulação de fuso horário 4 | * @param dateString String de data para formatar 5 | * @returns Data formatada como string ou string vazia se inválida 6 | */ 7 | export const formatTimestampWithTimezone = (dateString?: string): string => { 8 | if (!dateString) return ''; 9 | 10 | try { 11 | // Parse the date, handling common UTC formats that might lack a timezone indicator 12 | let date; 13 | 14 | // If the dateString already has a timezone indicator, use it as is 15 | if ( 16 | dateString.includes('Z') || 17 | dateString.includes('+') || 18 | dateString.match(/\d-\d{2}:\d{2}$/) 19 | ) { 20 | date = new Date(dateString); 21 | } else { 22 | // If it doesn't have a timezone indicator, assume it's UTC 23 | if (dateString.includes('T')) { 24 | // ISO format without timezone 25 | date = new Date(dateString + 'Z'); 26 | } else if (dateString.includes(' ') && dateString.includes(':')) { 27 | // "YYYY-MM-DD HH:MM:SS" format 28 | date = new Date(dateString.replace(' ', 'T') + 'Z'); 29 | } else { 30 | // Fallback 31 | date = new Date(dateString); 32 | } 33 | } 34 | 35 | // Format using locale string to convert to user's timezone 36 | return date.toLocaleString('pt-BR', { 37 | year: 'numeric', 38 | month: '2-digit', 39 | day: '2-digit', 40 | hour: '2-digit', 41 | minute: '2-digit', 42 | }); 43 | } catch (error) { 44 | console.error('Error formatting date:', error); 45 | return dateString; 46 | } 47 | }; 48 | 49 | /** 50 | * Formata uma data para exibição no formato brasileiro (DD/MM/YYYY) 51 | * @param dateString String de data ou objeto Date 52 | * @returns String formatada ou string vazia se inválida 53 | */ 54 | export const formatDate = (dateString?: string | Date | null): string => { 55 | if (!dateString) return ''; 56 | 57 | try { 58 | const date = 59 | typeof dateString === 'string' ? new Date(dateString) : dateString; 60 | return new Intl.DateTimeFormat('pt-BR').format(date); 61 | } catch (error) { 62 | console.error('Error formatting date:', error); 63 | return ''; 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /client/src/utils/formatters.ts: -------------------------------------------------------------------------------- 1 | // Path: utils\formatters.ts 2 | 3 | /** 4 | * Formata uma data para exibição no formato brasileiro (DD/MM/YYYY) 5 | * @param dateString String de data ou objeto Date 6 | * @returns String formatada ou string vazia se inválida 7 | */ 8 | export const formatDate = (dateString?: string | Date | null): string => { 9 | if (!dateString) return ''; 10 | 11 | try { 12 | const date = 13 | typeof dateString === 'string' ? new Date(dateString) : dateString; 14 | return new Intl.DateTimeFormat('pt-BR').format(date); 15 | } catch (error) { 16 | console.error('Error formatting date:', error); 17 | return ''; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | // Path: vite-env.d.ts 2 | 3 | /** 4 | * Declarações de variáveis de ambiente para o Vite 5 | */ 6 | interface ImportMetaEnv { 7 | readonly VITE_API_URL: string; 8 | } 9 | 10 | interface ImportMeta { 11 | readonly env: ImportMetaEnv; 12 | } 13 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["./src/*"], 6 | "@components/*": ["./src/components/*"], 7 | "@features/*": ["./src/features/*"], 8 | "@hooks/*": ["./src/hooks/*"], 9 | "@lib/*": ["./src/lib/*"], 10 | "@services/*": ["./src/services/*"], 11 | "@stores/*": ["./src/stores/*"], 12 | "@types/*": ["./src/types/*"], 13 | "@utils/*": ["./src/utils/*"], 14 | "@routes/*": ["./src/routes/*"], 15 | "@assets/*": ["./src/assets/*"] 16 | }, 17 | "target": "ES2020", 18 | "useDefineForClassFields": true, 19 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 20 | "module": "ESNext", 21 | "skipLibCheck": true, 22 | "moduleResolution": "bundler", 23 | "allowImportingTsExtensions": true, 24 | "resolveJsonModule": true, 25 | "isolatedModules": true, 26 | "noEmit": true, 27 | "jsx": "react-jsx", 28 | "strict": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noFallthroughCasesInSwitch": true 32 | }, 33 | "include": ["src"], 34 | "references": [{ "path": "./tsconfig.node.json" }] 35 | } -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { defineConfig, loadEnv } from 'vite' 3 | import react from '@vitejs/plugin-react' 4 | import viteCompression from 'vite-plugin-compression' 5 | import { fileURLToPath } from 'url' 6 | import path from 'path' 7 | 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 9 | 10 | export default defineConfig(({ mode }) => { 11 | const env = loadEnv(mode, process.cwd(), '') 12 | 13 | return { 14 | plugins: [ 15 | react({ 16 | jsxImportSource: '@emotion/react', 17 | babel: { 18 | plugins: ['@emotion/babel-plugin'] 19 | } 20 | }), 21 | viteCompression() 22 | ], 23 | server: { 24 | port: 3000, 25 | proxy: { 26 | '/api': { 27 | target: env.VITE_API_URL || 'http://localhost:3013', 28 | changeOrigin: true, 29 | secure: false 30 | } 31 | } 32 | }, 33 | build: { 34 | outDir: 'build', 35 | rollupOptions: { 36 | output: { 37 | manualChunks: { 38 | 'vendor-core': ['react', 'react-dom', 'react-router-dom'], 39 | 'vendor-mui': [ 40 | '@mui/material', 41 | '@mui/icons-material', 42 | '@emotion/react', 43 | '@emotion/styled' 44 | ], 45 | 'vendor-data': [ 46 | '@tanstack/react-query', 47 | 'axios', 48 | 'zustand', 49 | 'd3', 50 | 'recharts' 51 | ], 52 | 'vendor-form': [ 53 | 'react-hook-form', 54 | '@hookform/resolvers', 55 | 'zod' 56 | ] 57 | } 58 | } 59 | }, 60 | sourcemap: mode === 'development', 61 | chunkSizeWarningLimit: 1000, 62 | target: 'esnext', 63 | minify: 'esbuild' 64 | }, 65 | resolve: { 66 | alias: { 67 | '@': path.resolve(__dirname, './src'), 68 | '@components': path.resolve(__dirname, './src/components'), 69 | '@features': path.resolve(__dirname, './src/features'), 70 | '@hooks': path.resolve(__dirname, './src/hooks'), 71 | '@lib': path.resolve(__dirname, './src/lib'), 72 | '@services': path.resolve(__dirname, './src/services'), 73 | '@stores': path.resolve(__dirname, './src/stores'), 74 | '@types': path.resolve(__dirname, './src/types'), 75 | '@utils': path.resolve(__dirname, './src/utils'), 76 | '@routes': path.resolve(__dirname, './src/routes'), 77 | '@assets': path.resolve(__dirname, './src/assets') 78 | } 79 | } 80 | } 81 | }) -------------------------------------------------------------------------------- /create_build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const npmRun = require('npm-run') 3 | const path = require('path') 4 | const colors = require('colors') 5 | 6 | colors.enable() 7 | 8 | const createBuild = async () => { 9 | console.log('Criando build do frontend'.blue) 10 | console.log('Esta operação pode demorar alguns minutos') 11 | 12 | npmRun.exec('npm run build', { cwd: path.join(__dirname, 'client') }, async (err, stdout, stderr) => { 13 | if (err) { 14 | console.log('Erro ao criar build!'.red) 15 | process.exit(0) 16 | } 17 | console.log('Build criada com sucesso!') 18 | console.log('Copiando arquivos') 19 | try { 20 | await fs.copy(path.join(__dirname, 'client', 'build'), path.join(__dirname, 'server', 'src', 'build')) 21 | console.log('Arquivos copiados com sucesso!'.blue) 22 | } catch (error) { 23 | console.log(error.message.red) 24 | console.log('-------------------------------------------------') 25 | console.log(error) 26 | } 27 | }) 28 | } 29 | 30 | createBuild() 31 | -------------------------------------------------------------------------------- /er/campo.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE SCHEMA controle_campo; 4 | 5 | CREATE TABLE controle_campo.situacao 6 | ( 7 | code SMALLINT NOT NULL PRIMARY KEY, 8 | nome VARCHAR(255) NOT NULL UNIQUE 9 | ); 10 | 11 | INSERT INTO controle_campo.situacao (code, nome) VALUES 12 | (1, 'Previsto'), 13 | (2, 'Em Execução'), 14 | (3, 'Finalizado'), 15 | (4, 'Cancelado'); 16 | 17 | CREATE TYPE controle_campo.categoria_campo AS ENUM ( 18 | 'Reambulação', 19 | 'Modelos 3D', 20 | 'Imagens Panorâmicas em 360º', 21 | 'Pontos de Controle', 22 | 'Capacitação em Geoinformação', 23 | 'Ortoimagens de Drone' 24 | ); 25 | 26 | CREATE TABLE controle_campo.campo 27 | ( 28 | id uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), 29 | nome VARCHAR(255) NOT NULL UNIQUE, 30 | descricao text, 31 | pit SMALLINT NOT NULL, 32 | orgao VARCHAR(255) NOT NULL, 33 | militares text, 34 | placas_vtr text, 35 | inicio timestamp with time zone, 36 | fim timestamp with time zone, 37 | categorias controle_campo.categoria_campo[] NOT NULL DEFAULT '{}', 38 | situacao_id SMALLINT NOT NULL REFERENCES controle_campo.situacao (code), 39 | geom geometry(MULTIPOLYGON, 4326) 40 | ); 41 | 42 | CREATE TABLE controle_campo.relacionamento_campo_produto 43 | ( 44 | id uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), 45 | campo_id uuid NOT NULL REFERENCES controle_campo.campo (id), 46 | produto_id SERIAL NOT NULL REFERENCES macrocontrole.produto (id), 47 | UNIQUE (campo_id, produto_id) 48 | ); 49 | 50 | CREATE TABLE controle_campo.imagem 51 | ( 52 | id uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), 53 | descricao text, 54 | data_imagem timestamp with time zone, 55 | imagem_bin bytea, 56 | campo_id uuid NOT NULL REFERENCES controle_campo.campo (id) 57 | ); 58 | 59 | CREATE TABLE controle_campo.track 60 | ( 61 | id uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), 62 | chefe_vtr VARCHAR(255) NOT NULL, 63 | motorista VARCHAR(255) NOT NULL, 64 | placa_vtr VARCHAR(255) NOT NULL, 65 | dia date NOT NULL, 66 | campo_id uuid NOT NULL REFERENCES controle_campo.campo (id) 67 | ); 68 | 69 | CREATE TABLE controle_campo.track_p 70 | ( 71 | id uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), 72 | track_id uuid NOT NULL REFERENCES controle_campo.track (id), 73 | x_ll real, 74 | y_ll real, 75 | track_id_garmin text, 76 | track_segment integer, 77 | track_segment_point_index integer, 78 | elevation real, 79 | creation_time timestamp with time zone, 80 | geom geometry(Point,4326), 81 | data_importacao timestamp(6) without time zone 82 | ); 83 | 84 | CREATE INDEX track_p_geom_idx ON controle_campo.track_p USING gist (geom); 85 | 86 | CREATE MATERIALIZED VIEW controle_campo.track_l 87 | AS 88 | SELECT row_number() OVER () AS id, 89 | p.track_id, 90 | p.track_id_garmin, 91 | min(p.creation_time) AS min_t, 92 | max(p.creation_time) AS max_t, 93 | st_makeline(st_setsrid(st_makepointm(st_x(p.geom), st_y(p.geom), date_part('epoch'::text, p.creation_time)), 4326) ORDER BY p.creation_time)::geometry(LineStringM, 4326) AS geom 94 | FROM controle_campo.track_p AS p 95 | GROUP BY p.track_id_garmin, p.track_id 96 | WITH DATA; 97 | 98 | CREATE INDEX track_l_geom_idx ON controle_campo.track_l USING gist (geom); 99 | 100 | COMMIT; -------------------------------------------------------------------------------- /er/permissao.sql: -------------------------------------------------------------------------------- 1 | GRANT USAGE ON schema public TO $1:name; 2 | GRANT SELECT ON ALL TABLES IN SCHEMA public TO $1:name; 3 | 4 | GRANT USAGE ON schema dominio TO $1:name; 5 | GRANT SELECT ON ALL TABLES IN SCHEMA dominio TO $1:name; 6 | 7 | GRANT USAGE ON SCHEMA dgeo TO $1:name; 8 | GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA dgeo TO $1:name; 9 | GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA dgeo TO $1:name; 10 | 11 | GRANT USAGE ON SCHEMA macrocontrole TO $1:name; 12 | GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA macrocontrole TO $1:name; 13 | GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA macrocontrole TO $1:name; 14 | GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA macrocontrole TO $1:name; 15 | 16 | GRANT USAGE ON schema acompanhamento TO $1:name; 17 | GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA acompanhamento TO $1:name; 18 | GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA acompanhamento TO $1:name; 19 | GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA acompanhamento TO $1:name; -------------------------------------------------------------------------------- /er/recurso_humano.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE SCHEMA recurso_humano; 4 | 5 | CREATE TABLE recurso_humano.perda_recurso_humano( 6 | id SERIAL NOT NULL PRIMARY KEY, 7 | usuario_id INTEGER NOT NULL REFERENCES dgeo.usuario (id), 8 | tipo_perda_recurso_humano_id SMALLINT NOT NULL REFERENCES dominio.tipo_perda_recurso_humano (code), 9 | horas REAL, 10 | data_inicio timestamp with time zone NOT NULL, 11 | data_fim timestamp with time zone NOT NULL, 12 | aprovado boolean not null default true, 13 | observacao TEXT 14 | ); 15 | 16 | CREATE TABLE recurso_humano.ganho_recurso_humano(--horas além do expediente 17 | id SERIAL NOT NULL PRIMARY KEY, 18 | usuario_id INTEGER NOT NULL REFERENCES dgeo.usuario (id), 19 | horas REAL, 20 | data_inicio timestamp with time zone NOT NULL, 21 | data_fim timestamp with time zone NOT NULL, 22 | aprovado boolean not null default true, 23 | observacao TEXT 24 | ); 25 | 26 | CREATE TABLE recurso_humano.funcao_especial( 27 | id SERIAL NOT NULL PRIMARY KEY, 28 | usuario_id INTEGER NOT NULL REFERENCES dgeo.usuario (id), 29 | funcao VARCHAR(255) NOT NULL, 30 | data_inicio timestamp with time zone NOT NULL, 31 | data_fim timestamp with time zone 32 | ); 33 | 34 | CREATE TABLE recurso_humano.expediente( 35 | id SERIAL NOT NULL PRIMARY KEY, 36 | nome VARCHAR(255) NOT NULL, 37 | segunda_feira REAL NOT NULL, 38 | terca_feira REAL NOT NULL, 39 | quarta_feira REAL NOT NULL, 40 | quinta_feira REAL NOT NULL, 41 | sexta_feira REAL NOT NULL, 42 | sabado REAL NOT NULL, 43 | domingo REAL NOT NULL, 44 | UNIQUE(nome) 45 | ); 46 | 47 | INSERT INTO recurso_humano.expediente (nome, segunda_feira, terca_feira, quarta_feira, quinta_feira, sexta_feira, sabado, domingo) VALUES 48 | ('Expediente integral', 6.5, 6.5, 6.5, 6.5, 4, 0, 0), 49 | ('Turno', 6, 6, 6, 6, 6, 0, 0), 50 | ('Integral terça e quinta', 6, 6.5, 6, 6.5, 6, 0, 0); 51 | 52 | CREATE TABLE recurso_humano.perfil_expediente( 53 | id SERIAL NOT NULL PRIMARY KEY, 54 | usuario_id INTEGER NOT NULL REFERENCES dgeo.usuario (id), 55 | expediente_id INTEGER NOT NULL REFERENCES recurso_humano.expediente (id), 56 | data_inicio timestamp with time zone NOT NULL, 57 | data_fim timestamp with time zone 58 | ); 59 | 60 | CREATE TABLE recurso_humano.informacoes_usuario( 61 | id SERIAL NOT NULL PRIMARY KEY, 62 | usuario_id INTEGER NOT NULL REFERENCES dgeo.usuario (id), 63 | turma_promocao INTEGER, 64 | antiguidade_turma INTEGER, 65 | data_nascimento DATE, 66 | telefone VARCHAR(255), 67 | identidade VARCHAR(255), 68 | cpf VARCHAR(255), 69 | email_eb VARCHAR(255), 70 | email VARCHAR(255), 71 | codigo_banco VARCHAR(255), 72 | banco VARCHAR(255), 73 | agencia_bancaria VARCHAR(255), 74 | conta_bancaria VARCHAR(255), 75 | UNIQUE(usuario_id) 76 | ); 77 | 78 | CREATE TABLE recurso_humano.banco_dispensas( 79 | id SERIAL NOT NULL PRIMARY KEY, 80 | usuario_id INTEGER NOT NULL REFERENCES dgeo.usuario (id), 81 | motivo_dispensa TEXT NOT NULL, 82 | dias_totais INTEGER NOT NULL, 83 | dias_restantes INTEGER NOT NULL 84 | ); 85 | 86 | CREATE TABLE dominio.tipo_perda_recurso_humano( 87 | code SMALLINT NOT NULL PRIMARY KEY, 88 | nome VARCHAR(255) NOT NULL 89 | ); 90 | 91 | INSERT INTO dominio.tipo_perda_recurso_humano (code, nome) VALUES 92 | (1, 'Atividades militares'), 93 | (2, 'Atividades administrativas'), 94 | (3, 'Problemas técnicos'), 95 | (4, 'Feriado'), 96 | (5, 'Férias'), 97 | (6, 'Dispensa por motivo de saúde'), 98 | (7, 'Dispensa como recompensa'), 99 | (8, 'Dispensa por regresso de atividade de campo'), 100 | (9, 'Designação para realizar curso / capacitação'), 101 | (10, 'Designação para ministrar curso / capacitação'), 102 | (11, 'Designação para participação em eventos'), 103 | (99, 'Outros'); 104 | 105 | GRANT USAGE ON SCHEMA recurso_humano TO $1:name; 106 | GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA recurso_humano TO $1:name; 107 | GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA recurso_humano TO $1:name; 108 | 109 | COMMIT; -------------------------------------------------------------------------------- /er_microcontrole/microcontrole.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | CREATE EXTENSION IF NOT EXISTS postgis; 3 | 4 | CREATE SCHEMA microcontrole; 5 | 6 | CREATE TABLE microcontrole.tipo_operacao( 7 | code SMALLINT NOT NULL PRIMARY KEY, 8 | nome VARCHAR(255) NOT NULL 9 | ); 10 | 11 | INSERT INTO microcontrole.tipo_operacao (code, nome) VALUES 12 | (1, 'INSERT'), 13 | (2, 'DELETE'), 14 | (3, 'UPDATE ATRIBUTE'), 15 | (4, 'UPDATE GEOM'); 16 | 17 | CREATE TABLE microcontrole.monitoramento_feicao( 18 | id SERIAL NOT NULL PRIMARY KEY, 19 | tipo_operacao_id SMALLINT NOT NULL REFERENCES microcontrole.tipo_operacao (code), 20 | camada varchar(255) NOT NULL, 21 | quantidade integer NOT NULL, 22 | comprimento real NOT NULL, 23 | vertices integer NOT NULL, 24 | data timestamp with time zone NOT NULL, 25 | atividade_id INTEGER NOT NULL, 26 | usuario_id INTEGER NOT NULL 27 | ); 28 | 29 | CREATE INDEX monitoramento_feicao_idx 30 | ON microcontrole.monitoramento_feicao USING btree 31 | (data DESC) 32 | TABLESPACE pg_default; 33 | 34 | CREATE TABLE microcontrole.monitoramento_tela( 35 | id SERIAL NOT NULL PRIMARY KEY, 36 | data timestamp with time zone NOT NULL, 37 | zoom REAL NOT NULL, 38 | atividade_id INTEGER NOT NULL, 39 | usuario_id INTEGER NOT NULL, 40 | geom geometry(POLYGON, 4326) NOT NULL 41 | ); 42 | 43 | CREATE INDEX monitoramento_tela_geom 44 | ON microcontrole.monitoramento_tela USING gist 45 | (geom) 46 | TABLESPACE pg_default; 47 | 48 | CREATE INDEX monitoramento_tela_idx 49 | ON microcontrole.monitoramento_tela USING btree 50 | (data DESC) 51 | TABLESPACE pg_default; 52 | 53 | CREATE INDEX monitoramento_tela_data_idx ON microcontrole.monitoramento_tela USING BRIN (data) WITH (pages_per_range = 128); 54 | CREATE INDEX monitoramento_tela_atividade_id_idx ON microcontrole.monitoramento_tela (atividade_id); 55 | 56 | COMMIT; -------------------------------------------------------------------------------- /er_microcontrole/permissao.sql: -------------------------------------------------------------------------------- 1 | GRANT USAGE ON schema microcontrole TO $1:name; 2 | GRANT SELECT ON ALL TABLES IN SCHEMA microcontrole TO $1:name; -------------------------------------------------------------------------------- /er_microcontrole/versao.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE TABLE public.versao( 4 | code SMALLINT NOT NULL PRIMARY KEY, 5 | nome VARCHAR(255) NOT NULL 6 | ); 7 | 8 | INSERT INTO public.versao (code, nome) VALUES 9 | (1, '1.0.0'); 10 | 11 | COMMIT; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sap", 3 | "version": "2.0.0", 4 | "description": "Sistema de Apoio a Produção", 5 | "main": "src/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/1cgeo/sap" 9 | }, 10 | "scripts": { 11 | "install-all": "npm install && cd server && npm install && cd ../client && npm install", 12 | "config": "node create_config.js", 13 | "build": "node create_build.js", 14 | "start": "pm2 start server/src/index.js --name sap", 15 | "start-dev": "concurrently \"cd server && npm run dev\"" 16 | }, 17 | "nodemonConfig": { 18 | "ignore": [ 19 | "src/js_docs/*" 20 | ] 21 | }, 22 | "keywords": [ 23 | "Controle de Produção", 24 | "Node", 25 | "Express" 26 | ], 27 | "author": "DSG/1CGEO ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/1cgeo/sap/issues" 31 | }, 32 | "dependencies": { 33 | "axios": "^0.27.2", 34 | "bcryptjs": "^2.4.3", 35 | "bluebird": "^3.7.2", 36 | "colors": "^1.4.0", 37 | "commander": "^11.0.0", 38 | "concurrently": "^7.2.2", 39 | "fs-extra": "^10.1.0", 40 | "inquirer": "8.2.2", 41 | "npm-check-updates": "^15.0.2", 42 | "npm-run": "^5.0.1", 43 | "pg-promise": "^10.11.1", 44 | "pgtools": "^0.3.2", 45 | "pm2": "^5.2.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sap", 3 | "version": "2.0.0", 4 | "main": "src/index.js", 5 | "scripts": { 6 | "create-docs": "node create_documentation.js", 7 | "dev": "nodemon src/index.js", 8 | "dev-https": "nodemon src/index.js --https", 9 | "production": "pm2 start src/index.js --name sap", 10 | "production-https": "pm2 start src/index.js --name sap-https -- --https " 11 | }, 12 | "nodemonConfig": { 13 | "ignore": [ 14 | "src/js_docs/*" 15 | ] 16 | }, 17 | "license": "MIT", 18 | "dependencies": { 19 | "archiver": "^5.3.1", 20 | "axios": "^0.27.2", 21 | "better-queue": "^3.8.12", 22 | "bluebird": "^3.7.2", 23 | "body-parser": "^1.20.0", 24 | "colors": "^1.4.0", 25 | "cors": "^2.8.5", 26 | "date-fns": "^2.28.0", 27 | "documentation": "^13.2.5", 28 | "dotenv": "^16.0.1", 29 | "express": "^4.18.1", 30 | "express-rate-limit": "^6.4.0", 31 | "helmet": "^5.1.0", 32 | "hpp": "^0.2.3", 33 | "inquirer": "^9.0.0", 34 | "joi": "^17.6.0", 35 | "jsdoc": "^3.6.10", 36 | "jsonwebtoken": "^8.5.1", 37 | "minimist": "^1.2.6", 38 | "nocache": "^3.0.4", 39 | "nodemon": "^2.0.18", 40 | "nunjucks": "^3.2.3", 41 | "pg-promise": "^10.11.1", 42 | "pgtools": "^0.3.2", 43 | "pm2": "^5.2.0", 44 | "semver": "^7.3.7", 45 | "serialize-error": "8.1.0", 46 | "swagger-jsdoc": "^6.2.1", 47 | "swagger-ui-express": "^4.4.0", 48 | "winston": "^3.8.1", 49 | "winston-daily-rotate-file": "^4.7.1" 50 | }, 51 | "devDependencies": { 52 | "standard": "^17.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /server/src/acompanhamento/acompanhamento_schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Joi = require('joi') 4 | 5 | const models = {} 6 | 7 | models.loteSubfaseParams = Joi.object().keys({ 8 | lote: Joi.number().integer() 9 | .required(), 10 | subfase: Joi.number().integer() 11 | .required() 12 | }) 13 | 14 | models.loteParams = Joi.object().keys({ 15 | lote: Joi.number().integer() 16 | .required() 17 | }) 18 | 19 | models.anoParam = Joi.object().keys({ 20 | anoParam: Joi.string() 21 | .regex(/^20[0-3][0-9]$/) 22 | .required() 23 | }) 24 | 25 | models.nomeParams = Joi.object().keys({ 26 | nome: Joi.string().required() 27 | }) 28 | 29 | /* 30 | 31 | 32 | models.mesParam = Joi.object().keys({ 33 | mes: Joi.string() 34 | .regex(/^(1|2|3|4|5|6|7|8|9|10|11|12)$/) 35 | .required() 36 | }) 37 | 38 | 39 | models.finalizadoQuery = Joi.object().keys({ 40 | finalizado: Joi.string().valid('true', 'false') 41 | }) 42 | 43 | models.mvtParams = Joi.object().keys({ 44 | nome: Joi.string().required(), 45 | x: Joi.number() 46 | .integer() 47 | .required(), 48 | y: Joi.number() 49 | .integer() 50 | .required(), 51 | z: Joi.number() 52 | .integer() 53 | .required() 54 | }) 55 | 56 | models.diasQuery = Joi.object().keys({ 57 | dias: Joi.number().integer() 58 | }) 59 | 60 | models.perdaRecursoHumano = Joi.object().keys({ 61 | perda_recurso_humano: Joi.array().items( 62 | Joi.object().keys({ 63 | usuario_id: Joi.number() 64 | .integer() 65 | .strict() 66 | .required(), 67 | tipo_perda_recurso_humano_id: Joi.number() 68 | .integer() 69 | .strict() 70 | .required(), 71 | horas: Joi.number() 72 | .strict() 73 | .required(), 74 | data: Joi.date().required(), 75 | observacao: Joi.string().required() 76 | }) 77 | ) 78 | .required() 79 | .min(1) 80 | }) 81 | */ 82 | module.exports = models 83 | -------------------------------------------------------------------------------- /server/src/acompanhamento/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | acompanhamentoRoute: require('./acompanhamento_route') 5 | } 6 | -------------------------------------------------------------------------------- /server/src/authentication/authenticate_user.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const axios = require('axios') 4 | 5 | const { AppError, httpCode } = require('../utils') 6 | 7 | const { AUTH_SERVER } = require('../config') 8 | 9 | const authorization = async (usuario, senha, aplicacao) => { 10 | const server = `${AUTH_SERVER}/api/login` 11 | try { 12 | const response = await axios.post(server, { 13 | usuario, 14 | senha, 15 | aplicacao 16 | }) 17 | 18 | if (!response || response.status !== 201 || !('data' in response)) { 19 | throw new Error() 20 | } 21 | 22 | return response.data.success || false 23 | } catch (err) { 24 | if ( 25 | 'response' in err && 26 | 'data' in err.response && 27 | 'message' in err.response.data 28 | ) { 29 | throw new AppError( 30 | err.response.data.message, 31 | httpCode.BadRequest 32 | ) 33 | } else { 34 | throw new AppError( 35 | 'Erro ao se comunicar com o servidor de autenticação' 36 | ) 37 | } 38 | } 39 | } 40 | 41 | module.exports = authorization 42 | -------------------------------------------------------------------------------- /server/src/authentication/get_usuarios.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const axios = require('axios') 4 | 5 | const { AppError, httpCode } = require('../utils') 6 | 7 | const { AUTH_SERVER } = require('../config') 8 | 9 | const getUsuarios = async () => { 10 | const server = `${AUTH_SERVER}/api/usuarios` 11 | try { 12 | const response = await axios.get(server) 13 | 14 | if ( 15 | !response || 16 | response.status !== 200 || 17 | !('data' in response) || 18 | !('dados' in response.data) 19 | ) { 20 | throw new Error() 21 | } 22 | 23 | return response.data.dados 24 | } catch (err) { 25 | if ( 26 | 'response' in err && 27 | 'data' in err.response && 28 | 'message' in err.response.data 29 | ) { 30 | throw new AppError(err.response.data.message, httpCode.BadRequest) 31 | } else { 32 | throw new AppError('Erro ao se comunicar com o servidor de autenticação') 33 | } 34 | } 35 | } 36 | 37 | module.exports = getUsuarios 38 | -------------------------------------------------------------------------------- /server/src/authentication/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | authenticateUser: require('./authenticate_user'), 5 | verifyAuthServer: require('./verify_server'), 6 | getUsuariosAuth: require('./get_usuarios') 7 | } 8 | -------------------------------------------------------------------------------- /server/src/authentication/verify_server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const axios = require('axios') 4 | 5 | const { AppError } = require('../utils') 6 | 7 | const { AUTH_SERVER } = require('../config') 8 | 9 | const verifyAuthServer = async () => { 10 | try { 11 | const response = await axios.get(`${AUTH_SERVER}/api`) 12 | const test = 13 | !response || 14 | response.status !== 200 || 15 | !('data' in response) || 16 | response.data.message !== 'Serviço de autenticação operacional' 17 | if (test) { 18 | throw new Error() 19 | } 20 | } catch (e) { 21 | throw new AppError('Erro ao se comunicar com o servidor de autenticação', null, e) 22 | } 23 | } 24 | 25 | module.exports = verifyAuthServer 26 | -------------------------------------------------------------------------------- /server/src/campo/campo_schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Joi = require('joi') 4 | 5 | const models = {} 6 | 7 | models.idParams = Joi.object().keys({ 8 | id: Joi.number().integer().required() 9 | }) 10 | 11 | models.uuidParams = Joi.object().keys({ 12 | uuid: Joi.string().uuid().required().min(1) 13 | }) 14 | 15 | models.campo = Joi.object().keys({ 16 | campo: Joi.object() 17 | .keys({ 18 | nome: Joi.string().required(), 19 | descricao: Joi.string().required().allow(null), 20 | orgao: Joi.string().required(), 21 | pit: Joi.number().integer().required().strict(), 22 | militares: Joi.string().required().allow(null), 23 | placas_vtr: Joi.string().required().allow(null), 24 | inicio: Joi.date().required().allow(null), 25 | fim: Joi.date().required().allow(null), 26 | situacao_id: Joi.number().integer().required().strict(), 27 | geom: Joi.string().required() 28 | }) 29 | .required() 30 | }) 31 | 32 | models.fotos = Joi.object().keys({ 33 | fotos: Joi.array().items( 34 | Joi.object().keys({ 35 | campo_id: Joi.string().uuid().required(), 36 | descricao: Joi.string().required(), 37 | data_imagem: Joi.date().required(), 38 | imagem_base64: Joi.string().base64().max(10485760).required() // Base64 com no máximo 10MB 39 | }) 40 | ).required() 41 | }) 42 | 43 | models.fotoUpdate = Joi.object().keys({ 44 | foto: Joi.object() 45 | .keys({ 46 | descricao: Joi.string().allow(null), 47 | data_imagem: Joi.date().allow(null) 48 | }) 49 | .required() 50 | }) 51 | 52 | // Esquema para criação/atualização de track 53 | models.track = Joi.object().keys({ 54 | track: Joi.object() 55 | .keys({ 56 | chefe_vtr: Joi.string().required(), 57 | motorista: Joi.string().required(), 58 | placa_vtr: Joi.string().required(), 59 | dia: Joi.date().required(), 60 | campo_id: Joi.string().uuid().required() 61 | }) 62 | .required() 63 | }) 64 | 65 | // Esquema para atualização de track 66 | models.trackUpdate = Joi.object().keys({ 67 | track: Joi.object() 68 | .keys({ 69 | chefe_vtr: Joi.string(), 70 | motorista: Joi.string(), 71 | placa_vtr: Joi.string(), 72 | dia: Joi.date(), 73 | geom: Joi.object() // GeoJSON 74 | }) 75 | .required() 76 | }) 77 | 78 | models.loteidParams = Joi.object().keys({ 79 | lote_id: Joi.number().integer().strict().required() 80 | }) 81 | 82 | module.exports = models 83 | -------------------------------------------------------------------------------- /server/src/campo/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | campoRoute: require('./campo_route') 5 | } 6 | -------------------------------------------------------------------------------- /server/src/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const dotenv = require('dotenv') 4 | const Joi = require('joi') 5 | const fs = require('fs') 6 | const path = require('path') 7 | 8 | const AppError = require('./utils/app_error') 9 | const errorHandler = require('./utils/error_handler') 10 | 11 | const configFile = 12 | process.env.NODE_ENV === 'test' ? 'config_testing.env' : 'config.env' 13 | 14 | const configPath = path.join(__dirname, '..', configFile) 15 | 16 | if (!fs.existsSync(configPath)) { 17 | errorHandler.critical( 18 | new AppError( 19 | 'Arquivo de configuração não encontrado. Configure o serviço primeiro.' 20 | ) 21 | ) 22 | } 23 | 24 | dotenv.config({ 25 | path: configPath 26 | }) 27 | 28 | const VERSION = '2.2.3' 29 | const MIN_DATABASE_VERSION = '2.2.3' 30 | const MIN_MICROCONTROLE_VERSION = '1.0.0' 31 | 32 | 33 | const configSchema = Joi.object().keys({ 34 | PORT: Joi.number() 35 | .integer() 36 | .required(), 37 | DB_SERVER: Joi.string().required(), 38 | DB_PORT: Joi.number() 39 | .integer() 40 | .required(), 41 | DB_NAME: Joi.string().required(), 42 | DB_NAME_MICROCONTROLE: Joi.string().required(), 43 | DB_USER: Joi.string().required(), 44 | DB_PASSWORD: Joi.string().required(), 45 | JWT_SECRET: Joi.string().required(), 46 | AUTH_SERVER: Joi.string() 47 | .uri() 48 | .required(), 49 | VERSION: Joi.string().required(), 50 | MIN_DATABASE_VERSION: Joi.string().required(), 51 | MIN_MICROCONTROLE_VERSION: Joi.string().required() 52 | }) 53 | 54 | /** 55 | * Objeto de configuração 56 | * @description 57 | * Objeto que guarda as constantes da aplicação para fins de configuração do serviço 58 | * @typedef {Object} config 59 | * @property {number} PORT - Porta do serviço do SAP 60 | * @property {string} DB_SERVER - Servidor do banco de dados do banco do SAP 61 | * @property {string} DB_PORT - Porta do banco de dados do banco do SAP 62 | * @property {string} DB_NAME - Nome do banco de dados do banco do SAP 63 | * @property {string} DB_NAME_MICROCONTROLE - Nome do banco de dados de microcontrole 64 | * @property {string} DB_USER - Nome do usuário administrador do banco de dados utilizado para controlar o SAP 65 | * @property {string} DB_PASSWORD - Senha do usuário administrador do banco de dados utilizado para controlar o SAP 66 | * @property {string} JWT_SECRET - Texto utilizado para encriptar o Json Web Token 67 | * @property {string} AUTH_SERVER - URL para o serviço de autenticação 68 | * @property {string} VERSION - Versão da aplicação do SAP 69 | * @property {string} MIN_DATABASE_VERSION - Versão mínima do banco de dados do SAP compatível com a versão da aplicação 70 | * @property {string} MIN_MICROCONTROLE_VERSION - Versão mínima do banco de dados de microcontrole 71 | */ 72 | const config = { 73 | PORT: process.env.PORT, 74 | DB_SERVER: process.env.DB_SERVER, 75 | DB_PORT: process.env.DB_PORT, 76 | DB_NAME: process.env.DB_NAME, 77 | DB_NAME_MICROCONTROLE: process.env.DB_NAME_MICROCONTROLE, 78 | DB_USER: process.env.DB_USER, 79 | DB_PASSWORD: process.env.DB_PASSWORD, 80 | JWT_SECRET: process.env.JWT_SECRET, 81 | AUTH_SERVER: process.env.AUTH_SERVER, 82 | VERSION, 83 | MIN_DATABASE_VERSION, 84 | MIN_MICROCONTROLE_VERSION 85 | } 86 | 87 | const { error } = configSchema.validate(config, { 88 | abortEarly: false 89 | }) 90 | if (error) { 91 | const { details } = error 92 | const message = details.map(i => i.message).join(',') 93 | 94 | errorHandler.critical( 95 | new AppError( 96 | 'Arquivo de configuração inválido. Configure novamente o serviço.', 97 | null, 98 | message 99 | ) 100 | ) 101 | } 102 | 103 | module.exports = config 104 | -------------------------------------------------------------------------------- /server/src/database/database_version.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const semver = require('semver') 4 | 5 | const db = require('./db') 6 | 7 | const { AppError } = require('../utils') 8 | 9 | const { MIN_DATABASE_VERSION } = require('../config') 10 | 11 | const dbVersion = {} 12 | 13 | const validate = dbv => { 14 | if (semver.lt(semver.coerce(dbv), semver.coerce(MIN_DATABASE_VERSION))) { 15 | throw new AppError( 16 | `Versão do banco de dados (${dbv}) não compatível com a versão do SAP. A versão deve ser superior a ${MIN_DATABASE_VERSION}.` 17 | ) 18 | } 19 | } 20 | 21 | /** 22 | * Carrega assincronamente o nome da versão do banco de dados 23 | */ 24 | dbVersion.load = async () => { 25 | if (!('nome' in dbVersion)) { 26 | const dbv = await db.sapConn.oneOrNone('SELECT nome FROM public.versao') 27 | 28 | if (!dbv) { 29 | throw new AppError( 30 | 'O banco de dados não não é compatível com a versão do SAP.' 31 | ) 32 | } 33 | validate(dbv.nome) 34 | dbVersion.nome = dbv.nome 35 | } 36 | } 37 | 38 | module.exports = dbVersion 39 | -------------------------------------------------------------------------------- /server/src/database/database_version_microcontrole.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const semver = require('semver') 4 | 5 | const db = require('./db') 6 | 7 | const { AppError } = require('../utils') 8 | 9 | const { MIN_MICROCONTROLE_VERSION } = require('../config') 10 | 11 | const dbVersion = {} 12 | 13 | const validate = dbv => { 14 | if (semver.lt(semver.coerce(dbv), semver.coerce(MIN_MICROCONTROLE_VERSION))) { 15 | throw new AppError( 16 | `Versão do banco de dados (${dbv}) não compatível com a versão do SAP Microcontrole. A versão deve ser superior a ${MIN_MICROCONTROLE_VERSION}.` 17 | ) 18 | } 19 | } 20 | 21 | /** 22 | * Carrega assincronamente o nome da versão do banco de dados 23 | */ 24 | dbVersion.load = async () => { 25 | if (!('nome' in dbVersion)) { 26 | const dbv = await db.microConn.oneOrNone('SELECT nome FROM public.versao') 27 | 28 | if (!dbv) { 29 | throw new AppError( 30 | 'O banco de dados não não é compatível com a versão do SAP Microcontrole.' 31 | ) 32 | } 33 | validate(dbv.nome) 34 | dbVersion.nome = dbv.nome 35 | } 36 | } 37 | 38 | module.exports = dbVersion 39 | -------------------------------------------------------------------------------- /server/src/database/db.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { errorHandler } = require('../utils') 4 | 5 | const { DB_USER, DB_PASSWORD, DB_SERVER, DB_PORT, DB_NAME, DB_NAME_MICROCONTROLE } = require('../config') 6 | 7 | const promise = require('bluebird') 8 | 9 | const testeDBs = {} 10 | 11 | const db = {} 12 | 13 | db.pgp = require('pg-promise')({ 14 | promiseLib: promise 15 | }) 16 | 17 | db.createConn = async (user, password, host, port, database, handle = true) => { 18 | const key = `${user}@${host}:${port}/${database}` 19 | const cn = { host, port, database, user, password } 20 | 21 | let newDB 22 | 23 | if (key in testeDBs) { 24 | newDB = testeDBs[key] 25 | newDB.$pool.options.password = password 26 | } else { 27 | newDB = db.pgp(cn) 28 | testeDBs[key] = newDB 29 | 30 | await newDB 31 | .connect() 32 | .then(obj => { 33 | obj.done() 34 | }) 35 | .catch(e => { 36 | if (!handle) { 37 | throw new Error() 38 | } 39 | errorHandler.critical(e) 40 | }) 41 | } 42 | 43 | return newDB 44 | } 45 | 46 | db.testConn = async (usuario, senha, server, port, dbname) => { 47 | try { 48 | await db.createConn(usuario, senha, server, port, dbname, false) 49 | 50 | return true 51 | } catch (e) { 52 | return false 53 | } 54 | } 55 | 56 | db.createAdminConn = async (server, port, dbname, handle) => { 57 | return db.createConn(DB_USER, DB_PASSWORD, server, port, dbname, handle) 58 | } 59 | 60 | db.createSapConn = async () => { 61 | db.sapConn = await db.createAdminConn(DB_SERVER, DB_PORT, DB_NAME, true) 62 | } 63 | 64 | db.createMicroConn = async () => { 65 | db.microConn = await db.createAdminConn(DB_SERVER, DB_PORT, DB_NAME_MICROCONTROLE, true) 66 | } 67 | 68 | module.exports = db 69 | -------------------------------------------------------------------------------- /server/src/database/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | db: require('./db'), 5 | databaseVersion: require('./database_version'), 6 | microcontroleDatabaseVersion: require('./database_version_microcontrole'), 7 | sqlFile: require('./sql_file'), 8 | temporaryLogin: require('./temporary_login'), 9 | managePermissions: require('./manage_permissions'), 10 | disableTriggers: require('./disable_triggers') 11 | } 12 | -------------------------------------------------------------------------------- /server/src/database/sql/revoke.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Retorna SQL de revoke 3 | */ 4 | SELECT string_agg(query, ' ') AS revoke_query FROM ( 5 | SELECT DISTINCT 'REVOKE ALL ON TABLE ' || table_schema || '.' || table_name || ' FROM ' || $1 || ';' AS query 6 | FROM information_schema.table_privileges 7 | WHERE grantee ~* $1 AND table_schema NOT IN ('information_schema') AND table_schema !~ '^pg_' 8 | UNION ALL 9 | SELECT DISTINCT 'REVOKE ALL ON FUNCTION ' || routine_schema || '.' || routine_name || '(' 10 | || pg_get_function_identity_arguments( 11 | (regexp_matches(specific_name, E'.*\_([0-9]+)'))[1]::oid) || ') FROM ' || $1 || ';' AS query 12 | FROM information_schema.routine_privileges 13 | WHERE grantee ~* $1 AND routine_schema != 'pg_catalog' 14 | UNION ALL 15 | SELECT 'REVOKE ALL ON SEQUENCE ' || sequence_schema || '.' || sequence_name || ' FROM ' || $1 || ';' AS query 16 | FROM information_schema.sequences 17 | UNION ALL 18 | SELECT 'REVOKE ALL ON SCHEMA ' || schema_name || ' FROM ' || $1 || ';' AS query 19 | FROM information_schema.schemata 20 | WHERE schema_name NOT IN ('information_schema') AND schema_name !~ '^pg_' 21 | UNION ALL 22 | SELECT 'REVOKE CONNECT ON DATABASE ' || current_database() || ' FROM ' || $1 || ';' AS query 23 | ) AS foo; -------------------------------------------------------------------------------- /server/src/database/sql/revoke_all_users.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Retorna SQL de revoke de todos os usuarios 3 | */ 4 | SELECT string_agg(query, ' ') AS revoke_query FROM ( 5 | SELECT DISTINCT 'REVOKE ALL ON TABLE ' || table_schema || '.' || table_name || ' FROM ' || grantee || ';' AS query 6 | FROM information_schema.table_privileges 7 | WHERE table_schema NOT IN ('information_schema', 'public') AND table_schema !~ '^pg_' 8 | UNION ALL 9 | SELECT DISTINCT 'REVOKE ALL ON FUNCTION ' || routine_schema || '.' || routine_name || '(' 10 | || pg_get_function_identity_arguments( 11 | (regexp_matches(specific_name, E'.*\_([0-9]+)'))[1]::oid) || ') FROM ' || grantee || ';' AS query 12 | FROM information_schema.routine_privileges 13 | WHERE routine_schema NOT IN ('information_schema', 'public', 'PUBLIC') AND routine_schema !~ '^pg_' 14 | UNION ALL 15 | SELECT 'REVOKE ALL ON SEQUENCE ' || ss.sequence_schema || '.' || ss.sequence_name || ' FROM ' || rtg.grantee || ';' AS query 16 | FROM information_schema.sequences AS ss CROSS JOIN (SELECT DISTINCT grantee FROM information_schema.role_table_grants) AS rtg 17 | UNION ALL 18 | SELECT 'REVOKE ALL ON SCHEMA ' || ss.schema_name || ' FROM ' || rtg.grantee || ';' AS query 19 | FROM information_schema.schemata AS ss CROSS JOIN (SELECT DISTINCT grantee FROM information_schema.role_table_grants) AS rtg 20 | WHERE ss.schema_name NOT IN ('information_schema') AND ss.schema_name !~ '^pg_' 21 | UNION ALL 22 | SELECT 'REVOKE CONNECT ON DATABASE ' || current_database() || ' FROM ' || rtg.grantee || ';' AS query 23 | FROM (SELECT DISTINCT grantee FROM information_schema.role_table_grants) AS rtg 24 | ) AS foo; -------------------------------------------------------------------------------- /server/src/database/sql_file.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { QueryFile, PreparedStatement: PS } = require('pg-promise') 4 | 5 | const { AppError, errorHandler } = require('../utils') 6 | 7 | /** 8 | * Lê arquivo SQL e retorna QueryFile para uso no pgPromise 9 | * @param {string} file - Full path to SQL file 10 | * @returns {QueryFile} Retorna objeto QueryFile do pgPromise 11 | */ 12 | const readSqlFile = file => { 13 | const qf = new QueryFile(file, { minify: true }) 14 | 15 | if (qf.error) { 16 | throw new AppError('Erro carregando os arquivos SQL', null, qf.error) 17 | } 18 | return qf 19 | } 20 | 21 | /** 22 | * Cria um PreparedStatement para uso no pgPromise 23 | * @param {string} sql - Full path to SQL file 24 | * @returns {PreparedStatement} Retorna objeto PreparedStatement do pgPromise 25 | */ 26 | const createPS = sql => { 27 | try { 28 | const psName = sql.split(/.*[/|\\]/)[1].replace('.sql', '') 29 | 30 | return new PS({ name: psName, text: readSqlFile(sql) }) 31 | } catch (e) { 32 | errorHandler.critical(e) 33 | } 34 | } 35 | 36 | module.exports = { readSqlFile, createPS } 37 | -------------------------------------------------------------------------------- /server/src/gerencia/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | gerenciaRoute: require('./gerencia_route') 5 | } 6 | -------------------------------------------------------------------------------- /server/src/gerencia/qgis_project.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { 4 | DB_NAME, 5 | DB_SERVER, 6 | DB_PORT, 7 | DB_USER, 8 | DB_PASSWORD 9 | } = require('../config') 10 | 11 | const fs = require('fs') 12 | const path = require('path') 13 | 14 | const qgisProject = (() => { 15 | return fs 16 | .readFileSync( 17 | path.join(__dirname, '../templates/sap_config_template.qgs'), 18 | 'utf-8' 19 | ) 20 | .replace(/{{DATABASE}}/g, DB_NAME) 21 | .replace(/{{HOST}}/g, DB_SERVER) 22 | .replace(/{{PORT}}/g, DB_PORT) 23 | .replace(/{{USER}}/g, DB_USER) 24 | .replace(/{{PASSWORD}}/g, DB_PASSWORD) 25 | })() 26 | 27 | module.exports = qgisProject -------------------------------------------------------------------------------- /server/src/gerenciador_fme/check_connection.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const axios = require('axios') 4 | 5 | const { AppError, httpCode } = require('../utils') 6 | 7 | const checkFMEConnection = async (url) => { 8 | try { 9 | const serverurl = `${url}/api` 10 | const response = await axios.get(serverurl) 11 | 12 | if (!response || 13 | response.status !== 200 || 14 | !('data' in response) || 15 | response.data.message !== 'Serviço do Gerenciador do FME operacional') { 16 | throw new Error() 17 | } 18 | } catch (e) { 19 | throw new AppError( 20 | 'Erro ao se comunicar com o servidor do gerenciador do FME', 21 | httpCode.BadRequest, 22 | e 23 | ) 24 | } 25 | } 26 | 27 | module.exports = checkFMEConnection 28 | -------------------------------------------------------------------------------- /server/src/gerenciador_fme/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | checkFMEConnection: require('./check_connection'), 5 | validadeParameters: require('./validate_parameters') 6 | } 7 | -------------------------------------------------------------------------------- /server/src/gerenciador_fme/validate_parameters.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const axios = require('axios') 4 | 5 | const { AppError, httpCode } = require('../utils') 6 | 7 | const { db } = require('../database') 8 | 9 | const verifyParameters = parameters => { 10 | const possibleParameters = [ 11 | 'dbname', 12 | 'dbhost', 13 | 'dbport', 14 | 'dbarea', 15 | 'dbuser', 16 | 'dbpassword', 17 | 'sapsubfase', 18 | 'LOG_FILE' 19 | ] 20 | return parameters.every(p => possibleParameters.some(pp => p.includes(pp))) 21 | } 22 | 23 | const getRotinas = async servidorId => { 24 | const serverInfo = await db.sapConn.oneOrNone( 25 | ` 26 | SELECT url FROM dgeo.gerenciador_fme WHERE id = $ 27 | `, 28 | { servidorId } 29 | ) 30 | if (!serverInfo) { 31 | throw new AppError( 32 | 'Gerenciador do FME informado não está cadastrado no SAP.', 33 | httpCode.BadRequest 34 | ) 35 | } 36 | try { 37 | const serverurl = `${serverInfo.servidor}:${serverInfo.porta}/api/rotinas` 38 | const response = await axios.get(serverurl) 39 | if (!response || 40 | response.status !== 200 || 41 | !('data' in response) || 42 | !('dados' in response.data)) { 43 | throw new Error() 44 | } 45 | return response.data.dados 46 | } catch (e) { 47 | throw new AppError( 48 | 'Erro ao se comunicar com o servidor do gerenciador do FME', 49 | null, 50 | e 51 | ) 52 | } 53 | } 54 | 55 | const validadeParameters = async rotinas => { 56 | const servidores = rotinas 57 | .map(c => c.servidor) 58 | .filter((v, i, array) => array.indexOf(v) === i) 59 | 60 | const dadosServidores = {} 61 | for (const s of servidores) { 62 | dadosServidores[s] = await getRotinas(s) 63 | } 64 | 65 | rotinas.forEach(r => { 66 | dadosServidores[r.servidor].forEach(v => { 67 | if (!verifyParameters(v.parametros)) { 68 | throw new AppError( 69 | `A rotina ${r.rotina} não é compatível com o SAP. Verifique seus parâmetros`, 70 | httpCode.BadRequest 71 | ) 72 | } 73 | }) 74 | }) 75 | return true 76 | } 77 | 78 | module.exports = validadeParameters 79 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | // Validates Node Version then starts the main code 2 | var version = process.versions.node.split('.') 3 | var major = +version[0] 4 | var minor = +version[1] 5 | 6 | if (major < 16 || (major === 16 && minor < 15)) { 7 | throw new Error('Versão mínima do Node.js suportada pelo Serviço é 16.15') 8 | } 9 | 10 | module.exports = require('./main') 11 | -------------------------------------------------------------------------------- /server/src/login/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | loginRoute: require('./login_route'), 5 | verifyLogin: require('./verify_login'), 6 | verifyAdmin: require('./verify_admin') 7 | } 8 | -------------------------------------------------------------------------------- /server/src/login/login_ctrl.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const jwt = require('jsonwebtoken') 4 | const semver = require('semver') 5 | 6 | const { db } = require('../database') 7 | 8 | const { AppError, httpCode } = require('../utils') 9 | 10 | const { JWT_SECRET } = require('../config') 11 | 12 | const { authenticateUser } = require('../authentication') 13 | 14 | const controller = {} 15 | 16 | const verificaQGIS = async (qgis) => { 17 | const qgisMinimo = await db.sapConn.oneOrNone( 18 | 'SELECT versao_minima FROM dgeo.versao_qgis LIMIT 1' 19 | ) 20 | if (!qgisMinimo) { 21 | return 22 | } 23 | const qgisVersionOk = 24 | qgis && 25 | semver.gte(semver.coerce(qgis), semver.coerce(qgisMinimo.versao_minima)) 26 | 27 | if (!qgisVersionOk) { 28 | const msg = `Versão incorreta do QGIS. A seguinte versão é necessária: ${qgisMinimo.versao_minima}` 29 | throw new AppError(msg, httpCode.BadRequest) 30 | } 31 | } 32 | 33 | const verificaPlugins = async (plugins) => { 34 | const pluginsMinimos = await db.sapConn.any( 35 | 'SELECT nome, versao_minima FROM dgeo.plugin' 36 | ) 37 | if (!pluginsMinimos) { 38 | return 39 | } 40 | 41 | for (let i = 0; i < pluginsMinimos.length; i++) { 42 | let notFound = true 43 | if (plugins) { 44 | plugins.forEach((p) => { 45 | if ( 46 | p.nome === pluginsMinimos[i].nome && 47 | semver.gte( 48 | semver.coerce(p.versao), 49 | semver.coerce(pluginsMinimos[i].versao_minima) 50 | ) 51 | ) { 52 | notFound = false 53 | } 54 | }) 55 | } 56 | if (notFound) { 57 | const listplugins = [] 58 | 59 | pluginsMinimos.forEach((pm) => { 60 | listplugins.push(pm.nome + ' - Versão: ' + pm.versao_minima) 61 | }) 62 | 63 | const msg = `Plugins desatualizados, não instalados ou desabilitados. Os seguintes plugins são necessários: \n ${listplugins.join( 64 | '\n ' 65 | )}` 66 | throw new AppError(msg, httpCode.BadRequest) 67 | } 68 | } 69 | } 70 | 71 | const gravaLogin = async (usuarioId) => { 72 | return db.sapConn.any( 73 | ` 74 | INSERT INTO acompanhamento.login(usuario_id, data_login) VALUES($, now()) 75 | `, 76 | { usuarioId } 77 | ) 78 | } 79 | 80 | const signJWT = (data, secret) => { 81 | return new Promise((resolve, reject) => { 82 | jwt.sign( 83 | data, 84 | secret, 85 | { 86 | expiresIn: '10h' 87 | }, 88 | (err, token) => { 89 | if (err) { 90 | reject(new AppError('Erro durante a assinatura do token', null, err)) 91 | } 92 | resolve(token) 93 | } 94 | ) 95 | }) 96 | } 97 | 98 | controller.login = async (usuario, senha, aplicacao, plugins, qgis) => { 99 | const usuarioDb = await db.sapConn.oneOrNone( 100 | 'SELECT id, uuid, administrador FROM dgeo.usuario WHERE login = $ and ativo IS TRUE', 101 | { usuario } 102 | ) 103 | if (!usuarioDb) { 104 | throw new AppError( 105 | 'Usuário não autorizado para utilizar o SAP', 106 | httpCode.BadRequest 107 | ) 108 | } 109 | 110 | const verifyAuthentication = await authenticateUser( 111 | usuario, 112 | senha, 113 | aplicacao 114 | ) 115 | if (!verifyAuthentication) { 116 | throw new AppError('Usuário ou senha inválida', httpCode.BadRequest) 117 | } 118 | 119 | if (aplicacao === 'sap_fp') { 120 | await verificaQGIS(qgis) 121 | 122 | await verificaPlugins(plugins) 123 | } 124 | const { id, administrador, uuid } = usuarioDb 125 | 126 | const token = await signJWT({ id, uuid, administrador }, JWT_SECRET) 127 | 128 | await gravaLogin(id) 129 | 130 | return { token, administrador, uuid } 131 | } 132 | 133 | module.exports = controller 134 | -------------------------------------------------------------------------------- /server/src/login/login_route.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const express = require('express') 4 | 5 | const { schemaValidation, asyncHandler, httpCode } = require('../utils') 6 | 7 | const loginCtrl = require('./login_ctrl') 8 | const loginSchema = require('./login_schema') 9 | 10 | const router = express.Router() 11 | 12 | /** 13 | * @swagger 14 | * /api/login: 15 | * post: 16 | * summary: Autenticação de um usuário 17 | * description: Retorna um token de autenticação caso o usuário seja válido e as versões dos plugins e do QGIS estejam corretas. 18 | * produces: 19 | * - application/json 20 | * tags: 21 | * - login 22 | * requestBody: 23 | * content: 24 | * application/json: 25 | * schema: 26 | * $ref: '#/components/schemas/LoginRequest' 27 | * responses: 28 | * 201: 29 | * description: Usuário autenticado com sucesso 30 | * content: 31 | * application/json: 32 | * schema: 33 | * type: object 34 | * properties: 35 | * success: 36 | * type: boolean 37 | * description: Indica se a requisição ocorreu com sucesso 38 | * message: 39 | * type: string 40 | * description: Descrição do resultado da requisição 41 | * dados: 42 | * type: object 43 | * properties: 44 | * administrador: 45 | * type: boolean 46 | * description: Indica se o usuário possui privilégios de administrador 47 | * token: 48 | * type: string 49 | * description: Token de login 50 | * 400: 51 | * description: Erro de validação ou autenticação 52 | * content: 53 | * application/json: 54 | * schema: 55 | * type: object 56 | * properties: 57 | * success: 58 | * type: boolean 59 | * message: 60 | * type: string 61 | * description: Descrição do erro ocorrido 62 | */ 63 | router.post( 64 | '/', 65 | schemaValidation({ body: loginSchema.login }), 66 | asyncHandler(async (req, res, next) => { 67 | const dados = await loginCtrl.login( 68 | req.body.usuario, 69 | req.body.senha, 70 | req.body.cliente, 71 | req.body.plugins, 72 | req.body.qgis 73 | ) 74 | 75 | return res.sendJsonAndLog( 76 | true, 77 | 'Usuário autenticado com sucesso', 78 | httpCode.Created, 79 | dados 80 | ) 81 | }) 82 | ) 83 | 84 | module.exports = router 85 | -------------------------------------------------------------------------------- /server/src/login/login_schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Joi = require('joi') 4 | 5 | const models = {} 6 | 7 | /** 8 | * @swagger 9 | * components: 10 | * schemas: 11 | * LoginRequest: 12 | * type: object 13 | * required: 14 | * - usuario 15 | * - senha 16 | * - cliente 17 | * properties: 18 | * usuario: 19 | * type: string 20 | * description: Nome do usuário 21 | * senha: 22 | * type: string 23 | * description: Senha do usuário 24 | * cliente: 25 | * type: string 26 | * enum: [sap_fp, sap_fg, sap] 27 | * description: Tipo de cliente que está fazendo a solicitação 28 | * plugins: 29 | * type: array 30 | * description: Lista de plugins em uso (requerido para sap_fp ou sap_fg) 31 | * items: 32 | * type: object 33 | * properties: 34 | * nome: 35 | * type: string 36 | * description: Nome do plugin 37 | * versao: 38 | * type: string 39 | * description: Versão do plugin 40 | * qgis: 41 | * type: string 42 | * description: Versão do QGIS em uso (requerido para sap_fp ou sap_fg) 43 | */ 44 | models.login = Joi.object().keys({ 45 | usuario: Joi.string().required(), 46 | senha: Joi.string().required(), 47 | cliente: Joi.string().valid('sap_fp', 'sap_fg', 'sap').required(), 48 | plugins: Joi.when('cliente', { 49 | is: Joi.string().regex(/^(sap_fp|sap_fg)$/), 50 | then: Joi.array() 51 | .items( 52 | Joi.object({ 53 | nome: Joi.string().required(), 54 | versao: Joi.string().required() 55 | }) 56 | ) 57 | .unique('nome') 58 | .required(), 59 | otherwise: Joi.forbidden() 60 | }), 61 | qgis: Joi.when('cliente', { 62 | is: Joi.string().regex(/^(sap_fp|sap_fg)$/), 63 | then: Joi.string().required(), 64 | otherwise: Joi.forbidden() 65 | }) 66 | }) 67 | 68 | module.exports = models 69 | -------------------------------------------------------------------------------- /server/src/login/validate_token.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const jwt = require('jsonwebtoken') 4 | 5 | const { AppError, httpCode } = require('../utils') 6 | 7 | const { JWT_SECRET } = require('../config') 8 | 9 | const decodeJwt = (token, secret) => { 10 | return new Promise((resolve, reject) => { 11 | jwt.verify(token, secret, (err, decoded) => { 12 | if (err) { 13 | reject( 14 | new AppError('Falha ao autenticar token', httpCode.Unauthorized, err) 15 | ) 16 | } 17 | resolve(decoded) 18 | }) 19 | }) 20 | } 21 | 22 | const validateToken = async token => { 23 | if (!token) { 24 | throw new AppError('Nenhum token fornecido', httpCode.Unauthorized) 25 | } 26 | if (token.startsWith('Bearer ')) { 27 | // Remove Bearer from string 28 | token = token.slice(7, token.length) 29 | } 30 | 31 | return decodeJwt(token, JWT_SECRET) 32 | } 33 | 34 | module.exports = validateToken 35 | -------------------------------------------------------------------------------- /server/src/login/verify_admin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { AppError, asyncHandler, httpCode } = require('../utils') 4 | 5 | const { db } = require('../database') 6 | 7 | const validateToken = require('./validate_token') 8 | 9 | // middleware para verificar se o usuário é administrador 10 | const verifyAdmin = asyncHandler(async (req, res, next) => { 11 | const token = req.headers.authorization 12 | 13 | const decoded = await validateToken(token) 14 | 15 | if (!('id' in decoded && decoded.id && 'uuid' in decoded && decoded.uuid)) { 16 | throw new AppError('Falta informação de usuário') 17 | } 18 | 19 | const { 20 | administrador 21 | } = await db.sapConn.oneOrNone( 22 | 'SELECT administrador FROM dgeo.usuario WHERE id = $ and ativo IS TRUE', 23 | { usuarioId: decoded.id } 24 | ) 25 | if (!administrador) { 26 | throw new AppError( 27 | 'Usuário necessita ser um administrador', 28 | httpCode.Forbidden 29 | ) 30 | } 31 | req.usuarioUuid = decoded.uuid 32 | req.usuarioId = decoded.id 33 | req.administrador = true 34 | 35 | next() 36 | }) 37 | 38 | module.exports = verifyAdmin 39 | -------------------------------------------------------------------------------- /server/src/login/verify_login.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { AppError, asyncHandler, httpCode } = require('../utils') 4 | 5 | const { db } = require('../database') 6 | 7 | const validateToken = require('./validate_token') 8 | 9 | // middleware para verificar o JWT 10 | const verifyLogin = asyncHandler(async (req, res, next) => { 11 | // verifica o header authorization para pegar o token 12 | const token = req.headers.authorization 13 | 14 | const decoded = await validateToken(token) 15 | 16 | if (!('id' in decoded && decoded.id && 'uuid' in decoded && decoded.uuid)) { 17 | throw new AppError('Falta informação de usuário') 18 | } 19 | 20 | if (req.params.usuario_uuid && decoded.uuid !== req.params.usuario_uuid) { 21 | throw new AppError( 22 | 'Usuário só pode acessar sua própria informação', 23 | httpCode.Unauthorized 24 | ) 25 | } 26 | const response = await db.sapConn.oneOrNone( 27 | 'SELECT ativo FROM dgeo.usuario WHERE uuid = $', 28 | { usuarioUuid: decoded.uuid } 29 | ) 30 | if (!response.ativo) { 31 | throw new AppError('Usuário não está ativo', httpCode.Forbidden) 32 | } 33 | 34 | req.usuarioUuid = decoded.uuid 35 | req.usuarioId = decoded.id 36 | 37 | next() 38 | }) 39 | 40 | module.exports = verifyLogin 41 | -------------------------------------------------------------------------------- /server/src/main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { errorHandler } = require('./utils') 4 | const { startServer } = require('./server') 5 | const { db, databaseVersion, microcontroleDatabaseVersion } = require('./database') 6 | const { verifyAuthServer } = require('./authentication') 7 | 8 | db.createSapConn() 9 | .then(db.createMicroConn) 10 | .then(databaseVersion.load) 11 | .then(microcontroleDatabaseVersion.load) 12 | .then(verifyAuthServer) 13 | .then(startServer) 14 | .catch(errorHandler.critical) 15 | -------------------------------------------------------------------------------- /server/src/metadados/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | metadadosRoute: require('./metadados_route') 5 | } 6 | -------------------------------------------------------------------------------- /server/src/microcontrole/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | microcontroleRoute: require('./microcontrole_route') 5 | } 6 | -------------------------------------------------------------------------------- /server/src/microcontrole/microcontrole_route.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const express = require('express') 4 | 5 | const { schemaValidation, asyncHandler, httpCode } = require('../utils') 6 | 7 | const { verifyLogin, verifyAdmin } = require('../login') 8 | 9 | const microcontroleCtrl = require('./microcontrole_ctrl') 10 | const microcontroleSchema = require('./microcontrole_schema') 11 | 12 | const router = express.Router() 13 | 14 | router.get( 15 | '/tipo_monitoramento', 16 | verifyAdmin, 17 | asyncHandler(async (req, res, next) => { 18 | const dados = await microcontroleCtrl.getTipoMonitoramento() 19 | 20 | const msg = 'Tipo de monitoramento retornados' 21 | 22 | return res.sendJsonAndLog(true, msg, httpCode.OK, dados) 23 | }) 24 | ) 25 | 26 | router.get( 27 | '/tipo_operacao', 28 | verifyAdmin, 29 | asyncHandler(async (req, res, next) => { 30 | const dados = await microcontroleCtrl.getTipoOperacao() 31 | 32 | const msg = 'Tipo de operação retornados' 33 | 34 | return res.sendJsonAndLog(true, msg, httpCode.OK, dados) 35 | }) 36 | ) 37 | 38 | router.post( 39 | '/feicao', 40 | verifyLogin, 41 | schemaValidation({ body: microcontroleSchema.feicao }), 42 | asyncHandler(async (req, res, next) => { 43 | await microcontroleCtrl.armazenaFeicao( 44 | req.body.atividade_id, 45 | req.usuarioId, 46 | req.body.dados 47 | ) 48 | 49 | const msg = 'Informações de produção de feição armazenadas com sucesso' 50 | 51 | return res.sendJsonAndLog(true, msg, httpCode.Created) 52 | }) 53 | ) 54 | 55 | router.post( 56 | '/tela', 57 | verifyLogin, 58 | schemaValidation({ body: microcontroleSchema.tela }), 59 | asyncHandler(async (req, res, next) => { 60 | await microcontroleCtrl.armazenaTela( 61 | req.body.atividade_id, 62 | req.usuarioId, 63 | req.body.dados 64 | ) 65 | 66 | const msg = 'Informações de tela armazenadas com sucesso' 67 | 68 | return res.sendJsonAndLog(true, msg, httpCode.Created) 69 | }) 70 | ) 71 | 72 | router.get( 73 | '/configuracao/perfil_monitoramento', 74 | verifyAdmin, 75 | asyncHandler(async (req, res, next) => { 76 | const dados = await microcontroleCtrl.getPerfilMonitoramento() 77 | 78 | const msg = 'Perfil monitoramento retornado com sucesso' 79 | 80 | return res.sendJsonAndLog(true, msg, httpCode.OK, dados) 81 | }) 82 | ) 83 | 84 | router.delete( 85 | '/configuracao/perfil_monitoramento', 86 | verifyAdmin, 87 | schemaValidation({ 88 | body: microcontroleSchema.perfilMonitoramentoOperadorIds 89 | }), 90 | asyncHandler(async (req, res, next) => { 91 | await microcontroleCtrl.deletePerfilMonitoramento(req.body.perfis_monitoramento_ids) 92 | 93 | const msg = 'Perfil monitoramento deletado com sucesso' 94 | 95 | return res.sendJsonAndLog(true, msg, httpCode.OK) 96 | }) 97 | ) 98 | 99 | router.post( 100 | '/configuracao/perfil_monitoramento', 101 | verifyAdmin, 102 | schemaValidation({ 103 | body: microcontroleSchema.perfilMonitoramento 104 | }), 105 | asyncHandler(async (req, res, next) => { 106 | await microcontroleCtrl.criaPerfilMonitoramento(req.body.perfis_monitoramento) 107 | 108 | const msg = 'Perfis monitoramento criados com sucesso' 109 | 110 | return res.sendJsonAndLog(true, msg, httpCode.Created) 111 | }) 112 | ) 113 | 114 | router.put( 115 | '/configuracao/perfil_monitoramento', 116 | verifyAdmin, 117 | schemaValidation({ 118 | body: microcontroleSchema.perfilMonitoramentoAtualizacao 119 | }), 120 | asyncHandler(async (req, res, next) => { 121 | await microcontroleCtrl.atualizaPerfilMonitoramento(req.body.perfis_monitoramento) 122 | 123 | const msg = 'Perfis monitoramento atualizados com sucesso' 124 | 125 | return res.sendJsonAndLog(true, msg, httpCode.OK) 126 | }) 127 | ) 128 | 129 | module.exports = router 130 | -------------------------------------------------------------------------------- /server/src/microcontrole/microcontrole_schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Joi = require('joi') 4 | 5 | const models = {} 6 | 7 | models.feicao = Joi.object().keys({ 8 | atividade_id: Joi.number() 9 | .integer() 10 | .strict() 11 | .required(), 12 | dados: Joi.array() 13 | .items( 14 | Joi.object().keys({ 15 | tipo_operacao_id: Joi.number() 16 | .integer() 17 | .strict() 18 | .required(), 19 | quantidade: Joi.number() 20 | .integer() 21 | .strict() 22 | .required(), 23 | comprimento: Joi.number() 24 | .strict() 25 | .when('operacao', { is: 1, then: Joi.required() }), 26 | vertices: Joi.number() 27 | .integer() 28 | .strict() 29 | .when('operacao', { is: 1, then: Joi.required() }), 30 | camada: Joi.string().required() 31 | }) 32 | ) 33 | .required() 34 | .min(1) 35 | }) 36 | 37 | models.tela = Joi.object().keys({ 38 | atividade_id: Joi.number() 39 | .integer() 40 | .strict() 41 | .required(), 42 | dados: Joi.array() 43 | .items( 44 | Joi.object().keys({ 45 | data: Joi.date().required(), 46 | x_min: Joi.number() 47 | .strict() 48 | .required(), 49 | x_max: Joi.number() 50 | .strict() 51 | .required(), 52 | y_min: Joi.number() 53 | .strict() 54 | .required(), 55 | y_max: Joi.number() 56 | .strict() 57 | .required(), 58 | zoom: Joi.number() 59 | .strict() 60 | .required() 61 | }) 62 | ) 63 | .required() 64 | .min(1) 65 | }) 66 | 67 | models.perfilMonitoramento = Joi.object().keys({ 68 | perfis_monitoramento: Joi.array() 69 | .items( 70 | Joi.object().keys({ 71 | subfase_id: Joi.number().integer().strict().required(), 72 | lote_id: Joi.number().integer().strict().required(), 73 | tipo_monitoramento_id: Joi.number().integer().strict().required() 74 | }) 75 | ) 76 | .required() 77 | .min(1) 78 | }) 79 | 80 | models.perfilMonitoramentoAtualizacao = Joi.object().keys({ 81 | perfis_monitoramento: Joi.array() 82 | .items( 83 | Joi.object().keys({ 84 | id: Joi.number().integer().strict().required(), 85 | subfase_id: Joi.number().integer().strict().required(), 86 | lote_id: Joi.number().integer().strict().required(), 87 | tipo_monitoramento_id: Joi.number().integer().strict().required() 88 | }) 89 | ) 90 | .required() 91 | .min(1) 92 | }) 93 | 94 | models.perfilMonitoramentoOperadorIds = Joi.object().keys({ 95 | perfis_monitoramento_ids: Joi.array() 96 | .items(Joi.number().integer().strict().required()) 97 | .unique() 98 | .required() 99 | .min(1) 100 | }) 101 | 102 | module.exports = models 103 | -------------------------------------------------------------------------------- /server/src/perigo/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | perigoRoute: require('./perigo_route') 5 | } 6 | -------------------------------------------------------------------------------- /server/src/producao/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | producaoCtrl: require('./producao_ctrl'), 5 | producaoRoute: require('./producao_route') 6 | } 7 | -------------------------------------------------------------------------------- /server/src/producao/prepared_statements.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const { sqlFile } = require('../database') 5 | 6 | /** 7 | * Returns the full path of a file 8 | * @param {string} p - file path 9 | * @returns {string} Full path of a file 10 | */ 11 | const fp = p => { 12 | return path.join(__dirname, 'sql', p) 13 | } 14 | 15 | module.exports = { 16 | calculaFilaPrioritaria: sqlFile.createPS(fp('calcula_fila_prioritaria.sql')), 17 | calculaFilaPrioritariaGrupo: sqlFile.createPS( 18 | fp('calcula_fila_prioritaria_grupo.sql') 19 | ), 20 | calculaFilaPausada: sqlFile.createPS(fp('calcula_fila_pausada.sql')), 21 | calculaFila: sqlFile.createPS(fp('calcula_fila.sql')), 22 | retornaDadosProducao: sqlFile.createPS(fp('retorna_dados_producao.sql')) 23 | } 24 | -------------------------------------------------------------------------------- /server/src/producao/sql/calcula_fila_pausada.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Verifica se existe alguma atividade disponível para o usuário na fila de atividades pausadas 3 | */ 4 | SELECT id 5 | FROM ( 6 | SELECT a.id, a.etapa_id, a.unidade_trabalho_id, e_ant.tipo_situacao_id AS situacao_ant, b.prioridade AS b_prioridade, ut.prioridade AS ut_prioridade 7 | FROM macrocontrole.atividade AS a 8 | INNER JOIN macrocontrole.etapa AS e ON e.id = a.etapa_id 9 | INNER JOIN macrocontrole.unidade_trabalho AS ut ON ut.id = a.unidade_trabalho_id 10 | INNER JOIN macrocontrole.bloco AS b ON b.id = ut.bloco_id 11 | LEFT JOIN 12 | ( 13 | SELECT a.tipo_situacao_id, a.unidade_trabalho_id, e.ordem, e.subfase_id FROM macrocontrole.atividade AS a 14 | INNER JOIN macrocontrole.etapa AS e ON e.id = a.etapa_id 15 | WHERE a.tipo_situacao_id in (1,2,3,4) 16 | ) 17 | AS e_ant ON e_ant.unidade_trabalho_id = a.unidade_trabalho_id AND e_ant.subfase_id = e.subfase_id 18 | AND e.ordem > e_ant.ordem 19 | WHERE ut.disponivel IS TRUE AND a.usuario_id = $1 AND a.tipo_situacao_id = 3 20 | AND a.id NOT IN 21 | ( 22 | SELECT a.id FROM macrocontrole.atividade AS a 23 | INNER JOIN macrocontrole.relacionamento_ut AS ut_sr ON ut_sr.ut_id = a.unidade_trabalho_id 24 | INNER JOIN macrocontrole.atividade AS a_re ON a_re.unidade_trabalho_id = ut_sr.ut_re_id 25 | WHERE 26 | ((a_re.tipo_situacao_id IN (1, 2, 3) AND ut_sr.tipo_pre_requisito_id = 1) OR (a_re.tipo_situacao_id IN (2) AND ut_sr.tipo_pre_requisito_id = 2)) 27 | ) 28 | ) AS sit 29 | GROUP BY id, b_prioridade, ut_prioridade 30 | HAVING MIN(situacao_ant) IS NULL OR every(situacao_ant IN (4)) 31 | ORDER BY b_prioridade, ut_prioridade 32 | LIMIT 1 -------------------------------------------------------------------------------- /server/src/producao/sql/calcula_fila_prioritaria.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Verifica se existe alguma atividade disponível para o usuário na fila prioritária 3 | */ 4 | SELECT id 5 | FROM ( 6 | SELECT a.id, a.etapa_id, a.unidade_trabalho_id, a_ant.tipo_situacao_id AS situacao_ant, fp.prioridade AS fp_prioridade 7 | FROM macrocontrole.atividade AS a 8 | INNER JOIN macrocontrole.etapa AS e ON e.id = a.etapa_id 9 | INNER JOIN macrocontrole.unidade_trabalho AS ut ON ut.id = a.unidade_trabalho_id 10 | INNER JOIN macrocontrole.bloco AS b ON b.id = ut.bloco_id 11 | INNER JOIN macrocontrole.fila_prioritaria AS fp ON fp.atividade_id = a.id 12 | LEFT JOIN 13 | ( 14 | SELECT a.tipo_situacao_id, a.unidade_trabalho_id, e.ordem, e.subfase_id FROM macrocontrole.atividade AS a 15 | INNER JOIN macrocontrole.etapa AS e ON e.id = a.etapa_id 16 | WHERE a.tipo_situacao_id in (1,2,3,4) 17 | ) 18 | AS a_ant ON a_ant.unidade_trabalho_id = a.unidade_trabalho_id AND a_ant.subfase_id = e.subfase_id 19 | AND e.ordem > a_ant.ordem 20 | WHERE ut.disponivel IS TRUE AND a.tipo_situacao_id = 1 AND fp.usuario_id = $1 21 | AND a.id NOT IN 22 | ( 23 | SELECT a.id FROM macrocontrole.atividade AS a 24 | INNER JOIN macrocontrole.relacionamento_ut AS ut_sr ON ut_sr.ut_id = a.unidade_trabalho_id 25 | INNER JOIN macrocontrole.atividade AS a_re ON a_re.unidade_trabalho_id = ut_sr.ut_re_id 26 | WHERE 27 | ((a_re.tipo_situacao_id IN (1, 2, 3) AND ut_sr.tipo_pre_requisito_id = 1) OR (a_re.tipo_situacao_id IN (2) AND ut_sr.tipo_pre_requisito_id = 2)) 28 | ) 29 | ) AS sit 30 | GROUP BY id, fp_prioridade 31 | HAVING MIN(situacao_ant) IS NULL OR every(situacao_ant IN (4)) 32 | ORDER BY fp_prioridade 33 | LIMIT 1 -------------------------------------------------------------------------------- /server/src/producao/sql/calcula_fila_prioritaria_grupo.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Verifica se existe alguma atividade disponível para o usuário na fila prioritária de grupo 3 | */ 4 | SELECT id 5 | FROM ( 6 | SELECT a.id, a.etapa_id, a.unidade_trabalho_id, a_ant.tipo_situacao_id AS situacao_ant, fpg.prioridade AS fpg_prioridade 7 | FROM macrocontrole.atividade AS a 8 | INNER JOIN macrocontrole.etapa AS e ON e.id = a.etapa_id 9 | INNER JOIN macrocontrole.unidade_trabalho AS ut ON ut.id = a.unidade_trabalho_id 10 | INNER JOIN macrocontrole.fila_prioritaria_grupo AS fpg ON fpg.atividade_id = a.id 11 | INNER JOIN macrocontrole.perfil_producao_operador AS ppo ON ppo.perfil_producao_id = fpg.perfil_producao_id 12 | INNER JOIN macrocontrole.perfil_bloco_operador AS pbloco ON pbloco.bloco_id = ut.bloco_id AND pbloco.usuario_id = ppo.usuario_id 13 | LEFT JOIN 14 | ( 15 | SELECT a.tipo_situacao_id, a.unidade_trabalho_id, e.ordem, e.subfase_id FROM macrocontrole.atividade AS a 16 | INNER JOIN macrocontrole.etapa AS e ON e.id = a.etapa_id 17 | WHERE a.tipo_situacao_id in (1,2,3,4) 18 | ) 19 | AS a_ant ON a_ant.unidade_trabalho_id = a.unidade_trabalho_id AND a_ant.subfase_id = e.subfase_id 20 | AND e.ordem > a_ant.ordem 21 | WHERE ut.disponivel IS TRUE AND ppo.usuario_id = $1 AND a.tipo_situacao_id = 1 AND fpg.perfil_producao_id = ppo.perfil_producao_id 22 | AND a.id NOT IN 23 | ( 24 | SELECT a.id FROM macrocontrole.atividade AS a 25 | INNER JOIN macrocontrole.relacionamento_ut AS ut_sr ON ut_sr.ut_id = a.unidade_trabalho_id 26 | INNER JOIN macrocontrole.atividade AS a_re ON a_re.unidade_trabalho_id = ut_sr.ut_re_id 27 | WHERE 28 | ((a_re.tipo_situacao_id IN (1, 2, 3) AND ut_sr.tipo_pre_requisito_id = 1) OR (a_re.tipo_situacao_id IN (2) AND ut_sr.tipo_pre_requisito_id = 2)) 29 | ) 30 | ) AS sit 31 | GROUP BY id, fpg_prioridade 32 | HAVING MIN(situacao_ant) IS NULL OR every(situacao_ant IN (4)) 33 | ORDER BY fpg_prioridade 34 | LIMIT 1 -------------------------------------------------------------------------------- /server/src/producao/sql/retorna_dados_producao.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Retorna oa dados de produção correspondentes a atividade 3 | */ 4 | SELECT a.unidade_trabalho_id, a.etapa_id, e.subfase_id, u.login, u.id as usuario_id, u.nome_guerra, s.id as subfase_id, s.nome as subfase_nome, ut.epsg, 5 | ST_ASEWKT(ST_Transform(ut.geom,ut.epsg::integer)) as unidade_trabalho_geom, ut.lote_id, l.nome AS lote, l.denominador_escala, s.fase_id, ut.dificuldade, ut.tempo_estimado_minutos, 6 | dp.configuracao_producao, ut.id AS ut_id, dp.tipo_dado_producao_id, p.nome AS projeto, b.nome AS bloco, tpro.nome AS tipo_produto, 7 | e.tipo_etapa_id, te.nome as etapa_nome, a.observacao as observacao_atividade, ut.observacao AS observacao_unidade_trabalho 8 | FROM macrocontrole.atividade as a 9 | INNER JOIN macrocontrole.etapa as e ON e.id = a.etapa_id 10 | INNER JOIN dominio.tipo_etapa as te ON te.code = e.tipo_etapa_id 11 | INNER JOIN macrocontrole.subfase as s ON s.id = e.subfase_id 12 | INNER JOIN macrocontrole.unidade_trabalho as ut ON ut.id = a.unidade_trabalho_id 13 | INNER JOIN macrocontrole.lote as l ON l.id = ut.lote_id 14 | INNER JOIN macrocontrole.bloco as b ON b.id = ut.bloco_id 15 | INNER JOIN macrocontrole.projeto as p ON p.id = l.projeto_id 16 | INNER JOIN macrocontrole.produto AS pro ON pro.lote_id = l.id 17 | INNER JOIN dominio.tipo_produto AS tpro ON tpro.code = pro.tipo_produto_id 18 | LEFT JOIN macrocontrole.dado_producao AS dp ON dp.id = ut.dado_producao_id 19 | LEFT JOIN dgeo.usuario AS u ON u.id = a.usuario_id 20 | WHERE a.id = $1 21 | LIMIT 1 22 | 23 | 24 | -------------------------------------------------------------------------------- /server/src/projeto/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | projetoRoute: require('./projeto_route') 5 | } 6 | -------------------------------------------------------------------------------- /server/src/rh/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | rhRoute: require('./rh_route') 5 | } 6 | -------------------------------------------------------------------------------- /server/src/rh/rh_route.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const express = require('express') 4 | 5 | const { schemaValidation, asyncHandler, httpCode } = require('../utils') 6 | 7 | const { verifyAdmin } = require('../login') 8 | 9 | const rhCtrl = require('./rh_ctrl') 10 | const rhSchema = require('./rh_schema') 11 | 12 | const router = express.Router() 13 | 14 | router.get( 15 | '/tipo_perda_rh', 16 | asyncHandler(async (req, res, next) => { 17 | const dados = await rhCtrl.getTipoPerdaHr() 18 | 19 | const msg = 'Tipo perda de rh retornadas' 20 | 21 | return res.sendJsonAndLog(true, msg, httpCode.OK, dados) 22 | }) 23 | ) 24 | 25 | router.get( 26 | '/dias_logados/usuario/:id', 27 | verifyAdmin, 28 | schemaValidation({ 29 | params: rhSchema.idParams 30 | }), 31 | asyncHandler(async (req, res, next) => { 32 | const dados = await rhCtrl.getDiasLogadosUsuario(req.params.id) 33 | 34 | const msg = 'Dias logados do usuario retornados' 35 | 36 | return res.sendJsonAndLog(true, msg, httpCode.OK, dados) 37 | }) 38 | ) 39 | 40 | router.get( 41 | '/atividades_por_periodo/:dataInicio/:dataFim', 42 | asyncHandler(async (req, res, next) => { 43 | const { dataInicio, dataFim} = req.params; 44 | const dados = await rhCtrl.getAtividadesPorPeriodo(dataInicio, dataFim); 45 | const msg = 'Atividades por período retornadas com sucesso'; 46 | return res.sendJsonAndLog(true, msg, httpCode.OK, dados); 47 | }) 48 | ); 49 | 50 | router.get( 51 | '/atividades_por_usuario_e_periodo/:usuarioId/:dataInicio/:dataFim', 52 | verifyAdmin, 53 | schemaValidation({ 54 | params: rhSchema.getAtividadesPorUsuarioEPeriodoParams 55 | }), 56 | asyncHandler(async (req, res, next) => { 57 | const { usuarioId, dataInicio, dataFim } = req.params; 58 | const dados = await rhCtrl.getAtividadesPorUsuarioEPeriodo(usuarioId, dataInicio, dataFim); 59 | const msg = 'Atividades por usuário e período retornadas com sucesso'; 60 | return res.sendJsonAndLog(true, msg, httpCode.OK, dados); 61 | }) 62 | ); 63 | 64 | router.get( 65 | '/lote_stats/:dataInicio/:dataFim', 66 | asyncHandler(async (req, res, next) => { 67 | const { dataInicio, dataFim } = req.params; 68 | const dados = await rhCtrl.getAllLoteStatsByDate(dataInicio, dataFim); 69 | const msg = 'Estatísticas de lote retornadas com sucesso'; 70 | return res.sendJsonAndLog(true, msg, httpCode.OK, dados); 71 | }) 72 | ); 73 | 74 | router.get( 75 | '/bloco_stats/:dataInicio/:dataFim', 76 | asyncHandler(async (req, res, next) => { 77 | const { dataInicio, dataFim } = req.params; 78 | const dados = await rhCtrl.getAllBlocksStatsByDate(dataInicio, dataFim); 79 | const msg = 'Estatísticas de bloco retornadas com sucesso'; 80 | return res.sendJsonAndLog(true, msg, httpCode.OK, dados); 81 | }) 82 | ) 83 | 84 | module.exports = router 85 | -------------------------------------------------------------------------------- /server/src/rh/rh_schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Joi = require('joi') 4 | 5 | const models = {} 6 | 7 | models.idParams = Joi.object().keys({ 8 | id: Joi.number().integer().required() 9 | }) 10 | 11 | models.getAtividadesPorPeriodoParams = Joi.object().keys({ 12 | dataInicio: Joi.date().required(), 13 | dataFim: Joi.date().required() 14 | }); 15 | 16 | models.getAtividadesPorUsuarioEPeriodoParams = Joi.object().keys({ 17 | usuarioId: Joi.number().integer().required(), 18 | dataInicio: Joi.date().required(), 19 | dataFim: Joi.date().required() 20 | }); 21 | 22 | module.exports = models 23 | -------------------------------------------------------------------------------- /server/src/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const express = require('express') 3 | 4 | const { databaseVersion, microcontroleDatabaseVersion } = require('./database') 5 | const { 6 | httpCode 7 | } = require('./utils') 8 | 9 | const { loginRoute } = require('./login') 10 | const { producaoRoute } = require('./producao') 11 | const { microcontroleRoute } = require('./microcontrole') 12 | const { gerenciaRoute } = require('./gerencia') 13 | const { projetoRoute } = require('./projeto') 14 | const { acompanhamentoRoute } = require('./acompanhamento') 15 | const { metadadosRoute } = require('./metadados') 16 | const { usuarioRoute } = require('./usuario') 17 | const { perigoRoute } = require('./perigo') 18 | const { rhRoute } = require('./rh') 19 | const { campoRoute } = require('./campo') 20 | 21 | const router = express.Router() 22 | 23 | /** 24 | * @swagger 25 | * /api: 26 | * get: 27 | * summary: Verifica o status operacional do sistema 28 | * description: Retorna a versão atual do banco de dados e a versão do microcontrole utilizados pelo sistema. 29 | * produces: 30 | * - application/json 31 | * tags: 32 | * - status 33 | * responses: 34 | * 200: 35 | * description: Status do sistema retornado com sucesso 36 | * content: 37 | * application/json: 38 | * schema: 39 | * type: object 40 | * properties: 41 | * success: 42 | * type: boolean 43 | * description: Indica se a requisição ocorreu com sucesso 44 | * message: 45 | * type: string 46 | * description: Descrição do status operacional do sistema 47 | * database_version: 48 | * type: string 49 | * description: Versão atual do banco de dados 50 | * microcontrole_version: 51 | * type: string 52 | * description: Versão atual do microcontrole 53 | */ 54 | router.get('/', (req, res, next) => { 55 | return res.sendJsonAndLog( 56 | true, 57 | 'Sistema de Apoio a produção operacional', 58 | httpCode.OK, 59 | { 60 | database_version: databaseVersion.nome, 61 | microcontrole_version: microcontroleDatabaseVersion.nome 62 | } 63 | ) 64 | }) 65 | 66 | router.use('/login', loginRoute) 67 | 68 | router.use('/distribuicao', producaoRoute) 69 | 70 | router.use('/microcontrole', microcontroleRoute) 71 | 72 | router.use('/gerencia', gerenciaRoute) 73 | 74 | router.use('/projeto', projetoRoute) 75 | 76 | router.use('/acompanhamento', acompanhamentoRoute) 77 | 78 | router.use('/metadados', metadadosRoute) 79 | 80 | router.use('/usuarios', usuarioRoute) 81 | 82 | router.use('/perigo', perigoRoute) 83 | 84 | router.use('/rh', rhRoute) 85 | 86 | router.use('/campo', campoRoute) 87 | 88 | module.exports = router 89 | -------------------------------------------------------------------------------- /server/src/server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | startServer: require('./start_server') 5 | } 6 | -------------------------------------------------------------------------------- /server/src/server/start_server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { databaseVersion, microcontroleDatabaseVersion } = require('../database') 4 | 5 | const app = require('./app') 6 | 7 | const { logger, AppError } = require('../utils') 8 | 9 | const { VERSION, PORT } = require('../config') 10 | 11 | const httpsConfig = () => { 12 | const fs = require('fs') 13 | const https = require('https') 14 | const path = require('path') 15 | 16 | const key = path.join(__dirname, 'sslcert/key.pem') 17 | const cert = path.join(__dirname, 'sslcert/cert.pem') 18 | 19 | if (!fs.existsSync(key) || !fs.existsSync(cert)) { 20 | throw new AppError( 21 | 'Para executar o SAP no modo HTTPS é necessário criar a chave e certificado com OpenSSL. Verifique a Wiki do SAP no Github para mais informações' 22 | ) 23 | } 24 | 25 | const httpsServer = https.createServer( 26 | { 27 | key: fs.readFileSync(key, 'utf8'), 28 | cert: fs.readFileSync(cert, 'utf8') 29 | }, 30 | app 31 | ) 32 | 33 | return httpsServer.listen(PORT, () => { 34 | logger.info('Servidor HTTPS do SAP iniciado', { 35 | success: true, 36 | information: { 37 | version: VERSION, 38 | database_version: databaseVersion.nome, 39 | microcontrole_database_version: microcontroleDatabaseVersion.nome, 40 | port: PORT 41 | } 42 | }) 43 | }) 44 | } 45 | 46 | const httpConfig = () => { 47 | return app.listen(PORT, () => { 48 | logger.info('Servidor HTTP do SAP iniciado', { 49 | success: true, 50 | information: { 51 | version: VERSION, 52 | database_version: databaseVersion.nome, 53 | microcontrole_database_version: microcontroleDatabaseVersion.nome, 54 | port: PORT 55 | } 56 | }) 57 | }) 58 | } 59 | 60 | const startServer = () => { 61 | const argv = require('minimist')(process.argv.slice(2)) 62 | if ('https' in argv && argv.https) { 63 | return httpsConfig() 64 | } 65 | 66 | return httpConfig() 67 | } 68 | 69 | module.exports = startServer 70 | -------------------------------------------------------------------------------- /server/src/server/swagger_options.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path'); 4 | 5 | const swaggerOptions = { 6 | swaggerDefinition: { 7 | openapi: '3.0.0', 8 | info: { 9 | title: 'Sistema de Apoio a Produção', 10 | version: '2.2.3', 11 | description: 'API HTTP para utilização do Sistema de Apoio a Produção' 12 | }, 13 | components: { 14 | securitySchemes: { 15 | bearerAuth: { 16 | type: 'http', 17 | scheme: 'bearer', 18 | bearerFormat: 'JWT', 19 | } 20 | } 21 | }, 22 | security: [{ 23 | bearerAuth: [] 24 | }] 25 | }, 26 | apis: [path.join(__dirname, '../**/*.js')], 27 | } 28 | 29 | 30 | module.exports = swaggerOptions 31 | -------------------------------------------------------------------------------- /server/src/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsgoficial/sap/d4a251bed651e687f5f39431551b2c0774b5a722/server/src/templates/.gitkeep -------------------------------------------------------------------------------- /server/src/usuario/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | usuarioRoute: require('./usuario_route') 5 | } 6 | -------------------------------------------------------------------------------- /server/src/usuario/usuario_ctrl.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { db } = require('../database') 4 | 5 | const { AppError, httpCode } = require('../utils') 6 | 7 | const { getUsuariosAuth } = require('../authentication') 8 | 9 | const controller = {} 10 | 11 | controller.getUsuarios = async () => { 12 | return db.sapConn.any(` 13 | SELECT u.id, u.uuid, u.login, u.nome, u.tipo_posto_grad_id, tpg.nome_abrev AS tipo_posto_grad, 14 | u.tipo_turno_id, tt.nome AS tipo_turno, u.nome_guerra, u.administrador, u.ativo 15 | FROM dgeo.usuario AS u 16 | INNER JOIN dominio.tipo_posto_grad AS tpg ON tpg.code = u.tipo_posto_grad_id 17 | INNER JOIN dominio.tipo_turno AS tt ON tt.code = u.tipo_turno_id 18 | `) 19 | } 20 | 21 | controller.atualizaUsuario = async (uuid, administrador, ativo) => { 22 | const result = await db.sapConn.result( 23 | 'UPDATE dgeo.usuario SET administrador = $, ativo = $ WHERE uuid = $', 24 | { 25 | uuid, 26 | administrador, 27 | ativo 28 | } 29 | ) 30 | 31 | if (!result.rowCount || result.rowCount !== 1) { 32 | throw new AppError('Usuário não encontrado', httpCode.BadRequest) 33 | } 34 | } 35 | 36 | controller.atualizaUsuarioLista = async usuarios => { 37 | const cs = new db.pgp.helpers.ColumnSet(['?uuid', 'ativo', 'administrador']) 38 | 39 | const query = 40 | db.pgp.helpers.update( 41 | usuarios, 42 | cs, 43 | { table: 'usuario', schema: 'dgeo' }, 44 | { 45 | tableAlias: 'X', 46 | valueAlias: 'Y' 47 | } 48 | ) + 'WHERE Y.uuid::uuid = X.uuid' 49 | 50 | return db.sapConn.none(query) 51 | } 52 | 53 | controller.getUsuariosAuthServer = async () => { 54 | const usuariosAuth = await getUsuariosAuth() 55 | 56 | const usuarios = await db.sapConn.any('SELECT u.uuid FROM dgeo.usuario AS u') 57 | 58 | return usuariosAuth.filter(u => { 59 | return usuarios.map(r => r.uuid).indexOf(u.uuid) === -1 60 | }) 61 | } 62 | 63 | controller.atualizaListaUsuarios = async () => { 64 | const usuariosAuth = await getUsuariosAuth() 65 | 66 | const cs = new db.pgp.helpers.ColumnSet([ 67 | '?uuid', 68 | 'login', 69 | 'nome', 70 | 'nome_guerra', 71 | 'tipo_posto_grad_id', 72 | 'tipo_turno_id' 73 | ]) 74 | 75 | const query = 76 | db.pgp.helpers.update( 77 | usuariosAuth, 78 | cs, 79 | { table: 'usuario', schema: 'dgeo' }, 80 | { 81 | tableAlias: 'X', 82 | valueAlias: 'Y' 83 | } 84 | ) + 'WHERE Y.uuid::uuid = X.uuid' 85 | 86 | return db.sapConn.none(query) 87 | } 88 | 89 | controller.criaListaUsuarios = async usuarios => { 90 | const usuariosAuth = await getUsuariosAuth() 91 | 92 | const usuariosFiltrados = usuariosAuth.filter(f => { 93 | return usuarios.indexOf(f.uuid) !== -1 94 | }) 95 | 96 | const cs = new db.pgp.helpers.ColumnSet([ 97 | 'uuid', 98 | 'login', 99 | 'nome', 100 | 'nome_guerra', 101 | 'tipo_posto_grad_id', 102 | 'tipo_turno_id', 103 | { name: 'ativo', init: () => true }, 104 | { name: 'administrador', init: () => false } 105 | ]) 106 | 107 | const query = db.pgp.helpers.insert(usuariosFiltrados, cs, { 108 | table: 'usuario', 109 | schema: 'dgeo' 110 | }) 111 | 112 | return db.sapConn.none(query) 113 | } 114 | 115 | module.exports = controller 116 | -------------------------------------------------------------------------------- /server/src/usuario/usuario_schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Joi = require('joi') 4 | 5 | const models = {} 6 | 7 | models.uuidParams = Joi.object().keys({ 8 | uuid: Joi.string().guid({ version: 'uuidv4' }).required() 9 | }) 10 | 11 | /** 12 | * @swagger 13 | * components: 14 | * schemas: 15 | * ListaUsuario: 16 | * type: object 17 | * required: 18 | * - usuarios 19 | * properties: 20 | * usuarios: 21 | * type: array 22 | * description: Lista de UUIDs dos usuários a serem criados 23 | * items: 24 | * type: string 25 | * format: uuid 26 | * uniqueItems: true 27 | * minItems: 1 28 | */ 29 | models.listaUsuario = Joi.object().keys({ 30 | usuarios: Joi.array() 31 | .items(Joi.string().guid({ version: 'uuidv4' }).required()) 32 | .unique() 33 | .required() 34 | .min(1) 35 | }) 36 | 37 | /** 38 | * @swagger 39 | * components: 40 | * schemas: 41 | * UpdateUsuario: 42 | * type: object 43 | * required: 44 | * - administrador 45 | * - ativo 46 | * properties: 47 | * administrador: 48 | * type: boolean 49 | * description: Indica se o usuário é administrador 50 | * ativo: 51 | * type: boolean 52 | * description: Indica se o usuário está ativo 53 | */ 54 | models.updateUsuario = Joi.object().keys({ 55 | administrador: Joi.boolean().strict().required(), 56 | ativo: Joi.boolean().strict().required() 57 | }) 58 | 59 | /** 60 | * @swagger 61 | * components: 62 | * schemas: 63 | * UpdateUsuarioLista: 64 | * type: object 65 | * required: 66 | * - usuarios 67 | * properties: 68 | * usuarios: 69 | * type: array 70 | * description: Lista de objetos contendo UUID e status dos usuários a serem atualizados 71 | * items: 72 | * type: object 73 | * properties: 74 | * uuid: 75 | * type: string 76 | * format: uuid 77 | * description: UUID do usuário 78 | * administrador: 79 | * type: boolean 80 | * description: Indica se o usuário é administrador 81 | * ativo: 82 | * type: boolean 83 | * description: Indica se o usuário está ativo 84 | * uniqueItems: true 85 | * minItems: 1 86 | */ 87 | models.updateUsuarioLista = Joi.object().keys({ 88 | usuarios: Joi.array() 89 | .items( 90 | Joi.object().keys({ 91 | uuid: Joi.string().guid({ version: 'uuidv4' }).required(), 92 | administrador: Joi.boolean().strict().required(), 93 | ativo: Joi.boolean().strict().required() 94 | }) 95 | ) 96 | .unique('uuid') 97 | .required() 98 | .min(1) 99 | }) 100 | 101 | module.exports = models 102 | -------------------------------------------------------------------------------- /server/src/utils/app_error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { serializeError } = require('serialize-error') 4 | 5 | const httpCode = require('./http_code') 6 | class AppError extends Error { 7 | constructor (message, status = httpCode.InternalError, errorTrace = null) { 8 | super(message) 9 | this.statusCode = status 10 | this.errorTrace = 11 | errorTrace instanceof Error ? serializeError(errorTrace) : errorTrace 12 | } 13 | } 14 | 15 | module.exports = AppError 16 | -------------------------------------------------------------------------------- /server/src/utils/async_handler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asyncHandler = fn => (req, res, next) => { 4 | return Promise.resolve(fn(req, res, next)).catch(next); 5 | }; 6 | 7 | module.exports = asyncHandler 8 | -------------------------------------------------------------------------------- /server/src/utils/async_handler_with_queue.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BetterQueue = require('better-queue'); 4 | 5 | const processingQueue = new BetterQueue((task, cb) => { 6 | task.fn(task.req, task.res, task.next) 7 | .then(() => cb(null, 'success')) 8 | .catch(cb); 9 | }); 10 | 11 | const enqueueTask = (fn, req, res, next) => { 12 | return new Promise((resolve, reject) => { 13 | processingQueue.push({fn, req, res, next}, (err, result) => { 14 | if (err) { 15 | reject(err); 16 | } else { 17 | resolve(result); 18 | } 19 | }); 20 | }); 21 | } 22 | 23 | const asyncHandlerWithQueue = fn => (req, res, next) => { 24 | return Promise.resolve(enqueueTask( fn, req, res, next)).catch(next); 25 | }; 26 | 27 | module.exports = asyncHandlerWithQueue 28 | -------------------------------------------------------------------------------- /server/src/utils/error_handler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { serializeError } = require('serialize-error') 4 | 5 | const logger = require('./logger') 6 | const httpCode = require('./http_code') 7 | 8 | const errorHandler = {} 9 | 10 | errorHandler.log = (err, res = null) => { 11 | const statusCode = err.statusCode || httpCode.InternalError 12 | const message = err.message || 'Erro no servidor' 13 | const errorTrace = err.errorTrace || serializeError(err) || null 14 | 15 | if (res && res.sendJsonAndLog) { 16 | return res.sendJsonAndLog(false, message, statusCode, null, errorTrace) 17 | } 18 | 19 | logger.error(message, { 20 | error: errorTrace, 21 | status: statusCode, 22 | success: false 23 | }) 24 | } 25 | 26 | errorHandler.critical = (err, res = null) => { 27 | errorHandler.log(err, res) 28 | process.exit(1) 29 | } 30 | 31 | module.exports = errorHandler 32 | -------------------------------------------------------------------------------- /server/src/utils/http_code.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const httpCode = { 4 | OK: 200, 5 | Created: 201, 6 | NoContent: 204, 7 | BadRequest: 400, 8 | Unauthorized: 401, 9 | Forbidden: 403, 10 | NotFound: 404, 11 | InternalError: 500 12 | } 13 | 14 | module.exports = httpCode 15 | -------------------------------------------------------------------------------- /server/src/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | logger: require('./logger'), 5 | sendJsonAndLogMiddleware: require('./send_json_and_log'), 6 | schemaValidation: require('./schema_validation'), 7 | asyncHandler: require('./async_handler'), 8 | asyncHandlerWithQueue: require('./async_handler_with_queue'), 9 | errorHandler: require('./error_handler'), 10 | AppError: require('./app_error'), 11 | httpCode: require('./http_code') 12 | } 13 | -------------------------------------------------------------------------------- /server/src/utils/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createLogger, format, transports } = require('winston') 4 | const DailyRotateFile = require('winston-daily-rotate-file') 5 | 6 | const fs = require('fs') 7 | const path = require('path') 8 | const logDir = path.join(__dirname, '..', '..', 'logs') 9 | 10 | if (!fs.existsSync(logDir)) { 11 | // Create the directory if it does not exist 12 | fs.mkdirSync(logDir) 13 | } 14 | 15 | const rotateTransport = new DailyRotateFile({ 16 | format: format.combine(format.timestamp(), format.json()), 17 | filename: path.join(logDir, '/%DATE%-application.log'), 18 | datePattern: 'YYYY-MM-DD', 19 | maxSize: '20m', 20 | maxFiles: '14d' 21 | }) 22 | 23 | const combinedTransport = new transports.File({ 24 | format: format.printf(info => { 25 | const date = new Date(Date.now()) 26 | return `${date}|${info.message}|${JSON.stringify(info)}` 27 | }), 28 | filename: path.join(logDir, 'combined.log') 29 | }) 30 | 31 | const consoleTransport = new transports.Console({ 32 | format: format.combine(format.colorize(), format.timestamp(), format.simple()) 33 | }) 34 | 35 | const logger = createLogger({ 36 | transports: [consoleTransport, rotateTransport, combinedTransport] 37 | }) 38 | 39 | module.exports = logger 40 | -------------------------------------------------------------------------------- /server/src/utils/schema_validation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AppError = require('./app_error') 4 | const httpCode = require('./http_code') 5 | 6 | const validationError = (error, context) => { 7 | const { details } = error 8 | const message = details.map(i => i.message).join(',') 9 | 10 | return new AppError( 11 | `Erro de validação dos ${context}. Mensagem de erro: ${message}`, 12 | httpCode.BadRequest, 13 | message 14 | ) 15 | } 16 | 17 | const middleware = ({ 18 | body: bodySchema, 19 | query: querySchema, 20 | params: paramsSchema 21 | }) => { 22 | return (req, res, next) => { 23 | if (querySchema) { 24 | const { error } = querySchema.validate(req.query, { 25 | abortEarly: false 26 | }) 27 | if (error) { 28 | return next(validationError(error, 'Query')) 29 | } 30 | } 31 | if (paramsSchema) { 32 | const { error } = paramsSchema.validate(req.params, { 33 | abortEarly: false 34 | }) 35 | if (error) { 36 | return next(validationError(error, 'Parâmetros')) 37 | } 38 | } 39 | if (bodySchema) { 40 | const { error } = bodySchema.validate(req.body, { 41 | stripUnknown: true, 42 | abortEarly: false 43 | }) 44 | if (error) { 45 | return next(validationError(error, 'Dados')) 46 | } 47 | } 48 | 49 | return next() 50 | } 51 | } 52 | 53 | module.exports = middleware 54 | -------------------------------------------------------------------------------- /server/src/utils/send_json_and_log.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('./logger') 4 | const { VERSION } = require('../config') 5 | 6 | const truncate = dados => { 7 | if ('senha' in dados) { 8 | dados.senha = '*' 9 | } 10 | 11 | const MAX_LENGTH = 500 12 | 13 | for (const key in dados) { 14 | if (Object.prototype.toString.call(dados[key]) === '[object String]') { 15 | if (dados[key].length > MAX_LENGTH) { 16 | dados[key] = dados[key].substring(0, MAX_LENGTH) 17 | } 18 | } 19 | } 20 | } 21 | const sendJsonAndLogMiddleware = (req, res, next) => { 22 | res.sendJsonAndLog = (success, message, status, dados = null, error = null, metadata = {}) => { 23 | const url = req.protocol + '://' + req.get('host') + req.originalUrl 24 | 25 | logger.info(message, { 26 | url, 27 | information: truncate(req.body), 28 | status, 29 | success, 30 | error 31 | }) 32 | 33 | const userMessage = status === 500 ? 'Erro no servidor' : message 34 | const jsonData = { 35 | version: VERSION, 36 | success: success, 37 | message: userMessage, 38 | dados, 39 | ...metadata 40 | } 41 | 42 | return res.status(status).json(jsonData) 43 | } 44 | 45 | next() 46 | } 47 | 48 | module.exports = sendJsonAndLogMiddleware 49 | -------------------------------------------------------------------------------- /version_update/update_222_223.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE SCHEMA controle_campo; 4 | 5 | CREATE TABLE controle_campo.situacao 6 | ( 7 | code SMALLINT NOT NULL PRIMARY KEY, 8 | nome VARCHAR(255) NOT NULL UNIQUE 9 | ); 10 | 11 | INSERT INTO controle_campo.situacao (code, nome) VALUES 12 | (1, 'Previsto'), 13 | (2, 'Em Execução'), 14 | (3, 'Finalizado'), 15 | (4, 'Cancelado'); 16 | 17 | CREATE TYPE controle_campo.categoria_campo AS ENUM ( 18 | 'Reambulação', 19 | 'Modelos 3D', 20 | 'Imagens Panorâmicas em 360º', 21 | 'Pontos de Controle', 22 | 'Capacitação em Geoinformação' 23 | 'Ortoimagens de Drone', 24 | ); 25 | 26 | CREATE TABLE controle_campo.campo 27 | ( 28 | id uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), 29 | nome VARCHAR(255) NOT NULL UNIQUE, 30 | descricao text, 31 | pit SMALLINT NOT NULL, 32 | orgao VARCHAR(255) NOT NULL, 33 | militares text, 34 | placas_vtr text, 35 | inicio timestamp with time zone, 36 | fim timestamp with time zone, 37 | categorias controle_campo.categoria_campo[] NOT NULL DEFAULT '{}', 38 | situacao_id SMALLINT NOT NULL REFERENCES controle_campo.situacao (code), 39 | geom geometry(MULTIPOLYGON, 4326) 40 | ); 41 | 42 | CREATE TABLE controle_campo.relacionamento_campo_produto 43 | ( 44 | id uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), 45 | campo_id uuid NOT NULL REFERENCES controle_campo.campo (id), 46 | produto_id SERIAL NOT NULL REFERENCES macrocontrole.produto (id), 47 | UNIQUE (campo_id, produto_id) 48 | ); 49 | 50 | CREATE TABLE controle_campo.imagem 51 | ( 52 | id uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), 53 | descricao text, 54 | data_imagem timestamp with time zone, 55 | imagem_bin bytea, 56 | campo_id uuid NOT NULL REFERENCES controle_campo.campo (id) 57 | ); 58 | 59 | CREATE TABLE controle_campo.track 60 | ( 61 | id uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), 62 | chefe_vtr VARCHAR(255) NOT NULL, 63 | motorista VARCHAR(255) NOT NULL, 64 | placa_vtr VARCHAR(255) NOT NULL, 65 | dia date NOT NULL, 66 | campo_id uuid NOT NULL REFERENCES controle_campo.campo (id) 67 | ); 68 | 69 | CREATE TABLE controle_campo.track_p 70 | ( 71 | id uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), 72 | track_id uuid NOT NULL REFERENCES controle_campo.track (id), 73 | x_ll real, 74 | y_ll real, 75 | track_id_garmin text, 76 | track_segment integer, 77 | track_segment_point_index integer, 78 | elevation real, 79 | creation_time timestamp with time zone, 80 | geom geometry(Point,4326), 81 | data_importacao timestamp(6) without time zone 82 | ); 83 | 84 | CREATE INDEX track_p_geom_idx ON controle_campo.track_p USING gist (geom); 85 | 86 | CREATE MATERIALIZED VIEW controle_campo.track_l 87 | AS 88 | SELECT row_number() OVER () AS id, 89 | p.track_id, 90 | p.track_id_garmin, 91 | min(p.creation_time) AS min_t, 92 | max(p.creation_time) AS max_t, 93 | st_makeline(st_setsrid(st_makepointm(st_x(p.geom), st_y(p.geom), date_part('epoch'::text, p.creation_time)), 4326) ORDER BY p.creation_time)::geometry(LineStringM, 4326) AS geom 94 | FROM controle_campo.track_p AS p 95 | GROUP BY p.track_id_garmin, p.track_id 96 | WITH DATA; 97 | 98 | CREATE INDEX track_l_geom_idx ON controle_campo.track_l USING gist (geom); 99 | 100 | UPDATE public.versao 101 | SET nome = '2.2.3' WHERE code = 1; 102 | 103 | COMMIT; --------------------------------------------------------------------------------