last_changes1

main
elmer-20 2 months ago
parent 6694500ed5
commit 8f35717dc9

@ -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);
}
}
} }

@ -83,7 +83,7 @@ class WebController extends Controller
'fecha_fin_preinscripcion', 'fecha_fin_preinscripcion',
]) ])
->where('publicado', 1) ->where('publicado', 1)
->whereIn('estado', ['publicado', 'en_proceso']) ->whereIn('estado', ['nuevo','publicado','en_proceso'])
->whereNotNull('link_preinscripcion') ->whereNotNull('link_preinscripcion')
->whereNotNull('fecha_inicio_preinscripcion') ->whereNotNull('fecha_inicio_preinscripcion')
->whereNotNull('fecha_fin_preinscripcion') ->whereNotNull('fecha_fin_preinscripcion')

@ -13,66 +13,62 @@ class ExamenService
/** /**
* Generar preguntas según reglas * Generar preguntas según reglas
*/ */
public function generarPreguntasExamen(Examen $examen): array public function generarPreguntasExamen(Examen $examen): array
{ {
if ($examen->preguntasAsignadas()->exists()) { if ($examen->preguntasAsignadas()->exists()) {
return [ return [
'success' => false, 'success' => false,
'message' => 'El examen ya tiene preguntas' 'message' => 'El examen ya tiene preguntas'
]; ];
} }
$reglas = ReglaAreaProceso::where('area_proceso_id', $examen->area_proceso_id) $reglas = ReglaAreaProceso::where('area_proceso_id', $examen->area_proceso_id)
->orderBy('orden') ->orderBy('orden')
->get(); ->get();
if ($reglas->isEmpty()) { if ($reglas->isEmpty()) {
return [ return [
'success' => false, 'success' => false,
'message' => 'No hay reglas configuradas' 'message' => 'No hay reglas configuradas'
]; ];
} }
DB::beginTransaction(); 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::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) { if ($preguntas->count() < $regla->cantidad_preguntas) {
DB::rollBack(); throw new \Exception("Preguntas insuficientes para curso {$regla->curso_id}");
return ['success' => false, 'message' => $e->getMessage()]; }
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 public function obtenerPreguntasExamen(Examen $examen): array
{ {

@ -207,4 +207,9 @@ Route::middleware('auth:sanctum')->prefix('admin')->group(function () {
Route::get('/proceso-resultado/{procesoId}/archivos', [ProcesoAdmisionResultadoArchivoController::class, 'index']); Route::get('/proceso-resultado/{procesoId}/archivos', [ProcesoAdmisionResultadoArchivoController::class, 'index']);
Route::post('/proceso-resultado/{procesoId}/archivos', [ProcesoAdmisionResultadoArchivoController::class, 'store']); Route::post('/proceso-resultado/{procesoId}/archivos', [ProcesoAdmisionResultadoArchivoController::class, 'store']);
Route::delete('/proceso-resultado/archivos/{id}', [ProcesoAdmisionResultadoArchivoController::class, 'destroy']); Route::delete('/proceso-resultado/archivos/{id}', [ProcesoAdmisionResultadoArchivoController::class, 'destroy']);
}); });
Route::middleware('auth:sanctum')->get(
'/mis-procesos/{idProceso}/avance',
[PostulanteAuthController::class, 'obtenerAvanceProcesoPostulante']
);

@ -109,18 +109,24 @@ const routes = [
meta: { requiresAuth: true } meta: { requiresAuth: true }
}, },
{ // {
path: '/portal-postulante/pagos', // path: '/portal-postulante/pagos',
name: 'PanelPagos', // name: 'PanelPagos',
component: () => import('../views/postulante/Pagos.vue'), // component: () => import('../views/postulante/Pagos.vue'),
meta: { requiresAuth: true } // meta: { requiresAuth: true }
}, // },
{ {
path: '/portal-postulante/mis-procesos', path: '/portal-postulante/mis-procesos',
name: 'PanelProcesos', name: 'PanelProcesos',
component: () => import('../views/postulante/MisProcesos.vue'), component: () => import('../views/postulante/MisProcesos.vue'),
meta: { requiresAuth: true } meta: { requiresAuth: true }
}, },
{
path: '/portal-postulante/mis-procesos-estado/',
name: 'AvanceProceso',
component: () => import('../views/postulante/AvanceProceso.vue'),
meta: { requiresAuth: true }
},
] ]

