diff --git a/back/app/Http/Controllers/WebController.php b/back/app/Http/Controllers/WebController.php index 4a793b7..9b7ca21 100644 --- a/back/app/Http/Controllers/WebController.php +++ b/back/app/Http/Controllers/WebController.php @@ -1,70 +1,100 @@ with([ + 'detalles' => function ($query) { + $query->select( + 'id', + 'proceso_admision_id', + 'tipo', + 'titulo_detalle', + 'descripcion', + 'listas', + 'meta', + 'url', + 'imagen_path', + 'imagen_path_2', + 'created_at', + 'updated_at' + ); + } + ]) + ->latest() + ->get(); -public function GetProcesoAdmision() -{ - $procesos = ProcesoAdmision::select( - 'id', - 'titulo', - 'subtitulo', - 'descripcion', - 'slug', - 'tipo_proceso', - 'modalidad', - 'publicado', - 'fecha_publicacion', - 'fecha_inicio_preinscripcion', - 'fecha_fin_preinscripcion', - 'fecha_inicio_inscripcion', - 'fecha_fin_inscripcion', - 'fecha_examen1', - 'fecha_examen2', - 'fecha_resultados', - 'fecha_inicio_biometrico', - 'fecha_fin_biometrico', - 'imagen_path', - 'banner_path', - 'brochure_path', - 'link_preinscripcion', - 'link_inscripcion', - 'link_resultados', - 'link_reglamento', - 'estado', - 'created_at', - 'updated_at' - ) - ->with([ - 'detalles' => function ($query) { - $query->select( - 'id', - 'proceso_admision_id', - 'tipo', - 'titulo_detalle', - 'descripcion', - 'listas', - 'meta', - 'url', - 'imagen_path', - 'imagen_path_2', - 'created_at', - 'updated_at' - ); - } - ]) - ->latest() // 🔥 Esto ordena por created_at DESC - ->get(); + return response()->json([ + 'success' => true, + 'data' => $procesos + ]); + } - return response()->json([ - 'success' => true, - 'data' => $procesos - ]); -} + public function obtenerProcesosDisponiblesPreinscripcion(Request $request) + { + $now = Carbon::now(); + $postulante = $request->user(); + $procesos = ProcesoAdmision::query() + ->select([ + 'id', + 'titulo', + 'slug', + 'link_preinscripcion', + 'fecha_inicio_preinscripcion', + 'fecha_fin_preinscripcion', + ]) + ->where('publicado', 1) + ->whereIn('estado', ['publicado', 'en_proceso']) + ->whereNotNull('link_preinscripcion') + ->whereNotNull('fecha_inicio_preinscripcion') + ->whereNotNull('fecha_fin_preinscripcion') + ->where('fecha_inicio_preinscripcion', '<=', $now) + ->where('fecha_fin_preinscripcion', '>=', $now) + ->orderByDesc('fecha_inicio_preinscripcion') + ->orderBy('titulo') + ->get(); + return response()->json([ + 'success' => true, + 'data' => $procesos + ]); + } } diff --git a/back/routes/api.php b/back/routes/api.php index f9f913a..e789920 100644 --- a/back/routes/api.php +++ b/back/routes/api.php @@ -52,12 +52,9 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { Route::put('/areas/{id}', [AreaController::class, 'update']); Route::delete('/areas/{id}', [AreaController::class, 'destroy']); Route::patch('/areas/{id}/toggle', [AreaController::class, 'toggleEstado']); - Route::post('/areas/{area}/vincular-cursos', [AreaController::class, 'vincularCursosArea']); Route::post('/areas/{area}/desvincular-curso', [AreaController::class, 'desvincularCursoArea']); Route::get('/areas/{area}/cursos-disponibles', [AreaController::class, 'getCursosPorArea']); - - Route::post('areas/{area}/vincular-procesos', [AreaController::class, 'vincularProcesosArea']); Route::get('areas/{area}/procesos-disponibles', [AreaController::class, 'getProcesosPorArea'] ); Route::post('areas/{area}/desvincular-procesos', [AreaController::class, 'desvincularProcesoArea'] ); @@ -67,19 +64,14 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { - // NOTICIAS Route::get('/noticias', [NoticiaController::class, 'index']); Route::get('/noticias/{noticia}', [NoticiaController::class, 'show']); - Route::post('/noticias', [NoticiaController::class, 'store']); - - // usa SOLO UNA (PUT o PATCH). Aquí dejo PUT: Route::put('/noticias/{noticia}', [NoticiaController::class, 'update']); - Route::delete('/noticias/{noticia}', [NoticiaController::class, 'destroy']); }); -Route::get('/noticias', [NoticiaController::class, 'index']); -Route::get('/noticias/{noticia:slug}', [NoticiaController::class, 'showPublic']); + + Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { @@ -99,8 +91,6 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { }); - - Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { Route::get('cursos/{cursoId}/preguntas', [PreguntaController::class, 'getPreguntasCurso']); @@ -113,7 +103,6 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { Route::middleware('auth:sanctum')->group(function () { - Route::get('/calificaciones', [CalificacionController::class, 'index']); Route::post('/calificaciones', [CalificacionController::class, 'store']); Route::get('/calificaciones/{id}', [CalificacionController::class, 'show']); @@ -122,17 +111,11 @@ Route::middleware('auth:sanctum')->group(function () { }); - - Route::prefix('postulante')->group(function () { - // Registro Route::post('/register', [PostulanteAuthController::class, 'register']); - - // Login Route::post('/login', [PostulanteAuthController::class, 'login']); - // Rutas protegidas por token Route::middleware('auth:sanctum')->group(function () { Route::post('/logout', [PostulanteAuthController::class, 'logout']); Route::get('/me', [PostulanteAuthController::class, 'me']); @@ -142,10 +125,8 @@ Route::prefix('postulante')->group(function () { }); - }); - // Route::middleware('auth:sanctum')->group(function () { // Route::get('/procesos', [ExamenController::class, 'procesoexamen']); // Route::get('/areas', [ExamenController::class, 'areas']); @@ -157,51 +138,35 @@ Route::prefix('postulante')->group(function () { Route::middleware(['auth:sanctum'])->prefix('area-proceso')->group(function () { Route::get('areasprocesos', [ReglaAreaProcesoController::class, 'areasProcesos']); Route::prefix('{areaProcesoId}/reglas')->group(function () { - Route::get('/', [ReglaAreaProcesoController::class, 'index']); // Listar reglas - Route::post('/', [ReglaAreaProcesoController::class, 'store']); // Crear/actualizar regla individual - Route::post('/multiple', [ReglaAreaProcesoController::class, 'storeMultiple']); // Guardar múltiples reglas + Route::get('/', [ReglaAreaProcesoController::class, 'index']); + Route::post('/', [ReglaAreaProcesoController::class, 'store']); + Route::post('/multiple', [ReglaAreaProcesoController::class, 'storeMultiple']); }); }); Route::middleware(['auth:sanctum'])->prefix('reglas')->group(function () { - Route::put('/{reglaId}', [ReglaAreaProcesoController::class, 'update']); // Editar regla - Route::delete('/{reglaId}', [ReglaAreaProcesoController::class, 'destroy']); // Eliminar regla + Route::put('/{reglaId}', [ReglaAreaProcesoController::class, 'update']); + Route::delete('/{reglaId}', [ReglaAreaProcesoController::class, 'destroy']); }); - -// Examen - Flujo separado -Route::middleware(['auth:postulante'])->group(function () { +Route::middleware(['auth:sanctum'])->group(function () { Route::get('/examen/procesos', [ExamenController::class, 'procesoexamen']); Route::get('/examen/areas', [ExamenController::class, 'areas']); Route::get('/examen/actual', [ExamenController::class, 'miExamenActual']); - - // Crear examen (sin preguntas) Route::post('/examen/crear', [ExamenController::class, 'crearExamen']); - - // Generar preguntas Route::post('/examen/{examen}/generar-preguntas', [ExamenController::class, 'generarPreguntas']); - - // Obtener preguntas Route::get('/examen/{examen}/preguntas', [ExamenController::class, 'obtenerPreguntas']); - - // Iniciar examen (marcar hora inicio) Route::post('/examen/iniciar', [ExamenController::class, 'iniciarExamen']); - - // Responder preguntas Route::post('/examen/pregunta/{pregunta}/responder', [ExamenController::class, 'responderPregunta']); - - // Finalizar examen Route::post('/examen/{examen}/finalizar', [ExamenController::class, 'finalizarExamen']); Route::post('/examen/{examenId}/calificar', [ExamenController::class, 'calificarExamen']); }); +Route::middleware('auth:sanctum')->prefix('admin')->group(function () { - Route::middleware('auth:sanctum')->prefix('admin')->group(function () { - - // PROCESOS Route::prefix('procesos-admision')->group(function () { Route::get('/', [ProcesoAdmisionController::class, 'index'])->name('procesos-admision.index'); Route::post('/', [ProcesoAdmisionController::class, 'store'])->name('procesos-admision.store'); @@ -219,8 +184,16 @@ Route::middleware(['auth:postulante'])->group(function () { Route::delete('/{id}', [ProcesoAdmisionDetalleController::class, 'destroy'])->name('detalles-admision.destroy'); }); - }); +}); + + Route::middleware('auth:sanctum')->group(function () { + + Route::get('/procesos-disponibles-preinscripcion', [WebController::class, 'obtenerProcesosDisponiblesPreinscripcion']); +}); + Route::get('/procesos-admision', [WebController::class, 'GetProcesoAdmision']); +Route::get('/noticias', [NoticiaController::class, 'index']); +Route::get('/noticias/{noticia:slug}', [NoticiaController::class, 'showPublic']); \ No newline at end of file diff --git a/front/src/components/WebPageSections/ProcessSection.vue b/front/src/components/WebPageSections/ProcessSection.vue index b5aab08..bdd755c 100644 --- a/front/src/components/WebPageSections/ProcessSection.vue +++ b/front/src/components/WebPageSections/ProcessSection.vue @@ -54,11 +54,10 @@
- Tip: Si ves “🟢” en una fecha, significa que esa etapa está activa hoy. + Si ves “🟢” en una fecha, significa que esa etapa está activa hoy.
-
2) ¿Qué debo hacer ahora?
diff --git a/front/src/views/administrador/cursos/MarkdownLatex.vue b/front/src/views/administrador/cursos/MarkdownLatex.vue index 715da1c..e4b6c65 100644 --- a/front/src/views/administrador/cursos/MarkdownLatex.vue +++ b/front/src/views/administrador/cursos/MarkdownLatex.vue @@ -122,11 +122,11 @@ onMounted(() => { } .markdown-content :deep(.katex-display) { - margin: 0.75em 0; - text-align: center; - - overflow-x: auto; /* si es largo, que haga scroll horizontal */ - overflow-y: hidden; /* IMPORTANTE: no scroll vertical -> no flechas */ + margin: 14px 0; /* espacio arriba/abajo */ + padding: 10px 0; /* aire dentro del bloque */ + line-height: 1.25; /* evita que el contenedor aplaste */ + overflow-x: auto; + overflow-y: visible; /* ✅ CLAVE: que NO recorte arriba/abajo */ } /* Quitar botones/flechas del scrollbar (Chrome/Edge) */ .markdown-content :deep(.katex-display::-webkit-scrollbar-button) { diff --git a/front/src/views/postulante/Dashboard.vue b/front/src/views/postulante/Dashboard.vue index 7d49148..7bbe0eb 100644 --- a/front/src/views/postulante/Dashboard.vue +++ b/front/src/views/postulante/Dashboard.vue @@ -1,122 +1,118 @@ @@ -125,10 +121,7 @@ import { computed, onMounted, reactive, ref } from "vue"; import { Modal, message } from "ant-design-vue"; import { useRouter } from "vue-router"; import { useAuthStore } from "../../store/postulanteStore"; - - -const ROUTE_TEST_PANEL = { name: "PanelTest" }; -const ROUTE_PROCESS_DETAIL = (id) => ({ name: "ProcesoDetalle", params: { id } }); +import axios from "../../axiosPostulante"; const router = useRouter(); const authStore = useAuthStore(); @@ -143,56 +136,35 @@ const state = reactive({ const canGoTest = computed(() => !!state.test.hasAssigned); const processColumns = computed(() => [ - { title: "Proceso", dataIndex: "name", key: "name", ellipsis: true }, - { title: "Estado", key: "status", responsive: ["sm"] }, - { title: "Aptitud", key: "eligibility", responsive: ["md"] }, + { title: "Título", dataIndex: "titulo", key: "titulo", ellipsis: true }, + { title: "Inicio preinscripción", dataIndex: "fecha_inicio_preinscripcion", key: "fecha_inicio_preinscripcion" }, + { title: "Fin preinscripción", dataIndex: "fecha_fin_preinscripcion", key: "fecha_fin_preinscripcion" }, { title: "Acciones", key: "actions" }, ]); +function formatDate(val) { + if (!val) return "-"; + if (typeof val === "string" && val.includes("T")) return val.split("T")[0]; + const d = new Date(val); + if (isNaN(d.getTime())) return String(val); + return d.toLocaleDateString(); +} const api = { - async getDashboard() { - return new Promise((resolve) => { - setTimeout(() => { - resolve({ - test: { hasAssigned: true }, - processes: [ - { - id: 10, - name: "Admisión 2026-I", - statusText: "ABIERTO", - statusColor: "blue", - isEligible: true, - canApply: true, - blockReason: "", - }, - { - id: 11, - name: "Admisión Extraordinaria 2026", - statusText: "PRONTO", - statusColor: "gold", - isEligible: false, - canApply: false, - blockReason: "Este proceso aún no permite postular.", - }, - ], - }); - }, 250); - }); - }, - async applyToProcess(processId) { - return new Promise((resolve) => setTimeout(resolve, 300)); + async getProcesosActivos() { + const { data } = await axios.get("/procesos-disponibles-preinscripcion"); + return data; }, }; async function fetchDashboard() { loading.value = true; try { - const data = await api.getDashboard(); - state.test = { ...state.test, ...data.test }; - state.processes = Array.isArray(data.processes) ? data.processes : []; - } catch { - message.error("No se pudo cargar el dashboard."); + const resp = await api.getProcesosActivos(); + state.processes = Array.isArray(resp?.data) ? resp.data : []; + } catch (e) { + console.error(e); + state.processes = []; } finally { loading.value = false; } @@ -212,91 +184,33 @@ function goToTest() { }); } -function onViewProcess(process) { - - message.info(`Abrir detalle del proceso: ${process.name}`); -} - function onApply(process) { - if (!process.canApply) return; - - Modal.confirm({ - title: "Confirmar postulación", - content: `¿Deseas postular al proceso "${process.name}"?`, - okText: "Postular", - cancelText: "Cancelar", - async onOk() { - try { - await api.applyToProcess(process.id); - message.success("Postulación registrada."); - await fetchDashboard(); - } catch { - message.error("No se pudo completar la postulación."); - } - }, - }); + if (!process.link_preinscripcion) return; + window.open(process.link_preinscripcion, "_blank", "noopener,noreferrer"); } onMounted(fetchDashboard); + \ No newline at end of file diff --git a/front/src/views/postulante/PortalView.vue b/front/src/views/postulante/PortalView.vue index fecca08..c4052cf 100644 --- a/front/src/views/postulante/PortalView.vue +++ b/front/src/views/postulante/PortalView.vue @@ -690,23 +690,16 @@ onUnmounted(() => { height: 100%; } -@media (max-width: 992px) { - .main-layout, - .main-layout.layout-collapsed { - margin-left: 0 !important; - } - - .content-container { - max-width: 100%; - } -} - @media (max-width: 768px) { .header, .header-container { height: 56px; } + .header-container { + padding: 0 12px; /* ✅ menos padding lateral en móvil */ + } + .sidebar, .sidebar.sidebar-mobile { top: 56px; @@ -714,8 +707,8 @@ onUnmounted(() => { } .logo-img { - height: 36px; - width: 36px; + height: 34px; + width: 34px; } .portal-title { @@ -726,12 +719,58 @@ onUnmounted(() => { display: none; } + /* ✅ CONTENT: padding externo pequeño (se ve pro y no encoge tanto) */ .content { - padding: 12px; + padding: 8px !important; + background: var(--ant-colorBgLayout, #f5f5f5); } + .content-container { + max-width: 100% !important; + margin: 0 !important; + padding: 0 !important; + } + + /* ✅ CARD: sin borde/sombra pesada en móvil y radio moderado */ .content-card { - border-radius: 14px; + border-radius: 12px !important; + margin: 0 !important; + border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06)) !important; + box-shadow: 0 6px 16px rgba(0,0,0,.06) !important; + background: #fff; + overflow: hidden; + } + + /* ✅ CLAVE: padding interno del body (no exagerado) */ + .content-card :deep(.ant-card-body) { + padding: 12px !important; + } + + /* ✅ opcional: quita el “papel cuadriculado” en móvil (se ve más limpio) */ + .content-card::before { + display: none !important; + } +} + +/* Extra: teléfonos chicos (más pro aún) */ +@media (max-width: 480px) { + .content { + padding: 6px !important; + } + .content-card :deep(.ant-card-body) { + padding: 10px !important; + } +} +/* ===== FIX MÓVIL REAL ===== */ +@media (max-width: 992px) { + .main-layout, + .main-layout.layout-collapsed { + margin-left: 0 !important; + } + + .sidebar { + position: fixed !important; + left: 0; } } \ No newline at end of file diff --git a/front/src/views/postulante/PreguntasExamen.vue b/front/src/views/postulante/PreguntasExamen.vue index 7ae9a8d..d3cef05 100644 --- a/front/src/views/postulante/PreguntasExamen.vue +++ b/front/src/views/postulante/PreguntasExamen.vue @@ -1,14 +1,10 @@ - - - + \ No newline at end of file diff --git a/front/src/views/postulante/Resultados.vue b/front/src/views/postulante/Resultados.vue index 50bef88..de72e26 100644 --- a/front/src/views/postulante/Resultados.vue +++ b/front/src/views/postulante/Resultados.vue @@ -1,169 +1,171 @@ - + @@ -242,15 +244,17 @@ const rowsCursos = computed(() => { const rows = Array.from(keys).map((k) => { const correctas = parseCorrectas(corr?.[k] ?? 0) - const total = parseTotal(k) || (() => { - const val = corr?.[k] - if (typeof val === 'string' && val.includes('de')) { - const [, b] = val.split('de') - const y = Number(b.trim()) - return Number.isFinite(y) ? y : 0 - } - return 0 - })() + const total = + parseTotal(k) || + (() => { + const val = corr?.[k] + if (typeof val === 'string' && val.includes('de')) { + const [, b] = val.split('de') + const y = Number(b.trim()) + return Number.isFinite(y) ? y : 0 + } + return 0 + })() const incorrectas = parseIncorrectas(k) const ratio = total > 0 ? Math.round((correctas / total) * 100) : 0 return { curso: k, correctas, incorrectas, total, ratio } @@ -262,7 +266,6 @@ const rowsCursos = computed(() => { const recalcular = async () => { if (!examenId.value) return - cargando.value = true try { const r = await examenStore.calificarExamen(examenId.value) @@ -286,17 +289,58 @@ onMounted(async () => { + \ No newline at end of file diff --git a/front/src/views/postulante/Test.vue b/front/src/views/postulante/Test.vue index 2c32c19..cfb24fd 100644 --- a/front/src/views/postulante/Test.vue +++ b/front/src/views/postulante/Test.vue @@ -550,10 +550,10 @@ No se realiza ningún pago adicional. + .heroTitle { + font-size: 1.55rem; + } + + .ctaBtn { + height: 48px; + } +} + \ No newline at end of file