From 8f35717dc99cc3495ffe0dba7d0c81dbdc968287 Mon Sep 17 00:00:00 2001 From: elmer-20 <80175046+elmer-20@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:18:08 -0500 Subject: [PATCH] last_changes1 --- .../Controllers/PostulanteAuthController.php | 73 +++++ back/app/Http/Controllers/WebController.php | 2 +- back/app/Services/ExamenService.php | 100 ++++--- back/routes/api.php | 7 +- front/src/router/index.js | 18 +- front/src/views/postulante/AvanceProceso.vue | 271 ++++++++++++++++++ front/src/views/postulante/MisProcesos.vue | 96 +++---- front/src/views/postulante/PortalView.vue | 18 +- .../src/views/postulante/PreguntasExamen.vue | 99 ++++++- 9 files changed, 565 insertions(+), 119 deletions(-) create mode 100644 front/src/views/postulante/AvanceProceso.vue diff --git a/back/app/Http/Controllers/PostulanteAuthController.php b/back/app/Http/Controllers/PostulanteAuthController.php index 681ff06..02206c8 100644 --- a/back/app/Http/Controllers/PostulanteAuthController.php +++ b/back/app/Http/Controllers/PostulanteAuthController.php @@ -267,4 +267,77 @@ public function misProcesos(Request $request) ]); } +public function obtenerAvanceProcesoPostulante(Request $request, $idProceso) +{ + $postulante = $request->user(); + + if (!$postulante) { + return response()->json([ + 'success' => false, + 'message' => 'No autenticado' + ], 401); + } + + $dni = trim($postulante->dni); + + if (!$dni) { + return response()->json([ + 'success' => false, + 'message' => 'El postulante no tiene DNI registrado' + ], 422); + } + + $url = "https://inscripciones.admision.unap.edu.pe/api/get-avance-proceso-postulante/{$idProceso}/{$dni}"; + + try { + $response = Http::timeout(15)->get($url); + + if (!$response->successful()) { + return response()->json([ + 'success' => false, + 'message' => 'No se pudo obtener el avance del proceso', + 'status_http' => $response->status(), + 'error' => $response->json() ?? $response->body(), + ], 502); + } + + $payload = $response->json(); + + // Tu API externa devuelve: { estado: true/false, datos: {...} } + if (!isset($payload['estado']) || $payload['estado'] !== true) { + return response()->json([ + 'success' => false, + 'message' => 'La API devolvió estado false', + 'raw' => $payload + ], 404); + } + + $datos = $payload['datos'] ?? []; + + return response()->json([ + 'success' => true, + 'data' => [ + 'dni' => $datos['dni'] ?? $dni, + 'id_proceso' => $datos['id_proceso'] ?? (int)$idProceso, + 'avance' => $datos['avance'] ?? null, + 'estado' => $datos['estado'] ?? null, + 'observacion' => $datos['observacion'] ?? '', + ], + 'raw' => $payload, // si no lo quieres, elimínalo + ]); + + } catch (\Throwable $e) { + Log::error('Error al consultar avance del proceso', [ + 'idProceso' => $idProceso, + 'dni' => $dni, + 'error' => $e->getMessage(), + ]); + + return response()->json([ + 'success' => false, + 'message' => 'Error interno consultando avance del proceso' + ], 500); + } +} + } diff --git a/back/app/Http/Controllers/WebController.php b/back/app/Http/Controllers/WebController.php index 0f0cbe7..d9bb6c7 100644 --- a/back/app/Http/Controllers/WebController.php +++ b/back/app/Http/Controllers/WebController.php @@ -83,7 +83,7 @@ class WebController extends Controller 'fecha_fin_preinscripcion', ]) ->where('publicado', 1) - ->whereIn('estado', ['publicado', 'en_proceso']) + ->whereIn('estado', ['nuevo','publicado','en_proceso']) ->whereNotNull('link_preinscripcion') ->whereNotNull('fecha_inicio_preinscripcion') ->whereNotNull('fecha_fin_preinscripcion') diff --git a/back/app/Services/ExamenService.php b/back/app/Services/ExamenService.php index 0c99677..9c04b40 100644 --- a/back/app/Services/ExamenService.php +++ b/back/app/Services/ExamenService.php @@ -13,66 +13,62 @@ class ExamenService /** * Generar preguntas según reglas */ - public function generarPreguntasExamen(Examen $examen): array - { - if ($examen->preguntasAsignadas()->exists()) { - return [ - 'success' => false, - 'message' => 'El examen ya tiene preguntas' - ]; - } +public function generarPreguntasExamen(Examen $examen): array +{ + if ($examen->preguntasAsignadas()->exists()) { + return [ + 'success' => false, + 'message' => 'El examen ya tiene preguntas' + ]; + } - $reglas = ReglaAreaProceso::where('area_proceso_id', $examen->area_proceso_id) - ->orderBy('orden') - ->get(); + $reglas = ReglaAreaProceso::where('area_proceso_id', $examen->area_proceso_id) + ->orderBy('orden') + ->get(); - if ($reglas->isEmpty()) { - return [ - 'success' => false, - 'message' => 'No hay reglas configuradas' - ]; - } + if ($reglas->isEmpty()) { + return [ + 'success' => false, + 'message' => 'No hay reglas configuradas' + ]; + } - DB::beginTransaction(); - - try { - $orden = 1; - - foreach ($reglas as $regla) { - $preguntas = Pregunta::where('curso_id', $regla->curso_id) - ->where('activo', 1) - ->when($regla->nivel_dificultad, fn ($q) => - $q->where('nivel_dificultad', $regla->nivel_dificultad) - ) - ->inRandomOrder() - ->limit($regla->cantidad_preguntas) - ->get(); - - if ($preguntas->count() < $regla->cantidad_preguntas) { - throw new \Exception("Preguntas insuficientes para curso {$regla->curso_id}"); - } - - foreach ($preguntas as $pregunta) { - PreguntaAsignada::create([ - 'examen_id' => $examen->id, - 'pregunta_id' => $pregunta->id, - 'orden' => $orden++, - 'puntaje_base' => $regla->ponderacion, - 'estado' => 'pendiente', - ]); - } - } + DB::beginTransaction(); - DB::commit(); + try { + $orden = 1; - return ['success' => true]; + foreach ($reglas as $regla) { + $preguntas = Pregunta::where('curso_id', $regla->curso_id) + ->where('activo', 1) + // ✅ sin filtro por nivel_dificultad (trae todo) + ->inRandomOrder() + ->limit($regla->cantidad_preguntas) + ->get(); - } catch (\Throwable $e) { - DB::rollBack(); - return ['success' => false, 'message' => $e->getMessage()]; + if ($preguntas->count() < $regla->cantidad_preguntas) { + throw new \Exception("Preguntas insuficientes para curso {$regla->curso_id}"); + } + + foreach ($preguntas as $pregunta) { + PreguntaAsignada::create([ + 'examen_id' => $examen->id, + 'pregunta_id' => $pregunta->id, + 'orden' => $orden++, + 'puntaje_base' => $regla->ponderacion, + 'estado' => 'pendiente', + ]); + } } - } + DB::commit(); + return ['success' => true]; + + } catch (\Throwable $e) { + DB::rollBack(); + return ['success' => false, 'message' => $e->getMessage()]; + } +} public function obtenerPreguntasExamen(Examen $examen): array { diff --git a/back/routes/api.php b/back/routes/api.php index f08369b..0bbd438 100644 --- a/back/routes/api.php +++ b/back/routes/api.php @@ -207,4 +207,9 @@ Route::middleware('auth:sanctum')->prefix('admin')->group(function () { Route::get('/proceso-resultado/{procesoId}/archivos', [ProcesoAdmisionResultadoArchivoController::class, 'index']); Route::post('/proceso-resultado/{procesoId}/archivos', [ProcesoAdmisionResultadoArchivoController::class, 'store']); Route::delete('/proceso-resultado/archivos/{id}', [ProcesoAdmisionResultadoArchivoController::class, 'destroy']); -}); \ No newline at end of file +}); + +Route::middleware('auth:sanctum')->get( + '/mis-procesos/{idProceso}/avance', + [PostulanteAuthController::class, 'obtenerAvanceProcesoPostulante'] +); \ No newline at end of file diff --git a/front/src/router/index.js b/front/src/router/index.js index 8755c0a..1960778 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -109,18 +109,24 @@ const routes = [ meta: { requiresAuth: true } }, - { - path: '/portal-postulante/pagos', - name: 'PanelPagos', - component: () => import('../views/postulante/Pagos.vue'), - meta: { requiresAuth: true } - }, + // { + // path: '/portal-postulante/pagos', + // name: 'PanelPagos', + // component: () => import('../views/postulante/Pagos.vue'), + // meta: { requiresAuth: true } + // }, { path: '/portal-postulante/mis-procesos', name: 'PanelProcesos', component: () => import('../views/postulante/MisProcesos.vue'), meta: { requiresAuth: true } }, + { + path: '/portal-postulante/mis-procesos-estado/', + name: 'AvanceProceso', + component: () => import('../views/postulante/AvanceProceso.vue'), + meta: { requiresAuth: true } + }, ] diff --git a/front/src/views/postulante/AvanceProceso.vue b/front/src/views/postulante/AvanceProceso.vue new file mode 100644 index 0000000..4bb91c7 --- /dev/null +++ b/front/src/views/postulante/AvanceProceso.vue @@ -0,0 +1,271 @@ + + Seguimiento del proceso + + + + + + + Proceso 31: Examen General 2026-I + + + + + + Actualizar + + + + + + + + + Actualmente no estás participando en ningún proceso + + Si crees que esto es un error, verifica que hayas realizado tu preinscripción o inicia sesión con el DNI correcto. + + + + + + + + + + + Proceso + Examen General 2026-I + + + Etapa + {{ avance.estado || "-" }} + + + + + Indicador + {{ indicador }} + + + Observación + {{ avance.observacion }} + + + + + + + + + + + \ No newline at end of file diff --git a/front/src/views/postulante/MisProcesos.vue b/front/src/views/postulante/MisProcesos.vue index 76664fb..cc2044f 100644 --- a/front/src/views/postulante/MisProcesos.vue +++ b/front/src/views/postulante/MisProcesos.vue @@ -1,10 +1,10 @@ - Mis procesos de admisión + Mis procesos de admisión anteriores + - Resultados registrados por DNI @@ -22,21 +22,15 @@ - Total {{ procesosFiltrados.length }} - + - - - {{ record.nombre || "-" }} - ID: {{ record.id }} + + {{ record.titulo || "-" }} @@ -68,7 +60,6 @@ Ver detalle - @@ -77,16 +68,12 @@ - + - + - {{ p.nombre || "-" }} + {{ p.titulo || "-" }} {{ aptoTexto(p.apto) }} @@ -99,8 +86,8 @@ - ID - {{ p.id }} + Proceso: + {{ p.titulo }} @@ -119,7 +106,6 @@ - @@ -134,7 +120,7 @@ const loading = ref(false); const search = ref(""); const columns = [ - { title: "Proceso", dataIndex: "nombre", key: "nombre", width: 420 }, + { title: "Proceso", dataIndex: "titulo", key: "titulo", width: 420 }, { title: "Puntaje", dataIndex: "puntaje", key: "puntaje", width: 140 }, { title: "Estado", dataIndex: "apto", key: "apto", width: 160 }, { title: "Acciones", key: "acciones", width: 160 }, @@ -147,37 +133,42 @@ const obtenerProcesos = async () => { if (data?.success) { procesos.value = Array.isArray(data.data) ? data.data : []; } else { - message.error("No se pudieron obtener los procesos"); + message.error(data?.message || "No se pudieron obtener los procesos"); + procesos.value = []; } } catch (e) { message.error(e.response?.data?.message || "Error al cargar procesos"); + procesos.value = []; } finally { loading.value = false; } }; const aptoTexto = (apto) => { - if (apto == 1) return "APTO"; - if (apto == 0) return "NO APTO"; - return "-"; + if (apto === "SI") return "APTO"; + if (apto === "NO") return "NO APTO"; + return apto ?? "-"; }; const statusClass = (apto) => { - if (apto == 1) return "ok"; - if (apto == 0) return "bad"; + if (apto === "SI") return "ok"; + if (apto === "NO") return "bad"; return "neutral"; }; const procesosFiltrados = computed(() => { const q = search.value.trim().toLowerCase(); if (!q) return procesos.value; + return procesos.value.filter((p) => - String(p.nombre || "").toLowerCase().includes(q) + String(p.titulo || "").toLowerCase().includes(q) ); }); const verDetalle = (record) => { - message.info(`Proceso: ${record.nombre} | Puntaje: ${record.puntaje ?? "-"}`); + message.info( + `Proceso: ${record.titulo || "-"} | Puntaje: ${record.puntaje ?? "-"} | Estado: ${aptoTexto(record.apto)}` + ); }; onMounted(() => { @@ -186,8 +177,6 @@ onMounted(() => { + \ No newline at end of file diff --git a/front/src/views/postulante/PortalView.vue b/front/src/views/postulante/PortalView.vue index c4052cf..e82b156 100644 --- a/front/src/views/postulante/PortalView.vue +++ b/front/src/views/postulante/PortalView.vue @@ -118,20 +118,20 @@ Test - + Mis Procesos - + @@ -144,12 +144,12 @@ Estado del Proceso - + - + @@ -256,7 +256,7 @@ const handleMenuSelect = ({ key }) => { 'documentos': { name: 'DocumentosPostulante' }, 'pagos': { name: 'PanelPagos' }, 'mis-procesos': { name: 'PanelProcesos' }, - 'seguimiento': { name: 'SeguimientoPostulante' }, + 'seguimiento': { name: 'AvanceProceso' }, 'resultados': { name: 'ResultadosPostulante' }, 'configuracion': { name: 'ConfiguracionPostulante' } } diff --git a/front/src/views/postulante/PreguntasExamen.vue b/front/src/views/postulante/PreguntasExamen.vue index d3cef05..ef963d5 100644 --- a/front/src/views/postulante/PreguntasExamen.vue +++ b/front/src/views/postulante/PreguntasExamen.vue @@ -467,10 +467,71 @@ const iniciarSesionExamen = async () => { cargandoInicio.value = false; } }; +const onKeyDownAntiCheat = (e) => { + const key = (e.key || "").toLowerCase(); + + // F12 + if (e.key === "F12") { + e.preventDefault(); + e.stopPropagation(); + message.warning("Acción no permitida durante el examen."); + return; + } + + // Ctrl+Shift+I / J / C (DevTools) + if (e.ctrlKey && e.shiftKey && ["i", "j", "c"].includes(key)) { + e.preventDefault(); + e.stopPropagation(); + message.warning("Acción no permitida durante el examen."); + return; + } + + // Ctrl+U (view source) + if (e.ctrlKey && key === "u") { + e.preventDefault(); + e.stopPropagation(); + message.warning("Acción no permitida durante el examen."); + return; + } + + // Ctrl+S / Ctrl+P (guardar/imprimir) + if (e.ctrlKey && (key === "s" || key === "p")) { + e.preventDefault(); + e.stopPropagation(); + message.warning("Acción no permitida durante el examen."); + return; + } +}; + +const onContextMenuAntiCheat = (e) => { + e.preventDefault(); + message.warning("Acción no permitida durante el examen."); +}; + +const onCopyCutAntiCheat = (e) => { + e.preventDefault(); + message.warning("Copiar/Cortar deshabilitado durante el examen."); +}; -onMounted(iniciarSesionExamen); +onMounted(() => { + // anti-copia + window.addEventListener("keydown", onKeyDownAntiCheat, { capture: true }); + window.addEventListener("contextmenu", onContextMenuAntiCheat); + window.addEventListener("copy", onCopyCutAntiCheat); + window.addEventListener("cut", onCopyCutAntiCheat); + + // iniciar examen + iniciarSesionExamen(); +}); onBeforeUnmount(() => { + // anti-copia + window.removeEventListener("keydown", onKeyDownAntiCheat, { capture: true }); + window.removeEventListener("contextmenu", onContextMenuAntiCheat); + window.removeEventListener("copy", onCopyCutAntiCheat); + window.removeEventListener("cut", onCopyCutAntiCheat); + + // timer if (timerIntervalId) clearInterval(timerIntervalId); }); @@ -483,7 +544,20 @@ onBeforeUnmount(() => { background: #f5f7fb; min-height: 100vh; } +/* Bloquea selección */ +.exam-page { + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} +/* PERO permite seleccionar dentro de inputs/textarea */ +:deep(input), +:deep(textarea) { + -webkit-user-select: text; + -ms-user-select: text; + user-select: text; +} /* HEADER */ .top-card { border-radius: 16px; @@ -763,7 +837,30 @@ onBeforeUnmount(() => { .optRow:has(:deep(.ant-radio-checked)) { background: rgba(37, 99, 235, 0.06); } +/* Dentro de las opciones, fuerza a MarkdownLatex a no romper línea */ +.optTextInline :deep(p), +.optTextInline :deep(div) { + display: inline; + margin: 0; +} +/* Si KaTeX display te rompe línea, lo “inlinea” dentro de opciones */ +.optTextInline :deep(.katex-display) { + display: inline-block; + margin: 0; + padding: 0; +} +/* La parte del slot del radio (el span que envuelve tu contenido) */ +.optRow :deep(.ant-radio + span) { + display: flex; + gap: 10px; + align-items: flex-start; + width: 100%; +} +/* Evita márgenes raros dentro de opciones */ +.optTextInline :deep(.katex) { + line-height: 1.2; +} /* hover suave */ .optRow:hover { background: rgba(15, 23, 42, 0.03);