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:
parent
90abc157cb
commit
4ab340fb5a
5 changed files with 944 additions and 131 deletions
|
|
@ -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
203
index.js
|
|
@ -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
1
node_modules/.package-lock.json
generated
vendored
|
|
@ -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
1
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()">×</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;"><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>
|
||||
Loading…
Reference in a new issue