// ========================================== // CONFIGURACIÓN Y CONSTANTES // ========================================== const CONFIG = { API_BASE_URL: 'http://localhost:5000/api', FILE_PREFIX: 'candidatos' }; // ========================================== // ESTADO DE LA APLICACIÓN // ========================================== class AppState { constructor() { this.accessToken = ''; this.candidatesData = []; } setAccessToken(token) { this.accessToken = token; } getAccessToken() { return this.accessToken; } setCandidatesData(data) { this.candidatesData = data; } getCandidatesData() { return this.candidatesData; } hasToken() { return !!this.accessToken; } hasData() { return this.candidatesData.length > 0; } } // ========================================== // SERVICIOS API // ========================================== class ApiService { static async getAccessToken(clientId, clientSecret) { const formData = new URLSearchParams(); formData.append('grant_type', 'client_credentials'); formData.append('client_id', clientId); formData.append('client_secret', clientSecret); const response = await fetch(`${CONFIG.API_BASE_URL}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: formData }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Error en la autenticación'); } return await response.json(); } static async getCandidates(accessToken) { if (!accessToken) { throw new Error('No hay token de acceso disponible'); } const response = await fetch(`${CONFIG.API_BASE_URL}/candidatos/obtenerCandidatos`, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Error al obtener candidatos'); } return await response.json(); } } // ========================================== // UTILIDADES // ========================================== class Utils { static getCurrentDateTime() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); return `${year}${month}${day}_${hours}${minutes}`; } static flattenObject(obj, prefix = '') { const flattened = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { const newKey = prefix ? `${prefix}_${key}` : key; if (obj[key] !== null && typeof obj[key] === 'object' && !Array.isArray(obj[key])) { Object.assign(flattened, Utils.flattenObject(obj[key], newKey)); } else { flattened[newKey] = obj[key]; } } } return flattened; } static convertToCSV(data) { if (!data.length) return ''; const flatData = data.map(candidato => Utils.flattenObject(candidato)); const headers = Object.keys(flatData[0]); const csvHeaders = headers.join(','); const csvRows = flatData.map(row => headers.map(header => { const value = row[header]; if (typeof value === 'string' && (value.includes(',') || value.includes('\n') || value.includes('"'))) { return `"${value.replace(/"/g, '""')}"`; } return value || ''; }).join(',') ); return [csvHeaders, ...csvRows].join('\n'); } static downloadFile(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); URL.revokeObjectURL(link.href); } } // ========================================== // CONTROLADOR DE DESCARGA // ========================================== class DownloadController { constructor(appState) { this.appState = appState; } downloadJSON() { if (!this.appState.hasData()) { alert('No hay datos para descargar'); return; } const dataStr = JSON.stringify(this.appState.getCandidatesData(), null, 2); const filename = `${CONFIG.FILE_PREFIX}_${Utils.getCurrentDateTime()}.json`; Utils.downloadFile(dataStr, filename, 'application/json'); } downloadCSV() { if (!this.appState.hasData()) { alert('No hay datos para descargar'); return; } const csvData = Utils.convertToCSV(this.appState.getCandidatesData()); const filename = `${CONFIG.FILE_PREFIX}_${Utils.getCurrentDateTime()}.csv`; Utils.downloadFile(csvData, filename, 'text/csv;charset=utf-8;'); } downloadExcel() { if (!this.appState.hasData()) { alert('No hay datos para descargar'); return; } const flatData = this.appState.getCandidatesData().map(candidato => Utils.flattenObject(candidato)); const wb = XLSX.utils.book_new(); const ws = XLSX.utils.json_to_sheet(flatData); const cols = Object.keys(flatData[0]).map(() => ({ wch: 15 })); ws['!cols'] = cols; XLSX.utils.book_append_sheet(wb, ws, 'Candidatos'); const filename = `${CONFIG.FILE_PREFIX}_${Utils.getCurrentDateTime()}.xlsx`; XLSX.writeFile(wb, filename); } } // ========================================== // CONTROLADOR DE UI // ========================================== class UIController { constructor() { this.elements = { tokenType: document.getElementById('token-type'), expiresIn: document.getElementById('expires-in'), tokenSection: document.getElementById('token-section'), noTokenMessage: document.getElementById('no-token-message'), downloadSection: document.getElementById('download-section'), recordsCount: document.getElementById('records-count'), candidatesSection: document.getElementById('candidates-section'), candidatesTableBody: document.getElementById('candidates-table-body'), apiResponseSection: document.getElementById('api-response-section'), apiResponse: document.getElementById('api-response'), errorSection: document.getElementById('error-section'), errorMessage: document.getElementById('error-message'), noResultsMessage: document.getElementById('no-results-message') }; } displayTokenInfo(tokenData) { this.elements.tokenType.textContent = tokenData.token_type; this.elements.expiresIn.textContent = tokenData.expires_in; this.elements.tokenSection.classList.remove('hidden'); this.elements.noTokenMessage.classList.add('hidden'); console.log('Token obtenido y almacenado de forma segura'); } displayCandidates(candidates) { this.elements.apiResponse.textContent = JSON.stringify(candidates, null, 2); this.elements.apiResponseSection.classList.remove('hidden'); this.elements.downloadSection.classList.remove('hidden'); this.elements.recordsCount.textContent = candidates.length; this._fillCandidatesTable(candidates); this.elements.candidatesSection.classList.remove('hidden'); this.elements.noResultsMessage.classList.add('hidden'); } _fillCandidatesTable(candidates) { this.elements.candidatesTableBody.innerHTML = ''; candidates.forEach(candidato => { const dem = candidato.demograficos || {}; const ubi = dem.ubicacion || {}; const form = candidato.formacion || {}; const exam = candidato.examen || {}; const exp = candidato.experiencia_servicio || {}; const fechas = candidato.fechas || {}; const row = document.createElement('tr'); row.innerHTML = `