@ -0,0 +1,271 @@
<template>
<div class="title">Seguimiento del proceso</div>
<a-card class="card" :bordered="true">
<template #title>
<div class="header">
<div class="headerLeft">
<div class="subtitle">
Proceso 31: <b>Examen General 2026-I</b>
</div>
</div>
<div class="headerRight">
<a-button @click="cargarAvance" :loading="loading" type="primary">
Actualizar
</a-button>
</div>
</div>
</template>
<a-spin :spinning="loading">
<!-- NO PARTICIPA -->
<div v-if="noParticipa" class="noBox">
<div class="noTitle">Actualmente no estás participando en ningún proceso</div>
<div class="noDesc">
Si crees que esto es un error, verifica que hayas realizado tu preinscripción o inicia sesión con el DNI correcto.
</div>
</div>
<!-- PARTICIPA -->
<template v-else>
<a-alert
v-if="avance"
:message="`Actualmente estás en la etapa de: ${avance.estado || '-'}`"
:description="indicador"
type="info"
show-icon
class="alertState"
/>
<div class="timelineWrap" v-if="avance">
<div class="timelineHeader">
<div class="chip">
<span class="chipLabel">Proceso</span>
<span class="chipValue">Examen General 2026-I</span>
</div>
<div class="chip">
<span class="chipLabel">Etapa</span>
<span class="chipValue strong">{{ avance.estado || "-" }}</span>
</div>
</div>
<div class="simpleBox">
<div class="k">Indicador</div>
<div class="v">{{ indicador }}</div>
<div v-if="avance.observacion" class="obs">
<div class="obsTitle">Observación</div>
<div class="obsText">{{ avance.observacion }}</div>
</div>
</div>
</div>
</template>
</a-spin>
</a-card>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { message } from "ant-design-vue";
import api from "../../axiosPostulante";
const loading = ref(false);
const avance = ref(null);
// cuando el backend no devuelve nada / 404 / estado false
const noParticipa = ref(false);
// fijo: Proceso 31
const idProceso = 31;
const cargarAvance = async () => {
loading.value = true;
avance.value = null;
noParticipa.value = false;
try {
const { data } = await api.get(`/mis-procesos/${idProceso}/avance`);
// Caso OK con datos
if (data?.success && data?.data) {
avance.value = data.data;
noParticipa.value = false;
return;
}
// Caso: success false o data vacío
avance.value = null;
noParticipa.value = true;
} catch (e) {
// 404 => no participa
if (e?.response?.status === 404) {
avance.value = null;
noParticipa.value = true;
return;
}
// Si tu backend manda 200 pero { success:false, message:"..." } no cae aquí.
message.error(e.response?.data?.message || "Error al cargar seguimiento");
// ante error real, decide si mostrar "no participa" o no:
noParticipa.value = true;
} finally {
loading.value = false;
}
};
const normalizar = (txt) =>
String(txt || "")
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.trim();
const indicador = computed(() => {
const st = normalizar(avance.value?.estado);
if (!st) return "Sigue revisando esta sección para ver el siguiente paso.";
if (st.includes("preins"))
return "Ya realizaste la preinscripción. Por ahora, espera que el sistema habilite el siguiente paso.";
if (st.includes("inscrip"))
return "Estás en inscripción final. Completa lo solicitado para generar tu constancia.";
return "Por ahora, espera que el sistema habilite el siguiente paso.";
});
onMounted(() => {
cargarAvance();
});
</script>
<style scoped>
.card {
width: 100%;
max-width: 1100px;
margin: 16px auto;
border-radius: 14px;
}
.header {
display: flex;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.title {
font-size: clamp(1.2rem, 4vw, 1.8rem);
font-weight: 700;
color: #0d1b52;
line-height: 1.2;
word-break: break-word;
}
.subtitle {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.alertState {
margin-bottom: 14px;
border-radius: 14px;
}
.timelineWrap {
display: grid;
gap: 12px;
}
.timelineHeader {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.chip {
background: #fafafa;
border-radius: 999px;
padding: 8px 12px;
display: flex;
gap: 8px;
align-items: center;
border: 1px solid rgba(0, 0, 0, 0.06);
}
.chipLabel {
font-size: 12px;
color: #6b7280;
font-weight: 700;
}
.chipValue {
font-weight: 900;
}
.chipValue.strong {
color: #1677ff;
}
.simpleBox {
background: #fafafa;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 14px;
padding: 12px;
}
.k {
font-size: 12px;
color: #6b7280;
font-weight: 700;
}
.v {
font-weight: 900;
margin-top: 6px;
color: #111827;
}
.obs {
margin-top: 12px;
background: #fff;
border-radius: 12px;
padding: 10px;
border: 1px dashed rgba(22, 119, 255, 0.35);
}
.obsTitle {
font-size: 12px;
color: #6b7280;
font-weight: 800;
}
.obsText {
font-weight: 900;
margin-top: 4px;
}
/* Caja “no participa” */
.noBox {
border-radius: 14px;
padding: 14px;
background: rgba(107, 114, 128, 0.06);
border: 1px solid rgba(107, 114, 128, 0.18);
}
.noTitle {
font-weight: 900;
color: #111827;
margin-bottom: 6px;
}
.noDesc {
font-size: 12px;
color: #6b7280;
}
@media (max-width: 768px) {
.card {
margin: 0;
border-radius: 0;
}
}
</style>

@ -1,10 +1,10 @@
<template> <template>
<div class="title">Mis procesos de admisión</div> <div class="title">Mis procesos de admisión anteriores</div>
<a-card class="card" :bordered="true"> <a-card class="card" :bordered="true">
<template #title> <template #title>
<div class="header"> <div class="header">
<div class="headerLeft"> <div class="headerLeft">
<div class="subtitle">Resultados registrados por DNI</div> <div class="subtitle">Resultados registrados por DNI</div>
</div> </div>
@ -22,21 +22,15 @@
</template> </template>
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div class="tools"> <div class="tools">
<div class="counter"> <div class="counter">
<span class="counterLabel">Total</span> <span class="counterLabel">Total</span>
<span class="counterValue">{{ procesosFiltrados.length }}</span> <span class="counterValue">{{ procesosFiltrados.length }}</span>
</div> </div>
<a-input
v-model:value="search"
allow-clear
placeholder="Buscar por nombre de proceso…"
class="search"
/>
</div> </div>
<!-- Desktop -->
<div class="tableWrap desktopOnly"> <div class="tableWrap desktopOnly">
<a-table <a-table
:dataSource="procesosFiltrados" :dataSource="procesosFiltrados"
@ -46,10 +40,8 @@
:scroll="{ x: 900 }" :scroll="{ x: 900 }"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'titulo'">
<template v-if="column.key === 'nombre'"> <div class="nombre">{{ record.titulo || "-" }}</div>
<div class="nombre">{{ record.nombre || "-" }}</div>
<div class="meta">ID: {{ record.id }}</div>
</template> </template>
<template v-else-if="column.key === 'puntaje'"> <template v-else-if="column.key === 'puntaje'">
@ -68,7 +60,6 @@
Ver detalle Ver detalle
</a-button> </a-button>
</template> </template>
</template> </template>
<template #emptyText> <template #emptyText>
@ -77,16 +68,12 @@
</a-table> </a-table>
</div> </div>
<!-- Mobile -->
<div class="cards mobileOnly"> <div class="cards mobileOnly">
<template v-if="procesosFiltrados.length"> <template v-if="procesosFiltrados.length">
<div <div v-for="p in procesosFiltrados" :key="p.id" class="itemCard">
v-for="p in procesosFiltrados"
:key="p.id"
class="itemCard"
>
<div class="itemTop"> <div class="itemTop">
<div class="itemTitle">{{ p.nombre || "-" }}</div> <div class="itemTitle">{{ p.titulo || "-" }}</div>
<span class="statusPill" :class="statusClass(p.apto)"> <span class="statusPill" :class="statusClass(p.apto)">
{{ aptoTexto(p.apto) }} {{ aptoTexto(p.apto) }}
</span> </span>
@ -99,8 +86,8 @@
</div> </div>
<div class="kv"> <div class="kv">
<div class="k">ID</div> <div class="k">Proceso:</div>
<div class="v">{{ p.id }}</div> <div class="v">{{ p.titulo }}</div>
</div> </div>
</div> </div>
@ -119,7 +106,6 @@
<a-empty v-else description="No se encontraron procesos" /> <a-empty v-else description="No se encontraron procesos" />
</div> </div>
</a-spin> </a-spin>
</a-card> </a-card>
</template> </template>
@ -134,7 +120,7 @@ const loading = ref(false);
const search = ref(""); const search = ref("");
const columns = [ 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: "Puntaje", dataIndex: "puntaje", key: "puntaje", width: 140 },
{ title: "Estado", dataIndex: "apto", key: "apto", width: 160 }, { title: "Estado", dataIndex: "apto", key: "apto", width: 160 },
{ title: "Acciones", key: "acciones", width: 160 }, { title: "Acciones", key: "acciones", width: 160 },
@ -147,37 +133,42 @@ const obtenerProcesos = async () => {
if (data?.success) { if (data?.success) {
procesos.value = Array.isArray(data.data) ? data.data : []; procesos.value = Array.isArray(data.data) ? data.data : [];
} else { } else {
message.error("No se pudieron obtener los procesos"); message.error(data?.message || "No se pudieron obtener los procesos");
procesos.value = [];
} }
} catch (e) { } catch (e) {
message.error(e.response?.data?.message || "Error al cargar procesos"); message.error(e.response?.data?.message || "Error al cargar procesos");
procesos.value = [];
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
const aptoTexto = (apto) => { const aptoTexto = (apto) => {
if (apto == 1) return "APTO"; if (apto === "SI") return "APTO";
if (apto == 0) return "NO APTO"; if (apto === "NO") return "NO APTO";
return "-"; return apto ?? "-";
}; };
const statusClass = (apto) => { const statusClass = (apto) => {
if (apto == 1) return "ok"; if (apto === "SI") return "ok";
if (apto == 0) return "bad"; if (apto === "NO") return "bad";
return "neutral"; return "neutral";
}; };
const procesosFiltrados = computed(() => { const procesosFiltrados = computed(() => {
const q = search.value.trim().toLowerCase(); const q = search.value.trim().toLowerCase();
if (!q) return procesos.value; if (!q) return procesos.value;
return procesos.value.filter((p) => return procesos.value.filter((p) =>
String(p.nombre || "").toLowerCase().includes(q) String(p.titulo || "").toLowerCase().includes(q)
); );
}); });
const verDetalle = (record) => { const verDetalle = (record) => {
message.info(`Proceso: ${record.nombre} | Puntaje: ${record.puntaje ?? "-"}`); message.info(
`Proceso: ${record.titulo || "-"} | Puntaje: ${record.puntaje ?? "-"} | Estado: ${aptoTexto(record.apto)}`
);
}; };
onMounted(() => { onMounted(() => {
@ -186,8 +177,6 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
.card { .card {
width: 100%; width: 100%;
max-width: 1100px; max-width: 1100px;
@ -195,7 +184,6 @@ onMounted(() => {
border-radius: 14px; border-radius: 14px;
} }
.header { .header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -241,7 +229,6 @@ onMounted(() => {
border-radius: 12px; border-radius: 12px;
} }
.tableWrap { .tableWrap {
width: 100%; width: 100%;
overflow-x: auto; overflow-x: auto;
@ -261,7 +248,6 @@ onMounted(() => {
color: #6b7280; color: #6b7280;
} }
.statusPill { .statusPill {
padding: 4px 10px; padding: 4px 10px;
border-radius: 999px; border-radius: 999px;
@ -271,12 +257,17 @@ onMounted(() => {
} }
.statusPill.ok { .statusPill.ok {
background: rgba(22,119,255,.1); background: rgba(22, 119, 255, 0.1);
border: 1px solid rgba(22,119,255,.3); border: 1px solid rgba(22, 119, 255, 0.3);
} }
.statusPill.bad { .statusPill.bad {
background: rgba(0,0,0,.04); background: rgba(0, 0, 0, 0.04);
}
.statusPill.neutral {
background: rgba(107, 114, 128, 0.08);
border: 1px solid rgba(107, 114, 128, 0.18);
} }
.cards { .cards {
@ -285,7 +276,7 @@ onMounted(() => {
} }
.itemCard { .itemCard {
border: 1px solid rgba(0,0,0,.08); border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 14px; border-radius: 14px;
padding: 12px; padding: 12px;
} }
@ -294,6 +285,7 @@ onMounted(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px;
} }
.itemTitle { .itemTitle {
@ -333,17 +325,24 @@ onMounted(() => {
margin-top: 12px; margin-top: 12px;
} }
.desktopOnly {
.desktopOnly { display: block; } display: block;
.mobileOnly { display: none; } }
.mobileOnly {
display: none;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.tools { .tools {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.desktopOnly { display: none; } .desktopOnly {
.mobileOnly { display: block; } display: none;
}
.mobileOnly {
display: block;
}
.header { .header {
flex-direction: column; flex-direction: column;
@ -356,5 +355,4 @@ onMounted(() => {
border-radius: 0; border-radius: 0;
} }
} }
</style>
</style>

@ -118,20 +118,20 @@
<span>Test</span> <span>Test</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="documentos"> <!-- <a-menu-item key="documentos">
<FolderOutlined /> <FolderOutlined />
<span>Documentos</span> <span>Documentos</span>
</a-menu-item> </a-menu-item> -->
<a-menu-item key="mis-procesos"> <a-menu-item key="mis-procesos">
<FolderOutlined /> <FolderOutlined />
<span>Mis Procesos</span> <span>Mis Procesos</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="pagos"> <!-- <a-menu-item key="pagos">
<DollarOutlined /> <DollarOutlined />
<span>Pagos</span> <span>Pagos</span>
<a-tag v-if="!sidebarCollapsed" color="green" class="menu-tag">Al día</a-tag> <a-tag v-if="!sidebarCollapsed" color="green" class="menu-tag">Al día</a-tag>
</a-menu-item> </a-menu-item> -->
<a-menu-divider v-if="!sidebarCollapsed" /> <a-menu-divider v-if="!sidebarCollapsed" />
@ -144,12 +144,12 @@
<span>Estado del Proceso</span> <span>Estado del Proceso</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="resultados"> <!-- <a-menu-item key="resultados">
<FileDoneOutlined /> <FileDoneOutlined />
<span>Resultados</span> <span>Resultados</span>
</a-menu-item> </a-menu-item> -->
<a-menu-divider v-if="!sidebarCollapsed" /> <!-- <a-menu-divider v-if="!sidebarCollapsed" />
<div class="menu-section" v-if="!sidebarCollapsed"> <div class="menu-section" v-if="!sidebarCollapsed">
<div class="section-label">Configuración</div> <div class="section-label">Configuración</div>
@ -158,7 +158,7 @@
<a-menu-item key="configuracion"> <a-menu-item key="configuracion">
<SettingOutlined /> <SettingOutlined />
<span>Configuración</span> <span>Configuración</span>
</a-menu-item> </a-menu-item> -->
</a-menu> </a-menu>
</div> </div>
@ -256,7 +256,7 @@ const handleMenuSelect = ({ key }) => {
'documentos': { name: 'DocumentosPostulante' }, 'documentos': { name: 'DocumentosPostulante' },
'pagos': { name: 'PanelPagos' }, 'pagos': { name: 'PanelPagos' },
'mis-procesos': { name: 'PanelProcesos' }, 'mis-procesos': { name: 'PanelProcesos' },
'seguimiento': { name: 'SeguimientoPostulante' }, 'seguimiento': { name: 'AvanceProceso' },
'resultados': { name: 'ResultadosPostulante' }, 'resultados': { name: 'ResultadosPostulante' },
'configuracion': { name: 'ConfiguracionPostulante' } 'configuracion': { name: 'ConfiguracionPostulante' }
} }

@ -467,10 +467,71 @@ const iniciarSesionExamen = async () => {
cargandoInicio.value = false; 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(() => { 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); if (timerIntervalId) clearInterval(timerIntervalId);
}); });
</script> </script>
@ -483,7 +544,20 @@ onBeforeUnmount(() => {
background: #f5f7fb; background: #f5f7fb;
min-height: 100vh; 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 */ /* HEADER */
.top-card { .top-card {
border-radius: 16px; border-radius: 16px;
@ -763,7 +837,30 @@ onBeforeUnmount(() => {
.optRow:has(:deep(.ant-radio-checked)) { .optRow:has(:deep(.ant-radio-checked)) {
background: rgba(37, 99, 235, 0.06); 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 */ /* hover suave */
.optRow:hover { .optRow:hover {
background: rgba(15, 23, 42, 0.03); background: rgba(15, 23, 42, 0.03);

Loading…
Cancel
Save