| 1 | /** |
| 2 | * Directorio de Semestre Empresarial. |
| 3 | * Contrato esperado de GET /empresarial: |
| 4 | * { empresas: [{ id, nombre, ciudad, estado, modalidad, calificacion, |
| 5 | * totalResenas, descripcion, convenioActivo, tecnologias, experienciaDestacada }] } |
| 6 | */ |
| 7 | document.addEventListener('DOMContentLoaded', async () => { |
| 8 | Layout.init('empresarial.html'); |
| 9 | Layout.setPageTitle('Semestre Empresarial'); |
| 10 | |
| 11 | const content = document.getElementById('main-content'); |
| 12 | if (!content) return; |
| 13 | |
| 14 | content.innerHTML = '<div class="empresarial-container"><p class="loading-message">Cargando empresas vinculadas…</p></div>'; |
| 15 | await loadCompanies(content); |
| 16 | }); |
| 17 | |
| 18 | async function loadCompanies(content, filters = {}) { |
| 19 | const params = new URLSearchParams(); |
| 20 | if (filters.busqueda) params.set('busqueda', filters.busqueda); |
| 21 | if (filters.modalidad) params.set('modalidad', filters.modalidad); |
| 22 | if (filters.calificacionMinima) params.set('calificacionMinima', filters.calificacionMinima); |
| 23 | |
| 24 | let response = {}; |
| 25 | try { |
| 26 | response = await API.request(`/empresarial${params.size ? `?${params}` : ''}`); |
| 27 | } catch { |
| 28 | // El controlador todavía no expone datos; se conserva el estado vacío. |
| 29 | } |
| 30 | |
| 31 | const companies = Array.isArray(response) ? response : (Array.isArray(response.empresas) ? response.empresas : []); |
| 32 | renderCompanies(content, companies); |
| 33 | } |
| 34 | |
| 35 | function renderCompanies(content, companies) { |
| 36 | content.innerHTML = ` |
| 37 | <div class="empresarial-container"> |
| 38 | <header class="module-heading"><h1>Semestre Empresarial</h1><p>Explora empresas vinculadas y experiencias compartidas por la comunidad.</p></header> |
| 39 | <form class="filter-bar" id="company-filter-form"> |
| 40 | <label class="visually-hidden" for="company-search">Buscar empresa</label> |
| 41 | <input class="input-search" id="company-search" name="busqueda" type="search" placeholder="Buscar por empresa, tecnología o proyecto..."> |
| 42 | <select class="select-filter" name="modalidad" aria-label="Filtrar por modalidad"><option value="">Todas las modalidades</option><option value="HIBRIDO">Híbrido</option><option value="REMOTO">Remoto</option><option value="PRESENCIAL">Presencial</option></select> |
| 43 | <select class="select-filter" name="calificacionMinima" aria-label="Filtrar por calificación"><option value="">Calificación: todas</option><option value="4.5">4.5 estrellas o más</option><option value="4.0">4.0 estrellas o más</option></select> |
| 44 | <button class="btn-solid" type="submit">Buscar</button> |
| 45 | </form> |
| 46 | <section id="company-results" aria-live="polite">${companies.length ? companies.map(renderCompany).join('') : renderEmptyCompanies()}</section> |
| 47 | </div> |
| 48 | `; |
| 49 | |
| 50 | document.getElementById('company-filter-form')?.addEventListener('submit', event => { |
| 51 | event.preventDefault(); |
| 52 | const formData = new FormData(event.currentTarget); |
| 53 | loadCompanies(content, Object.fromEntries(formData.entries())); |
| 54 | }); |
| 55 | } |
| 56 | |
| 57 | function renderCompany(company) { |
| 58 | const rating = Number(company.calificacion); |
| 59 | const hasRating = Number.isFinite(rating) && rating > 0; |
| 60 | const location = [company.ciudad, company.estado].filter(Boolean).join(', '); |
| 61 | const locationAndMode = [location, company.modalidad && `Modalidad: ${formatMode(company.modalidad)}`].filter(Boolean).join(' · '); |
| 62 | const technologies = Array.isArray(company.tecnologias) ? company.tecnologias : []; |
| 63 | const experience = company.experienciaDestacada; |
| 64 | |
| 65 | return ` |
| 66 | <article class="card company-card"> |
| 67 | <header class="company-header"> |
| 68 | <div><h2 class="company-name">${escapeHtml(company.nombre || 'Empresa')}</h2>${locationAndMode ? `<span class="company-location">${escapeHtml(locationAndMode)}</span>` : ''}</div> |
| 69 | ${hasRating ? `<div class="rating-box"><span class="stars" aria-label="${rating} de 5 estrellas">${renderStars(rating)}</span><strong class="rating-number">${rating.toFixed(1)}</strong>${Number.isFinite(Number(company.totalResenas)) ? `<span class="rating-count">(${Number(company.totalResenas)} reseñas)</span>` : ''}</div>` : ''} |
| 70 | </header> |
| 71 | ${company.descripcion ? `<p class="company-desc"><strong>¿Qué hacen?</strong> ${escapeHtml(company.descripcion)}</p>` : ''} |
| 72 | ${(company.convenioActivo || technologies.length) ? `<div class="tags-group">${company.convenioActivo ? '<span class="badge badge-green">Convenio oficial activo</span>' : ''}${technologies.map(item => `<span class="badge badge-blue">${escapeHtml(item)}</span>`).join('')}</div>` : ''} |
| 73 | ${experience ? renderExperience(experience) : ''} |
| 74 | <footer class="company-footer"><button class="btn-clean" type="button" data-company-id="${escapeHtml(company.id ?? '')}">Ver experiencias${Number.isFinite(Number(company.totalResenas)) ? ` (${Number(company.totalResenas)})` : ''}</button><button class="btn-solid" type="button" data-company-id="${escapeHtml(company.id ?? '')}">Ver requisitos de vinculación</button></footer> |
| 75 | </article> |
| 76 | `; |
| 77 | } |
| 78 | |
| 79 | function renderExperience(experience) { |
| 80 | const author = [experience.autor, experience.semestre && `${experience.semestre}.º semestre`].filter(Boolean).join(' · '); |
| 81 | return `<div class="experience-box">${author ? `<strong class="experience-author">${escapeHtml(author)}</strong>` : ''}${experience.texto ? `<p class="experience-text"><em>“${escapeHtml(experience.texto)}”</em></p>` : ''}</div>`; |
| 82 | } |
| 83 | |
| 84 | function renderEmptyCompanies() { |
| 85 | return '<section class="card empty-companies"><h2>Aún no hay empresas disponibles</h2><p>Las empresas vinculadas y las experiencias de estudiantes aparecerán aquí cuando se registren.</p></section>'; |
| 86 | } |
| 87 | |
| 88 | function renderStars(rating) { |
| 89 | const filled = Math.round(rating); |
| 90 | return '★'.repeat(Math.min(5, filled)) + '☆'.repeat(Math.max(0, 5 - filled)); |
| 91 | } |
| 92 | |
| 93 | function formatMode(mode) { |
| 94 | return String(mode).toLowerCase().replace(/^./, character => character.toUpperCase()); |
| 95 | } |
| 96 | |
| 97 | function escapeHtml(value) { |
| 98 | return String(value ?? '').replace(/[&<>'"]/g, character => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' })[character]); |
| 99 | } |