feat: enhance UI and add help modal functionality

- Added a floating help button and modal for user guidance.
- Improved styling for result cards and added match score badges.
- Refactored CSS for better readability and organization.
- Updated HTML structure to accommodate new features and improve accessibility.
- Implemented JavaScript functions for modal management and collapsible sections.
- Marked 'pg' package as a peer dependency in package-lock files.
This commit is contained in:
Murilo Scolari 2025-10-14 04:08:12 -03:00
parent 90abc157cb
commit 4ab340fb5a
5 changed files with 944 additions and 131 deletions

View file

@ -4,18 +4,43 @@ 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'
NOME_TABELA = 'candidatos_sisu_enem'
# Path para a pasta com os arquivos CSV
CAMINHO_CSVS = 'csv/*.csv'
def corrigir_encoding(texto):
"""
Corrige problemas de encoding em textos que foram lidos incorretamente.
Converte de Latin-1 mal interpretado como UTF-8 de volta para UTF-8 correto.
"""
if pd.isna(texto):
return texto
try:
# Se o texto for string, tenta corrigir o encoding
if isinstance(texto, str):
# Tenta encodar como latin-1 e decodificar como utf-8
return texto.encode('latin-1').decode('utf-8')
except (UnicodeEncodeError, UnicodeDecodeError):
# Se der erro, retorna o texto original
return texto
return texto
def importar_dados():
"""
Lê todos os arquivos CSV de uma pasta, trata os dados e os envia
@ -23,51 +48,96 @@ def importar_dados():
"""
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=';')
# Tenta ler com diferentes encodings
try:
df_temp = pd.read_csv(arquivo, sep=';', low_memory=False, encoding='utf-8')
except UnicodeDecodeError:
try:
df_temp = pd.read_csv(arquivo, sep=';', low_memory=False, encoding='latin-1')
except:
df_temp = pd.read_csv(arquivo, sep=';', low_memory=False, encoding='iso-8859-1')
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("Corrigindo problemas de encoding nos textos...")
# Aplica a correção de encoding em todas as colunas de texto
colunas_texto = df.select_dtypes(include=['object']).columns
for coluna in colunas_texto:
df[coluna] = df[coluna].apply(corrigir_encoding)
# Converte as colunas numéricas que podem ter vírgula como separador decimal
colunas_numericas = [
'sisu_co_ies', 'sisu_co_ies_curso', 'sisu_qt_vagas_concorrencia',
'sisu_qt_bonus_perc', 'sisu_nu_nota_candidato', 'sisu_nu_notacorte_concorrida',
'sisu_nu_classificacao', 'enem_nu_sequencial', 'enem_nu_ano', 'enem_co_escola',
'enem_co_municipio_esc', 'enem_co_uf_esc', 'enem_tp_dependencia_adm_esc',
'enem_tp_localizacao_esc', 'enem_tp_sit_func_esc', 'enem_co_municipio_prova',
'enem_co_uf_prova', 'enem_tp_presenca_cn', 'enem_tp_presenca_ch',
'enem_tp_presenca_lc', 'enem_tp_presenca_mt', 'enem_co_prova_cn',
'enem_co_prova_ch', 'enem_co_prova_lc', 'enem_co_prova_mt',
'enem_nu_nota_cn', 'enem_nu_nota_ch', 'enem_nu_nota_lc', 'enem_nu_nota_mt',
'enem_tp_lingua', 'enem_tp_status_redacao', 'enem_nu_nota_comp1',
'enem_nu_nota_comp2', 'enem_nu_nota_comp3', 'enem_nu_nota_comp4',
'enem_nu_nota_comp5', 'enem_nu_nota_redacao', 'enem_media_enem',
'match_score'
]
for coluna in colunas_numericas:
if coluna in df.columns:
# Trata vírgulas como separador decimal
if df[coluna].dtype == 'object':
df[coluna] = pd.to_numeric(df[coluna].astype(str).str.replace(',', '.', regex=False), errors='coerce')
else:
df[coluna] = pd.to_numeric(df[coluna], errors='coerce')
print(f"Dados tratados e encoding corrigido. Total de {len(df)} registros. Enviando para o banco de dados...")
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)
df.to_sql(NOME_TABELA, engine, if_exists='replace', index=False, chunksize=1000)
print(f"🚀 Sucesso! Todos os dados foram importados para a tabela '{NOME_TABELA}'.")
print(f"📊 Total de registros inseridos: {len(df)}")
except Exception as e:
print(f"❌ Eita, deu um erro: {e}")
if __name__ == '__main__':
importar_dados()
importar_dados()

203
index.js
View file

