Adiciona arquivos de configuração e scripts para importação de dados do Sisu para o banco de dados
This commit is contained in:
parent
e6194438a7
commit
9f57ffa204
11 changed files with 722 additions and 229 deletions
2
.env.example
Normal file
2
.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Credenciais do seu banco de dados NeonDB
|
||||
DATABASE_URL="postgresql://usuario:senha@endereco_do_banco:porta/nome_do_banco"
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
csv/*
|
||||
!csv/.gitkeep
|
||||
.env
|
||||
node_modules/
|
||||
__pycache__/
|
||||
venv/
|
||||
75
README.md
Normal file
75
README.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# SisuFinderNode
|
||||
|
||||
SisuFinderNode é uma aplicação que permite buscar candidatos do Sisu por nome, utilizando dados públicos do MEC. O sistema faz scraping dos arquivos CSV, importa para um banco PostgreSQL e oferece uma interface web moderna para consulta.
|
||||
|
||||
## Funcionalidades
|
||||
- **Scraper:** Baixa arquivos CSV do Sisu a partir de um arquivo JSON com os IDs das instituições.
|
||||
- **Importação:** Junta e trata todos os CSVs, importando para uma tabela no PostgreSQL.
|
||||
- **API Express:** Endpoint `/buscar` para consultar candidatos por nome (busca simples ou múltipla).
|
||||
- **Frontend:** Interface web responsiva para busca e navegação paginada dos resultados.
|
||||
|
||||
## Estrutura dos arquivos
|
||||
- `scraper.py`: Baixa os arquivos CSV do Sisu usando os IDs do arquivo `arquivo.json`.
|
||||
- `importar_csv.py`: Lê todos os CSVs, trata os dados e importa para o banco PostgreSQL.
|
||||
- `index.js`: Backend Express que serve a API e o frontend.
|
||||
- `public/index.html`: Interface web para busca dos candidatos.
|
||||
- `arquivo.json`: Lista de objetos com o campo `co_ies` (IDs das instituições).
|
||||
- `csv/`: Pasta onde ficam os arquivos CSV baixados.
|
||||
|
||||
## Pré-requisitos
|
||||
- Node.js >= 16
|
||||
- Python >= 3.8
|
||||
- PostgreSQL (recomendado NeonDB)
|
||||
|
||||
## Instalação
|
||||
1. Clone o repositório:
|
||||
```bash
|
||||
git clone https://github.com/Revocable/SisuFinderNode.git
|
||||
cd SisuFinderNode
|
||||
```
|
||||
2. Instale as dependências Node.js:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
3. Instale as dependências Python:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
4. Configure o arquivo `.env`:
|
||||
```env
|
||||
DATABASE_URL=postgres://usuario:senha@host:porta/database
|
||||
```
|
||||
|
||||
## Como executar
|
||||
### 1. Baixar os arquivos CSV
|
||||
Coloque o arquivo `arquivo.json` na raiz do projeto, contendo os IDs das instituições no campo `co_ies`.
|
||||
Execute o scraper:
|
||||
```bash
|
||||
python scraper.py
|
||||
```
|
||||
|
||||
### 2. Importar os dados para o banco
|
||||
```bash
|
||||
python importar_csv.py
|
||||
```
|
||||
|
||||
### 3. Iniciar o servidor Express
|
||||
```bash
|
||||
node index.js
|
||||
```
|
||||
O servidor estará disponível em `http://localhost:8080`.
|
||||
|
||||
## Como utilizar
|
||||
- Acesse `http://localhost:8080` no navegador.
|
||||
- Digite o nome do candidato ou vários nomes separados por ponto e vírgula (`;`).
|
||||
- Clique em "Buscar" para ver os resultados paginados.
|
||||
- Use os botões de navegação para avançar entre as páginas.
|
||||
|
||||
## Observações
|
||||
- Os arquivos CSV não são versionados pelo Git (veja `.gitignore`).
|
||||
- O banco de dados deve estar acessível via URL definida no `.env`.
|
||||
- O scraper depende do formato da API pública do Sisu/MEC.
|
||||
|
||||
## Autor
|
||||
Desenvolvido por Murilo Scolari
|
||||
[GitHub](https://github.com/Revocable)
|
||||
0
csv/.gitkeep
Normal file
0
csv/.gitkeep
Normal file
73
importar_csv.py
Normal file
73
importar_csv.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import os
|
||||
import glob
|
||||
import pandas as pd
|
||||
from sqlalchemy import create_engine
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Carrega as variáveis do arquivo .env
|
||||
load_dotenv()
|
||||
|
||||
# Pega a URL de conexão do banco de dados do .env
|
||||
DATABASE_URL = os.getenv('DATABASE_URL')
|
||||
|
||||
# Define o nome da tabela que vamos criar no banco
|
||||
NOME_TABELA = 'candidatos_sisu'
|
||||
|
||||
# Path para a pasta com os arquivos CSV
|
||||
CAMINHO_CSVS = 'csv/*.csv'
|
||||
|
||||
def importar_dados():
|
||||
"""
|
||||
Lê todos os arquivos CSV de uma pasta, trata os dados e os envia
|
||||
para uma tabela no PostgreSQL.
|
||||
"""
|
||||
print("Iniciando a importação dos dados...")
|
||||
|
||||
try:
|
||||
# Cria a "engine" de conexão com o banco de dados
|
||||
engine = create_engine(DATABASE_URL)
|
||||
print("✅ Conexão com o NeonDB estabelecida!")
|
||||
|
||||
# Busca todos os arquivos que terminam com .csv na pasta especificada
|
||||
todos_os_arquivos = glob.glob(CAMINHO_CSVS)
|
||||
if not todos_os_arquivos:
|
||||
print("❌ Opa, não encontrei nenhum arquivo .csv na pasta 'csv/'.")
|
||||
return
|
||||
|
||||
print(f"Encontrei {len(todos_os_arquivos)} arquivos CSV para importar.")
|
||||
|
||||
# Junta todos os CSVs em um único DataFrame do Pandas
|
||||
lista_dfs = []
|
||||
for arquivo in todos_os_arquivos:
|
||||
print(f"Lendo o arquivo: {arquivo}...")
|
||||
# Lê o CSV usando ';' como separador
|
||||
df_temp = pd.read_csv(arquivo, sep=';')
|
||||
lista_dfs.append(df_temp)
|
||||
|
||||
df = pd.concat(lista_dfs, ignore_index=True)
|
||||
|
||||
# --- Tratamento dos dados ---
|
||||
# Deixa os nomes das colunas em minúsculo pra facilitar no SQL
|
||||
df.columns = df.columns.str.lower()
|
||||
|
||||
# Converte as notas (que estão com vírgula) para número
|
||||
df['nu_nota_candidato'] = pd.to_numeric(df['nu_nota_candidato'].str.replace(',', '.', regex=False), errors='coerce')
|
||||
df['nu_notacorte_concorrida'] = pd.to_numeric(df['nu_notacorte_concorrida'].str.replace(',', '.', regex=False), errors='coerce')
|
||||
|
||||
# Converte a coluna de bônus para número, tratando valores vazios
|
||||
df['qt_bonus_perc'] = pd.to_numeric(df['qt_bonus_perc'], errors='coerce')
|
||||
|
||||
print("Dados tratados. Enviando para o banco de dados...")
|
||||
|
||||
# Envia o DataFrame para a tabela no banco de dados
|
||||
# 'if_exists='replace'' apaga a tabela se ela já existir e cria de novo.
|
||||
# Se preferir, pode usar 'append' para só adicionar os novos dados.
|
||||
df.to_sql(NOME_TABELA, engine, if_exists='replace', index=False)
|
||||
|
||||
print(f"🚀 Sucesso! Todos os dados foram importados para a tabela '{NOME_TABELA}'.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Eita, deu um erro: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
importar_dados()
|
||||
168
index.js
168
index.js
|
|
@ -1,115 +1,81 @@
|
|||
// Carrega as variáveis de ambiente do arquivo .env
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const csv = require('csv-parse');
|
||||
const { Pool } = require('pg');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Enable CORS for all routes
|
||||
app.use(cors());
|
||||
|
||||
// Serve static files
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Global variable to store CSV data
|
||||
let globalCsvData = [];
|
||||
|
||||
// Function to load CSV data into memory - now with streaming for better performance
|
||||
async function loadCSV(csvFolder) {
|
||||
const dados = [];
|
||||
const files = fs.readdirSync(csvFolder).filter(file => file.endsWith('.csv'));
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(csvFolder, file);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const parser = csv.parse({
|
||||
delimiter: ';',
|
||||
skip_empty_lines: true,
|
||||
from_line: 2 // Skip header line
|
||||
});
|
||||
|
||||
fs.createReadStream(filePath, { encoding: 'utf-8' })
|
||||
.pipe(parser)
|
||||
.on('data', (line) => {
|
||||
if (line.length > 17) {
|
||||
dados.push({
|
||||
nota: line[16].trim(), // Atualizando para acessar "NU_NOTA_CANDIDATO" que é a coluna 17 (índice 16)
|
||||
nome_aluno: line[11].trim().toLowerCase(),
|
||||
instituicao: line[1].trim(),
|
||||
curso: line[6].trim(),
|
||||
no_campus: line[4].trim()
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('error', reject)
|
||||
.on('end', resolve);
|
||||
});
|
||||
}
|
||||
|
||||
return dados;
|
||||
}
|
||||
|
||||
// Optimized search function using Set for faster lookups
|
||||
function searchNamesInCSV(names, data) {
|
||||
const nameSet = new Set(names.map(name => name.trim().toLowerCase()));
|
||||
return data.filter(student =>
|
||||
Array.from(nameSet).some(name => student.nome_aluno.includes(name))
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize data at server startup
|
||||
async function initializeData() {
|
||||
try {
|
||||
console.log('Loading CSV data...');
|
||||
const startTime = Date.now();
|
||||
globalCsvData = await loadCSV("csv");
|
||||
const endTime = Date.now();
|
||||
console.log(`CSV data loaded successfully in ${endTime - startTime}ms. Total records: ${globalCsvData.length}`);
|
||||
} catch (error) {
|
||||
console.error('Error loading CSV data:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Root endpoint - serve index.html
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
// Search endpoint - now using pre-loaded data
|
||||
app.get('/buscar', (req, res) => {
|
||||
const names = req.query.nomes;
|
||||
|
||||
if (!names) {
|
||||
return res.status(400).json({
|
||||
error: "Por favor, forneça os nomes na URL, por exemplo: ?nomes=João;Maria"
|
||||
});
|
||||
app.get('/buscar', async (req, res) => {
|
||||
const { nomes = '', page = 1 } = req.query;
|
||||
|
||||
const limit = 50;
|
||||
const pageNumber = parseInt(page, 10);
|
||||
const offset = (pageNumber - 1) * limit;
|
||||
|
||||
try {
|
||||
let queryText;
|
||||
let countQueryText;
|
||||
let queryParams;
|
||||
|
||||
// Lógica inteligente: se tiver ';', faz busca múltipla. Senão, busca simples.
|
||||
if (nomes.includes(';')) {
|
||||
// Mapeia os nomes para o formato do ILIKE e cria um array
|
||||
const searchTerms = nomes.split(';').map(name => `%${name.trim()}%`);
|
||||
queryText = `
|
||||
SELECT no_inscrito, no_curso, no_ies, nu_nota_candidato
|
||||
FROM candidatos_sisu
|
||||
WHERE no_inscrito ILIKE ANY($1)
|
||||
ORDER BY no_inscrito
|
||||
LIMIT $2 OFFSET $3`;
|
||||
countQueryText = 'SELECT COUNT(*) FROM candidatos_sisu WHERE no_inscrito ILIKE ANY($1)';
|
||||
queryParams = [searchTerms, limit, offset];
|
||||
} else {
|
||||
const searchTerm = `%${nomes}%`;
|
||||
queryText = `
|
||||
SELECT no_inscrito, no_curso, no_ies, nu_nota_candidato
|
||||
FROM candidatos_sisu
|
||||
WHERE no_inscrito ILIKE $1
|
||||
ORDER BY no_inscrito
|
||||
LIMIT $2 OFFSET $3`;
|
||||
countQueryText = 'SELECT COUNT(*) FROM candidatos_sisu WHERE no_inscrito ILIKE $1';
|
||||
queryParams = [searchTerm, limit, offset];
|
||||
}
|
||||
|
||||
const resultsPromise = pool.query(queryText, queryParams);
|
||||
const countPromise = pool.query(countQueryText, [queryParams[0]]); // Usa só o primeiro param (termo de busca)
|
||||
|
||||
try {
|
||||
const nameList = names.split(';');
|
||||
const results = searchNamesInCSV(nameList, globalCsvData);
|
||||
|
||||
return res.json({ resultados: results });
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
return res.status(500).json({
|
||||
error: "Erro interno do servidor"
|
||||
});
|
||||
}
|
||||
});
|
||||
const [results, totalCountResult] = await Promise.all([resultsPromise, countPromise]);
|
||||
|
||||
// Start server after loading data
|
||||
const PORT = 8080;
|
||||
const totalResults = parseInt(totalCountResult.rows[0].count, 10);
|
||||
const totalPages = Math.ceil(totalResults / limit);
|
||||
|
||||
// Create public directory if it doesn't exist
|
||||
if (!fs.existsSync('public')) {
|
||||
fs.mkdirSync('public');
|
||||
}
|
||||
|
||||
initializeData().then(() => {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
res.json({
|
||||
data: results.rows,
|
||||
pagination: {
|
||||
totalResults,
|
||||
totalPages,
|
||||
currentPage: pageNumber
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro na busca no banco:', error);
|
||||
res.status(500).json({ message: 'Erro ao buscar os dados.' });
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = 8080;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Servidor rodando na porta ${PORT}!`);
|
||||
});
|
||||
145
node_modules/.package-lock.json
generated
vendored
145
node_modules/.package-lock.json
generated
vendored
|
|
@ -124,7 +124,6 @@
|
|||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
|
|
@ -179,6 +178,17 @@
|
|||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz",
|
||||
"integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
|
@ -257,7 +267,6 @@
|
|||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
|
|
@ -592,6 +601,122 @@
|
|||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.16.3",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
|
||||
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.9.1",
|
||||
"pg-pool": "^3.10.1",
|
||||
"pg-protocol": "^1.10.3",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
|
||||
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
|
||||
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
|
||||
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
|
||||
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
|
|
@ -802,6 +927,14 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
|
|
@ -859,6 +992,14 @@
|
|||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
149
package-lock.json
generated
149
package-lock.json
generated
|
|
@ -8,7 +8,9 @@
|
|||
"cors": "^2.8.5",
|
||||
"csv-parse": "^5.6.0",
|
||||
"csv-parser": "^3.1.0",
|
||||
"express": "^4.21.2"
|
||||
"dotenv": "^17.2.2",
|
||||
"express": "^4.21.2",
|
||||
"pg": "^8.16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
|
|
@ -132,7 +134,6 @@
|
|||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
|
|
@ -187,6 +188,17 @@
|
|||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz",
|
||||
"integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
|
@ -265,7 +277,6 @@
|
|||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
|
|
@ -600,6 +611,122 @@
|
|||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.16.3",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
|
||||
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.9.1",
|
||||
"pg-pool": "^3.10.1",
|
||||
"pg-protocol": "^1.10.3",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
|
||||
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
|
||||
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
|
||||
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
|
||||
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
|
|
@ -810,6 +937,14 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
|
|
@ -867,6 +1002,14 @@
|
|||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
"cors": "^2.8.5",
|
||||
"csv-parse": "^5.6.0",
|
||||
"csv-parser": "^3.1.0",
|
||||
"express": "^4.21.2"
|
||||
"dotenv": "^17.2.2",
|
||||
"express": "^4.21.2",
|
||||
"pg": "^8.16.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,114 +3,132 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Busca de Alunos</title>
|
||||
<title>SisuFinder</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #121212;
|
||||
color: #e0e0e0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
:root {
|
||||
--bg-color: #121212;
|
||||
--surface-color: rgba(30, 30, 30, 0.7);
|
||||
--primary-color: #00e676;
|
||||
--text-primary: #e0e0e0;
|
||||
--text-secondary: #a0a0a0;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background: linear-gradient(45deg, #1a237e, #0d47a1, #01579b, #006064);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientBG 15s ease infinite;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@keyframes gradientBG {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background-color: #1f1f1f;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
flex-grow: 1;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
.input-group {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px auto;
|
||||
padding: 30px;
|
||||
background: var(--surface-color);
|
||||
border-radius: 15px;
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
h1 { text-align: center; color: #ffffff; font-weight: 600; }
|
||||
|
||||
.input-group { margin-bottom: 20px; display: flex; gap: 10px; }
|
||||
|
||||
input[type="text"] {
|
||||
padding: 10px;
|
||||
width: 70%;
|
||||
border: 1px solid #555;
|
||||
border-radius: 5px;
|
||||
flex-grow: 1;
|
||||
padding: 12px 15px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
background-color: #333;
|
||||
color: #e0e0e0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
color: var(--text-primary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
button {
|
||||
padding: 10px 15px;
|
||||
margin-left: 10px;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 10px rgba(0, 230, 118, 0.3);
|
||||
}
|
||||
|
||||
#searchButton {
|
||||
background-color: var(--primary-color);
|
||||
color: #121212;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
padding: 10px 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.result {
|
||||
margin-top: 20px;
|
||||
#searchButton:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0, 230, 118, 0.4);
|
||||
}
|
||||
|
||||
.result-card {
|
||||
background-color: #333;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 10px;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.result-card h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
.result-card p {
|
||||
margin: 5px 0;
|
||||
color: #ddd;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
text-align: center;
|
||||
}
|
||||
.info {
|
||||
font-size: 14px;
|
||||
color: #bbb;
|
||||
text-align: center;
|
||||
}
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
color: #bbb;
|
||||
text-align: center;
|
||||
margin-top: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
.footer a {
|
||||
color: #4CAF50;
|
||||
text-decoration: none;
|
||||
.result-card:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.result-card h3 { margin: 0 0 10px 0; color: #fff; font-weight: 600; }
|
||||
.result-card p { margin: 5px 0; color: var(--text-secondary); }
|
||||
.result-card strong { color: var(--text-primary); }
|
||||
|
||||
.message { text-align: center; color: var(--text-secondary); padding: 20px; }
|
||||
|
||||
.pagination-controls { display: flex; justify-content: center; align-items: center; margin-top: 30px; gap: 15px; }
|
||||
.pagination-controls button { background: none; border: 1px solid var(--primary-color); color: var(--primary-color); border-radius: 8px; cursor: pointer; font-size: 16px; padding: 10px 20px; transition: all 0.3s ease; }
|
||||
.pagination-controls button:hover:not(:disabled) { background-color: var(--primary-color); color: var(--bg-color); }
|
||||
.pagination-controls button:disabled { border-color: #555; color: #555; cursor: not-allowed; }
|
||||
.pagination-info { font-size: 16px; color: var(--text-secondary); }
|
||||
|
||||
.footer { font-size: 14px; color: var(--text-secondary); text-align: center; margin-top: 40px; }
|
||||
.footer a { color: var(--primary-color); text-decoration: none; }
|
||||
.footer a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<h1>Busca de Alunos</h1>
|
||||
<h1>SisuFinder</h1>
|
||||
<div class="input-group">
|
||||
<input type="text" id="nome" placeholder="Digite o nome(s) para buscar" />
|
||||
<button onclick="buscarAluno()">Buscar</button>
|
||||
<input type="text" id="nomeInput" placeholder="Digite um nome ou vários separados por ;" />
|
||||
<button id="searchButton">Buscar</button>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<p><strong>Para buscar vários nomes, separe-os com ";".</strong></p>
|
||||
</div>
|
||||
<div id="message" class="message" style="display: none;"></div>
|
||||
<div id="result" class="result"></div>
|
||||
|
||||
<div class="result" id="result"></div>
|
||||
<div class="error" id="error"></div>
|
||||
<div id="pagination-controls" class="pagination-controls" style="display: none;">
|
||||
<button id="prevButton">Anterior</button>
|
||||
<span id="pagination-info" class="pagination-info"></span>
|
||||
<button id="nextButton">Próxima</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
|
|
@ -119,49 +137,112 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
function buscarAluno() {
|
||||
const nome = document.getElementById('nome').value.trim();
|
||||
const errorDiv = document.getElementById('error');
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
// Limpar resultados anteriores
|
||||
const nomeInput = document.getElementById('nomeInput');
|
||||
const searchButton = document.getElementById('searchButton');
|
||||
const resultDiv = document.getElementById('result');
|
||||
const messageDiv = document.getElementById('message');
|
||||
const paginationControls = document.getElementById('pagination-controls');
|
||||
const paginationInfo = document.getElementById('pagination-info');
|
||||
const prevButton = document.getElementById('prevButton');
|
||||
const nextButton = document.getElementById('nextButton');
|
||||
|
||||
let currentPage = 1;
|
||||
let totalPages = 0;
|
||||
let currentSearchTerm = '';
|
||||
let debounceTimeout;
|
||||
|
||||
async function fetchResults(searchTerm, page) {
|
||||
messageDiv.textContent = 'Buscando...';
|
||||
messageDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = '';
|
||||
errorDiv.innerHTML = '';
|
||||
paginationControls.style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/buscar?nomes=${encodeURIComponent(searchTerm)}&page=${page}`);
|
||||
if (!response.ok) throw new Error('Erro na resposta do servidor.');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
messageDiv.style.display = 'none';
|
||||
|
||||
if (!nome) {
|
||||
errorDiv.textContent = 'Por favor, digite um nome.';
|
||||
return;
|
||||
if (data.data && data.data.length > 0) {
|
||||
displayResults(data.data);
|
||||
updatePagination(data.pagination);
|
||||
} else {
|
||||
messageDiv.textContent = 'Nenhum candidato encontrado.';
|
||||
messageDiv.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
messageDiv.textContent = 'Ocorreu um erro ao buscar os dados.';
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
// Realiza a requisição GET
|
||||
fetch(`/buscar?nomes=${encodeURIComponent(nome)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
errorDiv.textContent = data.error;
|
||||
} else if (data.resultados.length === 0) {
|
||||
errorDiv.textContent = 'Nenhum aluno encontrado com esse nome.';
|
||||
} else {
|
||||
// Exibe os resultados
|
||||
data.resultados.forEach(item => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'result-card';
|
||||
card.innerHTML = `
|
||||
<h3>${item.nome_aluno}</h3>
|
||||
<p><strong>Nota:</strong> ${item.nota}</p>
|
||||
<p><strong>Instituição:</strong> ${item.instituicao}</p>
|
||||
<p><strong>Curso:</strong> ${item.curso}</p>
|
||||
<p><strong>Campus:</strong> ${item.no_campus}</p> <!-- Alterado para exibir "Campus" -->
|
||||
`;
|
||||
resultDiv.appendChild(card);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
errorDiv.textContent = 'Ocorreu um erro ao buscar os dados.';
|
||||
});
|
||||
}
|
||||
|
||||
function displayResults(results) {
|
||||
results.forEach(item => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'result-card';
|
||||
card.innerHTML = `
|
||||
<h3>${item.no_inscrito}</h3>
|
||||
<p><strong>Nota:</strong> ${item.nu_nota_candidato}</p>
|
||||
<p><strong>Instituição:</strong> ${item.no_ies}</p>
|
||||
<p><strong>Curso:</strong> ${item.no_curso}</p>
|
||||
`;
|
||||
resultDiv.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function updatePagination(pagination) {
|
||||
currentPage = pagination.currentPage;
|
||||
totalPages = pagination.totalPages;
|
||||
currentSearchTerm = nomeInput.value;
|
||||
|
||||
if (totalPages > 0) { // Mostra mesmo se for 1 página
|
||||
paginationControls.style.display = 'flex';
|
||||
paginationInfo.textContent = `Página ${currentPage} de ${totalPages}`;
|
||||
prevButton.disabled = currentPage <= 1;
|
||||
nextButton.disabled = currentPage >= totalPages;
|
||||
}
|
||||
}
|
||||
|
||||
nomeInput.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimeout);
|
||||
|
||||
// Só faz a busca em tempo real se NÃO tiver ponto e vírgula
|
||||
if (!nomeInput.value.includes(';')) {
|
||||
debounceTimeout = setTimeout(() => {
|
||||
const searchTerm = nomeInput.value.trim();
|
||||
if (searchTerm) {
|
||||
fetchResults(searchTerm, 1);
|
||||
} else {
|
||||
resultDiv.innerHTML = '';
|
||||
messageDiv.style.display = 'none';
|
||||
paginationControls.style.display = 'none';
|
||||
}
|
||||
}, 500); // Aumentei um pouco o tempo para melhor performance
|
||||
}
|
||||
});
|
||||
|
||||
// O botão de busca funciona para qualquer tipo de busca
|
||||
searchButton.addEventListener('click', () => {
|
||||
const searchTerm = nomeInput.value.trim();
|
||||
if (searchTerm) {
|
||||
fetchResults(searchTerm, 1);
|
||||
}
|
||||
});
|
||||
|
||||
prevButton.addEventListener('click', () => {
|
||||
if (currentPage > 1) {
|
||||
fetchResults(currentSearchTerm, currentPage - 1);
|
||||
}
|
||||
});
|
||||
|
||||
nextButton.addEventListener('click', () => {
|
||||
if (currentPage < totalPages) {
|
||||
fetchResults(currentSearchTerm, currentPage + 1);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pandas
|
||||
SQLAlchemy
|
||||
psycopg2-binary
|
||||
python-dotenv
|
||||
Loading…
Reference in a new issue