├── .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 |
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 |
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 | }
95 | size="small"
96 | sx={{
97 | position: 'absolute',
98 | left: 0,
99 | top: 10,
100 | zIndex: 10,
101 | borderTopLeftRadius: 0,
102 | borderBottomLeftRadius: 0,
103 | }}
104 | >
105 | Camadas
106 |
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 | }
82 | >
83 | Página inicial
84 |
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;
--------------------------------------------------------------------------------