@ -4,6 +4,7 @@ require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { Pool } = require('pg');
const TABLENAME = 'candidatos_sisu_enem';
const app = express();
app.use(cors());
@ -12,9 +13,6 @@ app.use(express.static('public'));
// Pool de conexões OTIMIZADO com configurações robustas
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: {
rejectUnauthorized: false
},
max: 20, // Máximo de conexões
min: 2, // Mínimo de conexões sempre ativas
idleTimeoutMillis: 30000, // 30 segundos
@ -87,7 +85,7 @@ function buildWordSearchConditions(searchTerm) {
if (words.length === 0) return null;
const conditions = words.map(() => `no_inscrito ILIKE ?`).join(' AND ');
const conditions = words.map(() => `sisu_no_inscrito ILIKE ?`).join(' AND ');
const params = words.map(word => `%${word}%`);
return { conditions, params, words };
@ -141,14 +139,22 @@ app.get('/buscar', async (req, res) => {
const pgCondition = finalCondition.replace(/\?/g, () => `$${paramIndex++}`);
queryText = `
SELECT no_inscrito, no_curso, no_ies, nu_nota_candidato
FROM candidatos_sisu
SELECT
sisu_no_inscrito, sisu_no_curso, sisu_no_ies, sisu_sg_uf_ies, sisu_no_campus,
sisu_ds_turno, sisu_ds_formacao, sisu_nu_nota_candidato, sisu_nu_notacorte_concorrida,
sisu_nu_classificacao, sisu_qt_vagas_concorrencia, sisu_no_modalidade_concorrencia,
enem_nu_nota_cn, enem_nu_nota_ch, enem_nu_nota_lc, enem_nu_nota_mt, enem_nu_nota_redacao,
enem_nu_nota_comp1, enem_nu_nota_comp2, enem_nu_nota_comp3, enem_nu_nota_comp4, enem_nu_nota_comp5,
enem_media_enem, match_score, enem_no_municipio_esc, enem_sg_uf_esc,
sisu_ensino_medio, sisu_quilombola, sisu_deficiente, sisu_tipo_concorrencia,
sisu_st_bonus_perc, sisu_qt_bonus_perc
FROM ${TABLENAME}
WHERE ${pgCondition}
ORDER BY no_inscrito
ORDER BY sisu_no_inscrito
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
countQueryText = `
SELECT COUNT(*) FROM candidatos_sisu
SELECT COUNT(*) FROM ${TABLENAME}
WHERE ${pgCondition}`;
queryParams = [...allParams, limit, offset];
@ -169,21 +175,29 @@ app.get('/buscar', async (req, res) => {
const searchTermWordStart = `% ${firstWord}%`;
queryText = `
SELECT no_inscrito, no_curso, no_ies, nu_nota_candidato
FROM candidatos_sisu
SELECT
sisu_no_inscrito, sisu_no_curso, sisu_no_ies, sisu_sg_uf_ies, sisu_no_campus,
sisu_ds_turno, sisu_ds_formacao, sisu_nu_nota_candidato, sisu_nu_notacorte_concorrida,
sisu_nu_classificacao, sisu_qt_vagas_concorrencia, sisu_no_modalidade_concorrencia,
enem_nu_nota_cn, enem_nu_nota_ch, enem_nu_nota_lc, enem_nu_nota_mt, enem_nu_nota_redacao,
enem_nu_nota_comp1, enem_nu_nota_comp2, enem_nu_nota_comp3, enem_nu_nota_comp4, enem_nu_nota_comp5,
enem_media_enem, match_score, enem_no_municipio_esc, enem_sg_uf_esc,
sisu_ensino_medio, sisu_quilombola, sisu_deficiente, sisu_tipo_concorrencia,
sisu_st_bonus_perc, sisu_qt_bonus_perc
FROM ${TABLENAME}
WHERE ${pgConditions}
ORDER BY
CASE
WHEN LOWER(no_inscrito) LIKE LOWER($${paramIndex}) THEN 1
WHEN LOWER(no_inscrito) LIKE LOWER($${paramIndex + 1}) THEN 2
WHEN LOWER(sisu_no_inscrito) LIKE LOWER($${paramIndex}) THEN 1
WHEN LOWER(sisu_no_inscrito) LIKE LOWER($${paramIndex + 1}) THEN 2
ELSE 3
END,
LENGTH(no_inscrito),
no_inscrito
LENGTH(sisu_no_inscrito),
sisu_no_inscrito
LIMIT $${paramIndex + 2} OFFSET $${paramIndex + 3}`;
countQueryText = `
SELECT COUNT(*) FROM candidatos_sisu
SELECT COUNT(*) FROM ${TABLENAME}
WHERE ${pgConditions}`;
queryParams = [...params, searchTermStart, searchTermWordStart, limit, offset];
@ -240,7 +254,7 @@ app.get('/filtros', async (req, res) => {
if (curso && curso.trim()) {
const normalizedCurso = normalizeText(curso.trim());
conditions.push(`
LOWER(TRANSLATE(no_curso,
LOWER(TRANSLATE(sisu_no_curso,
'áàâãäéèêëíìîïóòôõöúùûüçñÁÀÂÃÄÉÈÊËÍÌÎÏÓÒÔÕÖÚÙÛÜÇÑ',
'aaaaaeeeeiiiiooooouuuucnAAAAAEEEEIIIIOOOOOUUUUCN'
)) LIKE $${paramIndex}
@ -250,19 +264,19 @@ app.get('/filtros', async (req, res) => {
}
if (uf && uf.trim()) {
conditions.push(`sg_uf_ies = $${paramIndex}`);
conditions.push(`sisu_sg_uf_ies = $${paramIndex}`);
params.push(uf.trim().toUpperCase());
paramIndex++;
}
if (notaMin && notaMin.trim() && parseFloat(notaMin) > 0) {
conditions.push(`nu_nota_candidato >= $${paramIndex}`);
conditions.push(`sisu_nu_nota_candidato >= $${paramIndex}`);
params.push(parseFloat(notaMin));
paramIndex++;
}
if (turno && turno.trim()) {
conditions.push(`ds_turno ILIKE $${paramIndex}`);
conditions.push(`sisu_ds_turno ILIKE $${paramIndex}`);
params.push(`%${turno.trim()}%`);
paramIndex++;
}
@ -277,21 +291,21 @@ app.get('/filtros', async (req, res) => {
const queryText = `
SELECT
no_inscrito,
no_curso,
no_ies,
sg_uf_ies,
nu_nota_candidato,
nu_classificacao,
no_modalidade_concorrencia,
ds_turno
FROM candidatos_sisu
sisu_no_inscrito, sisu_no_curso, sisu_no_ies, sisu_sg_uf_ies, sisu_no_campus,
sisu_ds_turno, sisu_ds_formacao, sisu_nu_nota_candidato, sisu_nu_notacorte_concorrida,
sisu_nu_classificacao, sisu_qt_vagas_concorrencia, sisu_no_modalidade_concorrencia,
enem_nu_nota_cn, enem_nu_nota_ch, enem_nu_nota_lc, enem_nu_nota_mt, enem_nu_nota_redacao,
enem_nu_nota_comp1, enem_nu_nota_comp2, enem_nu_nota_comp3, enem_nu_nota_comp4, enem_nu_nota_comp5,
enem_media_enem, match_score, enem_no_municipio_esc, enem_sg_uf_esc,
sisu_ensino_medio, sisu_quilombola, sisu_deficiente, sisu_tipo_concorrencia,
sisu_st_bonus_perc, sisu_qt_bonus_perc
FROM ${TABLENAME}
WHERE ${whereClause}
ORDER BY nu_nota_candidato DESC
ORDER BY sisu_nu_nota_candidato DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
const countQuery = `
SELECT COUNT(*) FROM candidatos_sisu
SELECT COUNT(*) FROM ${TABLENAME}
WHERE ${whereClause}`;
params.push(limit, offset);
@ -324,15 +338,15 @@ app.get('/stats/cursos-concorridos', async (req, res) => {
try {
const query = `
SELECT
no_curso,
no_ies,
sg_uf_ies,
MAX(nu_notacorte_concorrida) as nota_corte,
sisu_no_curso,
sisu_no_ies,
sisu_sg_uf_ies,
MAX(sisu_nu_notacorte_concorrida) as nota_corte,
COUNT(*) as total_aprovados,
ROUND(AVG(nu_nota_candidato)::numeric, 2) as media_aprovados
FROM candidatos_sisu
WHERE nu_notacorte_concorrida > 0
GROUP BY no_curso, no_ies, sg_uf_ies
ROUND(AVG(sisu_nu_nota_candidato)::numeric, 2) as media_aprovados
FROM ${TABLENAME}
WHERE sisu_nu_notacorte_concorrida > 0
GROUP BY sisu_no_curso, sisu_no_ies, sisu_sg_uf_ies
ORDER BY nota_corte DESC
LIMIT 10`;
@ -349,16 +363,16 @@ app.get('/stats/universidades', async (req, res) => {
try {
const query = `
SELECT
no_ies,
sg_uf_ies,
ROUND(AVG(nu_nota_candidato)::numeric, 2) as media_notas,
sisu_no_ies,
sisu_sg_uf_ies,
ROUND(AVG(sisu_nu_nota_candidato)::numeric, 2) as media_notas,
COUNT(*) as total_aprovados,
MAX(nu_nota_candidato) as maior_nota,
MIN(nu_nota_candidato) as menor_nota,
COUNT(DISTINCT no_curso) as total_cursos
FROM candidatos_sisu
WHERE nu_nota_candidato IS NOT NULL
GROUP BY no_ies, sg_uf_ies
MAX(sisu_nu_nota_candidato) as maior_nota,
MIN(sisu_nu_nota_candidato) as menor_nota,
COUNT(DISTINCT sisu_no_curso) as total_cursos
FROM ${TABLENAME}
WHERE sisu_nu_nota_candidato IS NOT NULL
GROUP BY sisu_no_ies, sisu_sg_uf_ies
HAVING COUNT(*) >= 50
ORDER BY media_notas DESC
LIMIT 10`;
@ -376,16 +390,17 @@ app.get('/stats/maiores-notas', async (req, res) => {
try {
const query = `
SELECT
no_inscrito,
no_curso,
no_ies,
sg_uf_ies,
nu_nota_candidato,
nu_classificacao,
no_modalidade_concorrencia
FROM candidatos_sisu
WHERE nu_nota_candidato IS NOT NULL
ORDER BY nu_nota_candidato DESC
sisu_no_inscrito, sisu_no_curso, sisu_no_ies, sisu_sg_uf_ies, sisu_no_campus,
sisu_ds_turno, sisu_ds_formacao, sisu_nu_nota_candidato, sisu_nu_notacorte_concorrida,
sisu_nu_classificacao, sisu_qt_vagas_concorrencia, sisu_no_modalidade_concorrencia,
enem_nu_nota_cn, enem_nu_nota_ch, enem_nu_nota_lc, enem_nu_nota_mt, enem_nu_nota_redacao,
enem_nu_nota_comp1, enem_nu_nota_comp2, enem_nu_nota_comp3, enem_nu_nota_comp4, enem_nu_nota_comp5,
enem_media_enem, match_score, enem_no_municipio_esc, enem_sg_uf_esc,
sisu_ensino_medio, sisu_quilombola, sisu_deficiente, sisu_tipo_concorrencia,
sisu_st_bonus_perc, sisu_qt_bonus_perc
FROM ${TABLENAME}
WHERE sisu_nu_nota_candidato IS NOT NULL
ORDER BY sisu_nu_nota_candidato DESC
LIMIT 10`;
const result = await executeQueryWithRetry(query, []);
@ -396,6 +411,80 @@ app.get('/stats/maiores-notas', async (req, res) => {
}
});
// ============= ESTATÍSTICAS POR ESTADO =============
app.get('/stats/estado/:uf', async (req, res) => {
const { uf } = req.params;
try {
// Query 1: Dados gerais do estado
const queryGeral = `
SELECT
COUNT(*) as total_aprovados,
ROUND(AVG(sisu_nu_nota_candidato)::numeric, 2) as media_notas,
MAX(sisu_nu_nota_candidato) as maior_nota,
MIN(sisu_nu_nota_candidato) as menor_nota,
COUNT(DISTINCT sisu_no_ies) as total_universidades,
COUNT(DISTINCT sisu_no_curso) as total_cursos
FROM ${TABLENAME}
WHERE sisu_sg_uf_ies = $1 AND sisu_nu_nota_candidato IS NOT NULL`;
// Query 2: Top 3 cursos mais concorridos do estado
const queryTopCursos = `
SELECT
sisu_no_curso,
sisu_no_ies,
MAX(sisu_nu_notacorte_concorrida) as nota_corte,
COUNT(*) as total_aprovados
FROM ${TABLENAME}
WHERE sisu_sg_uf_ies = $1 AND sisu_nu_notacorte_concorrida > 0
GROUP BY sisu_no_curso, sisu_no_ies
ORDER BY nota_corte DESC
LIMIT 3`;
// Query 3: Distribuição por modalidade
const queryModalidades = `
SELECT
sisu_no_modalidade_concorrencia,
COUNT(*) as total
FROM ${TABLENAME}
WHERE sisu_sg_uf_ies = $1
GROUP BY sisu_no_modalidade_concorrencia
ORDER BY total DESC
LIMIT 5`;
// Query 4: Top 3 universidades do estado
const queryTopUniversidades = `
SELECT
sisu_no_ies,
COUNT(*) as total_aprovados,
ROUND(AVG(sisu_nu_nota_candidato)::numeric, 2) as media_notas
FROM ${TABLENAME}
WHERE sisu_sg_uf_ies = $1 AND sisu_nu_nota_candidato IS NOT NULL
GROUP BY sisu_no_ies
ORDER BY total_aprovados DESC
LIMIT 3`;
const [geral, topCursos, modalidades, topUniversidades] = await Promise.all([
executeQueryWithRetry(queryGeral, [uf.toUpperCase()]),
executeQueryWithRetry(queryTopCursos, [uf.toUpperCase()]),
executeQueryWithRetry(queryModalidades, [uf.toUpperCase()]),
executeQueryWithRetry(queryTopUniversidades, [uf.toUpperCase()])
]);
res.json({
estado: uf.toUpperCase(),
geral: geral.rows[0],
topCursos: topCursos.rows,
modalidades: modalidades.rows,
topUniversidades: topUniversidades.rows
});
} catch (error) {
console.error('❌ Erro ao buscar dados do estado:', error);
res.status(500).json({ message: 'Erro ao buscar dados do estado.' });
}
});
// ============= HEALTH CHECK =============
app.get('/health', async (req, res) => {
try {

1
node_modules/.package-lock.json generated vendored
View file

@ -822,6 +822,7 @@
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",

1
package-lock.json generated
View file

@ -834,6 +834,7 @@
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",

View file

@ -1,6 +1,9 @@
<!DOCTYPE html>
<html lang="pt-BR">
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -42,9 +45,17 @@
}
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.container {
@ -114,8 +125,15 @@
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Header com tooltip */
@ -193,7 +211,7 @@
opacity: 1;
}
/* Busca por nome */
/* Inputs */
.input-group {
margin-bottom: 20px;
display: flex;
@ -261,7 +279,7 @@
margin-bottom: 20px;
}
/* Cards de resultado */
/* Cards de resultado APRIMORADOS */
.result-card {
background-color: rgba(0, 0, 0, 0.2);
padding: 20px;
@ -276,19 +294,119 @@
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.result-card h3 {
margin: 0 0 10px 0;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid var(--primary-color);
}
.card-header h3 {
margin: 0;
color: #fff;
font-weight: 600;
}
.result-card p {
margin: 5px 0;
color: var(--text-secondary);
.match-badge {
padding: 5px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 700;
color: #fff;
}
.result-card strong {
color: var(--text-primary);
.info-section {
background: rgba(0, 0, 0, 0.15);
padding: 15px;
margin-bottom: 12px;
border-radius: 8px;
}
.info-section h4 {
color: var(--primary-color);
margin-bottom: 10px;
font-size: 1rem;
}
.notas-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 10px;
margin-top: 10px;
}
.nota-item {
background: rgba(0, 230, 118, 0.1);
padding: 10px;
border-radius: 6px;
text-align: center;
border: 1px solid rgba(0, 230, 118, 0.3);
}
.nota-label {
display: block;
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: 5px;
}
.nota-value {
display: block;
font-size: 1.2rem;
font-weight: 700;
color: var(--primary-color);
}
.competencias-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
margin-top: 10px;
}
.comp-item {
background: rgba(0, 0, 0, 0.2);
padding: 8px;
border-radius: 6px;
text-align: center;
font-size: 0.9rem;
}
.collapsible-header {
cursor: pointer;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s ease;
}
.collapsible-header:hover {
color: #fff;
}
.toggle-icon {
font-size: 0.8rem;
transition: transform 0.3s ease;
}
.collapsible-content {
margin-top: 10px;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Cards de estatísticas */
@ -323,6 +441,15 @@
font-size: 0.9rem;
}
.stats-card p {
margin: 5px 0;
color: var(--text-secondary);
}
.stats-card strong {
color: var(--text-primary);
}
.message {
text-align: center;
color: var(--text-secondary);
@ -433,10 +560,237 @@
width: 220px;
font-size: 12px;
}
.notas-grid {
grid-template-columns: repeat(2, 1fr);
}
.competencias-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* Botão de ajuda */
.help-button {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--primary-color);
color: #121212;
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
transition: all 0.3s ease;
cursor: pointer;
border: none;
font-size: 14px;
}
.help-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 230, 118, 0.4);
}
/* Modal de ajuda */
.help-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(5px);
z-index: 2000;
overflow-y: auto;
padding: 20px;
}
.help-modal.active {
display: flex;
justify-content: center;
align-items: flex-start;
animation: fadeIn 0.3s ease;
}
.help-modal-content {
background: var(--surface-color);
border: 2px solid var(--primary-color);
border-radius: 15px;
padding: 40px;
max-width: 900px;
width: 100%;
margin: 40px auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
animation: slideUp 0.3s ease;
}
.help-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 3px solid var(--primary-color);
}
.help-modal-header h2 {
color: var(--primary-color);
margin: 0;
font-size: 2rem;
}
.help-close {
background: none;
border: none;
color: var(--primary-color);
font-size: 35px;
cursor: pointer;
padding: 0;
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s ease;
}
.help-close:hover {
background: rgba(0, 230, 118, 0.2);
transform: rotate(90deg);
}
.help-section {
margin-bottom: 30px;
}
.help-section h3 {
color: var(--primary-color);
margin-bottom: 15px;
font-size: 1.3rem;
display: flex;
align-items: center;
gap: 10px;
}
.help-section p,
.help-section li {
color: var(--text-primary);
line-height: 1.8;
margin-bottom: 10px;
}
.help-section ul {
margin-left: 20px;
margin-top: 10px;
}
.help-section li {
margin-bottom: 8px;
}
.help-code {
background: rgba(0, 0, 0, 0.4);
padding: 15px;
border-radius: 8px;
border-left: 4px solid var(--primary-color);
font-family: 'Courier New', monospace;
font-size: 0.9rem;
overflow-x: auto;
margin: 15px 0;
}
.help-highlight {
background: rgba(0, 230, 118, 0.2);
padding: 2px 6px;
border-radius: 4px;
color: var(--primary-color);
font-weight: 600;
}
.help-link {
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
transition: all 0.3s ease;
}
.help-link:hover {
text-decoration: underline;
color: #00c853;
}
.weight-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.weight-table th,
.weight-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.weight-table th {
background: rgba(0, 230, 118, 0.1);
color: var(--primary-color);
font-weight: 600;
}
.weight-table tr:hover {
background: rgba(0, 230, 118, 0.05);
}
@media (max-width: 768px) {
.help-modal-content {
padding: 25px;
margin: 20px auto;
}
.help-modal-header h2 {
font-size: 1.5rem;
}
.help-section h3 {
font-size: 1.1rem;
}
}
.help-float {
position: fixed;
right: 30px;
bottom: 30px;
z-index: 3000;
background: var(--primary-color);
color: #121212;
border: none;
border-radius: 50%;
width: 56px;
height: 56px;
font-size: 2rem;
box-shadow: 0 4px 20px rgba(0, 230, 118, 0.3);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
justify-content: center;
align-items: center;
}
.help-float:hover {
background: #00c853;
color: #fff;
transform: scale(1.08);
}
</style>
</head>
<body>
<div class="container">
@ -577,6 +931,115 @@
<p>Desenvolvido por Murilo Scolari</p>
<p><a href="https://github.com/Revocable" target="_blank">GitHub</a></p>
</div>
<!-- Botão flutuante de ajuda -->
<button class="help-float" onclick="openHelpModal()" title="Como funciona?">❓</button>
<!-- Modal de Ajuda (igual estava no seu projeto, pode manter o HTML que você já usava) -->
<div id="help-modal" class="help-modal">
<div class="help-modal-content">
<div class="help-modal-header">
<h2>📚 Como Funciona o SisuData</h2>
<button class="help-close" onclick="closeHelpModal()">&times;</button>
</div>
<div class="help-section">
<h3>🔍 Origem dos Dados</h3>
<p>Todos os dados utilizados nesta plataforma são de <span class="help-highlight">origem pública</span>
e foram obtidos de fontes governamentais oficiais:</p>
<ul>
<li><strong>Dados do SISU:</strong> Eram disponibilizados publicamente até meados de 2025, quando o
governo atualizou os endpoints e alterou a forma de disponibilização. Os dados presentes nesta
plataforma são do último conjunto público disponível.</li>
<li><strong>Microdados do ENEM:</strong> Disponíveis gratuitamente através do INEP em <a
href="https://www.gov.br/inep/pt-br/acesso-a-informacao/dados-abertos/microdados/enem"
target="_blank" class="help-link">gov.br/inep/microdados/enem</a></li>
</ul>
</div>
<div class="help-section">
<h3>🔗 Metodologia de Matching</h3>
<p>O grande diferencial do SisuData é a <span class="help-highlight">combinação inteligente</span> entre
os dados do SISU (aprovados) e os Microdados do ENEM (notas detalhadas). Como esses datasets não
possuem um identificador comum, desenvolvi um algoritmo de matching probabilístico.</p>
</div>
<div class="help-section">
<h3>⚙️ Algoritmo de Matching</h3>
<p>O algoritmo calcula um <span class="help-highlight">score de precisão</span> (0-100%) para cada
candidato, considerando múltiplos critérios com pesos diferentes:</p>
<table class="weight-table">
<thead>
<tr>
<th>Critério</th>
<th>Peso</th>
<th>Descrição</th>
</tr>
</thead>
<tbody>
<tr>
<td>📊 Nota Final</td>
<td>35%</td>
<td>Diferença entre nota SISU e média ENEM (margem de ±5 pontos)</td>
</tr>
<tr>
<td>📍 Estado (UF)</td>
<td>20%</td>
<td>Candidato fez prova no mesmo estado da universidade</td>
</tr>
<tr>
<td>🏙️ Município</td>
<td>15%</td>
<td>Município onde realizou a prova</td>
</tr>
<tr>
<td>🏫 Tipo de Escola</td>
<td>15%</td>
<td>Dependência administrativa (pública/privada) compatível com a modalidade</td>
</tr>
<tr>
<td>🎓 Ensino Médio</td>
<td>10%</td>
<td>Conclusão do ensino médio em escola pública</td>
</tr>
<tr>
<td>♿ Deficiência</td>
<td>5%</td>
<td>Declaração de deficiência</td>
</tr>
</tbody>
</table>
<p style="margin-top: 15px;"><strong>Score Mínimo:</strong> Apenas matches com precisão <span
class="help-highlight">≥ 60%</span> são considerados válidos. Quanto maior o score, mais
confiável é a associação dos dados.</p>
</div>
<div class="help-section">
<h3>🎯 Interpretando o Badge de Precisão</h3>
<ul>
<li><span class="help-highlight"
style="background: linear-gradient(135deg, #00e676, #00c853); color: #fff; padding: 4px 10px; border-radius: 12px;">80-100%</span>
→ Alta confiabilidade nos dados do ENEM</li>
<li><span class="help-highlight"
style="background: linear-gradient(135deg, #ffd600, #ffab00); color: #121212; padding: 4px 10px; border-radius: 12px;">60-79%</span>
→ Confiabilidade moderada</li>
<li><span class="help-highlight"
style="background: linear-gradient(135deg, #ff6e40, #ff3d00); color: #fff; padding: 4px 10px; border-radius: 12px;">&lt;60%</span>
→ Dados do ENEM podem estar imprecisos</li>
</ul>
</div>
<div class="help-section">
<h3>⚠️ Limitações e Considerações</h3>
<ul>
<li>As <strong>notas do ENEM por área</strong> (CN, CH, LC, MT) são estimadas através do matching e
podem não ser 100% precisas para todos os candidatos.</li>
<li>Candidatos com score baixo podem ter dados do ENEM associados incorretamente.</li>
<li>A <strong>nota final do SISU</strong> é sempre precisa, pois vem diretamente da base oficial.
</li>
<li>Alguns candidatos não possuem match (não encontrados nos microdados do ENEM disponíveis).</li>
</ul>
</div>
<div class="help-section">
<h3>📧 Contato</h3>
<p>Dúvidas, sugestões ou reportar problemas? Entre em contato através do <a
href="https://github.com/Revocable" target="_blank" class="help-link">GitHub</a>.</p>
</div>
</div>
</div>
<script>
// ============= GERENCIAMENTO DE ABAS =============
@ -597,6 +1060,132 @@
if (tabName === 'notas') loadNotasStats();
}
// ============= FUNÇÕES AUXILIARES =============
function getMatchColor(score) {
if (!score) return '#666';
if (score >= 0.8) return 'linear-gradient(135deg, #00e676, #00c853)';
if (score >= 0.6) return 'linear-gradient(135deg, #ffd600, #ffab00)';
return 'linear-gradient(135deg, #ff6e40, #ff3d00)';
}
function getCotas(item) {
const cotas = [];
if (item.sisu_ensino_medio === 'S') cotas.push('Ensino Médio Público');
if (item.sisu_quilombola === 'S') cotas.push('Quilombola');
if (item.sisu_deficiente === 'S') cotas.push('PcD');
return cotas.length > 0 ? cotas.join(', ') : 'Não declarado';
}
function toggleSection(header) {
const content = header.nextElementSibling;
const icon = header.querySelector('.toggle-icon');
if (content.style.display === 'none') {
content.style.display = 'block';
icon.textContent = '▲';
} else {
content.style.display = 'none';
icon.textContent = '▼';
}
}
// ============= FUNÇÕES DE EXIBIÇÃO COM DADOS COMPLETOS =============
function displayResults(results, targetId) {
const resultDiv = document.getElementById(targetId);
results.forEach(item => {
const card = document.createElement('div');
card.className = 'result-card';
const matchScore = item.match_score ? (parseFloat(item.match_score) * 100).toFixed(1) : 'N/A';
const matchColor = getMatchColor(parseFloat(item.match_score));
card.innerHTML = `
<div class="card-header">
<h3>${item.sisu_no_inscrito}</h3>
<span class="match-badge" style="background: ${matchColor};">
${matchScore}% precisão
</span>
</div>
<div class="info-section">
<h4>📚 Curso e Instituição</h4>
<p><strong>Curso:</strong> ${item.sisu_no_curso}</p>
<p><strong>Instituição:</strong> ${item.sisu_no_ies} - ${item.sisu_sg_uf_ies}</p>
<p><strong>Campus:</strong> ${item.sisu_no_campus || 'N/A'}</p>
<p><strong>Turno:</strong> ${item.sisu_ds_turno} | <strong>Formação:</strong> ${item.sisu_ds_formacao}</p>
</div>
<div class="info-section">
<h4>📊 Classificação e Notas</h4>
<p><strong>Classificação:</strong> ${item.sisu_nu_classificacao}º lugar de ${item.sisu_qt_vagas_concorrencia} vagas</p>
<p><strong>Nota Final:</strong> ${parseFloat(item.sisu_nu_nota_candidato).toFixed(2)}</p>
<p><strong>Nota de Corte:</strong> ${parseFloat(item.sisu_nu_notacorte_concorrida).toFixed(2)}</p>
<p><strong>Modalidade:</strong> ${item.sisu_no_modalidade_concorrencia}</p>
</div>
<div class="info-section">
<h4>📝 Notas do ENEM ${matchScore !== 'N/A' ? `<span style="font-size: 0.8rem; color: var(--text-secondary);">(${matchScore}% precisão)</span>` : ''}</h4>
<div class="notas-grid">
<div class="nota-item">
<span class="nota-label">CN</span>
<span class="nota-value">${item.enem_nu_nota_cn ? parseFloat(item.enem_nu_nota_cn).toFixed(1) : 'N/A'}</span>
</div>
<div class="nota-item">
<span class="nota-label">CH</span>
<span class="nota-value">${item.enem_nu_nota_ch ? parseFloat(item.enem_nu_nota_ch).toFixed(1) : 'N/A'}</span>
</div>
<div class="nota-item">
<span class="nota-label">LC</span>
<span class="nota-value">${item.enem_nu_nota_lc ? parseFloat(item.enem_nu_nota_lc).toFixed(1) : 'N/A'}</span>
</div>
<div class="nota-item">
<span class="nota-label">MT</span>
<span class="nota-value">${item.enem_nu_nota_mt ? parseFloat(item.enem_nu_nota_mt).toFixed(1) : 'N/A'}</span>
</div>
<div class="nota-item">
<span class="nota-label">Redação</span>
<span class="nota-value">${item.enem_nu_nota_redacao ? parseFloat(item.enem_nu_nota_redacao).toFixed(0) : 'N/A'}</span>
</div>
</div>
${item.enem_media_enem ? `<p style="margin-top: 10px;"><strong>Média ENEM:</strong> ${parseFloat(item.enem_media_enem).toFixed(2)}</p>` : ''}
</div>
${item.enem_nu_nota_redacao ? `
<div class="info-section collapsible">
<h4 class="collapsible-header" onclick="toggleSection(this)">
✍️ Competências da Redação <span class="toggle-icon">▼</span>
</h4>
<div class="collapsible-content" style="display: none;">
<div class="competencias-grid">
<div class="comp-item"><strong>C1:</strong> ${item.enem_nu_nota_comp1 || 'N/A'}</div>
<div class="comp-item"><strong>C2:</strong> ${item.enem_nu_nota_comp2 || 'N/A'}</div>
<div class="comp-item"><strong>C3:</strong> ${item.enem_nu_nota_comp3 || 'N/A'}</div>
<div class="comp-item"><strong>C4:</strong> ${item.enem_nu_nota_comp4 || 'N/A'}</div>
<div class="comp-item"><strong>C5:</strong> ${item.enem_nu_nota_comp5 || 'N/A'}</div>
</div>
</div>
</div>
` : ''}
<div class="info-section collapsible">
<h4 class="collapsible-header" onclick="toggleSection(this)">
ℹ️ Informações Adicionais <span class="toggle-icon">▼</span>
</h4>
<div class="collapsible-content" style="display: none;">
<p><strong>Município da Escola:</strong> ${item.enem_no_municipio_esc || 'N/A'} - ${item.enem_sg_uf_esc || 'N/A'}</p>
${item.sisu_st_bonus_perc === 'SIM' ? `<p><strong>Bônus:</strong> ${item.sisu_qt_bonus_perc}%</p>` : ''}
<p><strong>Cotas:</strong> ${getCotas(item)}</p>
</div>
</div>
`;
resultDiv.appendChild(card);
});
}
function displayResultsFiltered(results, targetId) {
displayResults(results, targetId); // Usa a mesma função
}
// ============= BUSCA POR NOME =============
const nomeInput = document.getElementById('nomeInput');
const searchButton = document.getElementById('searchButton');
@ -747,8 +1336,8 @@
card.className = 'stats-card';
card.innerHTML = `
<span class="rank">#${index + 1}</span>
<h3>${item.no_curso}</h3>
<p><strong>Instituição:</strong> ${item.no_ies} - ${item.sg_uf_ies}</p>
<h3>${item.sisu_no_curso}</h3>
<p><strong>Instituição:</strong> ${item.sisu_no_ies} - ${item.sisu_sg_uf_ies}</p>
<p><strong>Nota de Corte:</strong> ${parseFloat(item.nota_corte).toFixed(2)}</p>
<p><strong>Total de Aprovados:</strong> ${item.total_aprovados}</p>
<p><strong>Média dos Aprovados:</strong> ${parseFloat(item.media_aprovados).toFixed(2)}</p>
@ -781,8 +1370,8 @@
card.className = 'stats-card';
card.innerHTML = `
<span class="rank">#${index + 1}</span>
<h3>${item.no_ies}</h3>
<p><strong>Estado:</strong> ${item.sg_uf_ies}</p>
<h3>${item.sisu_no_ies}</h3>
<p><strong>Estado:</strong> ${item.sisu_sg_uf_ies}</p>
<p><strong>Média das Notas:</strong> ${parseFloat(item.media_notas).toFixed(2)} ⭐</p>
<p><strong>Total de Aprovados:</strong> ${item.total_aprovados}</p>
<p><strong>Maior Nota:</strong> ${parseFloat(item.maior_nota).toFixed(2)}</p>
@ -814,16 +1403,93 @@
if (data.data && data.data.length > 0) {
data.data.forEach((item, index) => {
const card = document.createElement('div');
card.className = 'stats-card';
card.className = 'result-card';
const matchScore = item.match_score ? (parseFloat(item.match_score) * 100).toFixed(1) : 'N/A';
const matchColor = getMatchColor(parseFloat(item.match_score));
card.innerHTML = `
<span class="rank">#${index + 1}</span>
<h3>${item.no_inscrito}</h3>
<p><strong>Nota:</strong> ${parseFloat(item.nu_nota_candidato).toFixed(2)} 🏆</p>
<p><strong>Curso:</strong> ${item.no_curso}</p>
<p><strong>Instituição:</strong> ${item.no_ies} - ${item.sg_uf_ies}</p>
<p><strong>Classificação:</strong> ${item.nu_classificacao}º lugar</p>
<p><strong>Modalidade:</strong> ${item.no_modalidade_concorrencia}</p>
`;
<div class="card-header">
<h3>
<span class="rank" style="margin-right: 10px;">#${index + 1}</span>
${item.sisu_no_inscrito}
</h3>
<span class="match-badge" style="background: ${matchColor};">
${matchScore}% precisão
</span>
</div>
<div class="info-section">
<h4>📚 Curso e Instituição</h4>
<p><strong>Curso:</strong> ${item.sisu_no_curso}</p>
<p><strong>Instituição:</strong> ${item.sisu_no_ies} - ${item.sisu_sg_uf_ies}</p>
<p><strong>Campus:</strong> ${item.sisu_no_campus || 'N/A'}</p>
<p><strong>Turno:</strong> ${item.sisu_ds_turno} | <strong>Formação:</strong> ${item.sisu_ds_formacao}</p>
</div>
<div class="info-section">
<h4>📊 Classificação e Notas</h4>
<p><strong>Classificação:</strong> ${item.sisu_nu_classificacao}º lugar de ${item.sisu_qt_vagas_concorrencia} vagas</p>
<p><strong>Nota Final:</strong> ${parseFloat(item.sisu_nu_nota_candidato).toFixed(2)} 🏆</p>
<p><strong>Nota de Corte:</strong> ${parseFloat(item.sisu_nu_notacorte_concorrida).toFixed(2)}</p>
<p><strong>Modalidade:</strong> ${item.sisu_no_modalidade_concorrencia}</p>
</div>
<div class="info-section">
<h4>📝 Notas do ENEM ${matchScore !== 'N/A' ? `<span style="font-size: 0.8rem; color: var(--text-secondary);">(${matchScore}% precisão)</span>` : ''}</h4>
<div class="notas-grid">
<div class="nota-item">
<span class="nota-label">Ciências da Natureza</span>
<span class="nota-value">${item.enem_nu_nota_cn ? parseFloat(item.enem_nu_nota_cn).toFixed(1) : 'N/A'}</span>
</div>
<div class="nota-item">
<span class="nota-label">Ciências Humanas</span>
<span class="nota-value">${item.enem_nu_nota_ch ? parseFloat(item.enem_nu_nota_ch).toFixed(1) : 'N/A'}</span>
</div>
<div class="nota-item">
<span class="nota-label">Linguagens</span>
<span class="nota-value">${item.enem_nu_nota_lc ? parseFloat(item.enem_nu_nota_lc).toFixed(1) : 'N/A'}</span>
</div>
<div class="nota-item">
<span class="nota-label">Matemática</span>
<span class="nota-value">${item.enem_nu_nota_mt ? parseFloat(item.enem_nu_nota_mt).toFixed(1) : 'N/A'}</span>
</div>
<div class="nota-item">
<span class="nota-label">Redação</span>
<span class="nota-value">${item.enem_nu_nota_redacao ? parseFloat(item.enem_nu_nota_redacao).toFixed(0) : 'N/A'}</span>
</div>
</div>
${item.enem_media_enem ? `<p style="margin-top: 10px;"><strong>Média ENEM:</strong> ${parseFloat(item.enem_media_enem).toFixed(2)}</p>` : ''}
</div>
${item.enem_nu_nota_redacao ? `
<div class="info-section collapsible">
<h4 class="collapsible-header" onclick="toggleSection(this)">
✍️ Competências da Redação <span class="toggle-icon">▼</span>
</h4>
<div class="collapsible-content" style="display: none;">
<div class="competencias-grid">
<div class="comp-item"><strong>C1:</strong> ${item.enem_nu_nota_comp1 || 'N/A'}</div>
<div class="comp-item"><strong>C2:</strong> ${item.enem_nu_nota_comp2 || 'N/A'}</div>
<div class="comp-item"><strong>C3:</strong> ${item.enem_nu_nota_comp3 || 'N/A'}</div>
<div class="comp-item"><strong>C4:</strong> ${item.enem_nu_nota_comp4 || 'N/A'}</div>
<div class="comp-item"><strong>C5:</strong> ${item.enem_nu_nota_comp5 || 'N/A'}</div>
</div>
</div>
</div>
` : ''}
<div class="info-section collapsible">
<h4 class="collapsible-header" onclick="toggleSection(this)">
ℹ️ Informações Adicionais <span class="toggle-icon">▼</span>
</h4>
<div class="collapsible-content" style="display: none;">
<p><strong>Município da Escola:</strong> ${item.enem_no_municipio_esc || 'N/A'} - ${item.enem_sg_uf_esc || 'N/A'}</p>
${item.sisu_st_bonus_perc === 'SIM' ? `<p><strong>Bônus:</strong> ${item.sisu_qt_bonus_perc}%</p>` : ''}
<p><strong>Cotas:</strong> ${getCotas(item)}</p>
</div>
</div>
`;
resultDiv.appendChild(card);
});
}
@ -833,40 +1499,7 @@
}
}
// ============= FUNÇÕES AUXILIARES =============
function displayResults(results, targetId) {
const resultDiv = document.getElementById(targetId);
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 displayResultsFiltered(results, targetId) {
const resultDiv = document.getElementById(targetId);
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>Classificação:</strong> ${item.nu_classificacao}º lugar</p>
<p><strong>Instituição:</strong> ${item.no_ies} - ${item.sg_uf_ies}</p>
<p><strong>Curso:</strong> ${item.no_curso}</p>
<p><strong>Turno:</strong> ${item.ds_turno}</p>
<p><strong>Modalidade:</strong> ${item.no_modalidade_concorrencia}</p>
`;
resultDiv.appendChild(card);
});
}
// ============= PAGINAÇÃO =============
function updatePagination(pagination, type, searchTerm = '') {
const paginationDiv = document.getElementById(`pagination-${type}`);
const paginationInfo = document.getElementById(`pagination-info-${type}`);
@ -887,8 +1520,27 @@
nextButton.disabled = pagination.currentPage >= pagination.totalPages;
}
}
</script>
// ============= MODAL DE AJUDA =============
function openHelpModal() {
document.getElementById('help-modal').classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeHelpModal() {
document.getElementById('help-modal').classList.remove('active');
document.body.style.overflow = 'auto';
}
document.getElementById('help-modal').addEventListener('click', (e) => {
if (e.target.id === 'help-modal') {
closeHelpModal();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeHelpModal();
}
});
</script>
</body>
</html>
</html>