From f400639c864fae36fd23714beeb51acfced8b9e1 Mon Sep 17 00:00:00 2001 From: elmer Date: Sun, 1 Mar 2026 23:01:06 -0500 Subject: [PATCH] ultimo_cambio --- .../Controllers/PostulanteAuthController.php | 74 ++++++ back/routes/api.php | 4 +- front/src/store/postulanteStore.js | 22 +- front/src/views/postulante/AvanceProceso.vue | 21 +- front/src/views/postulante/Dashboard.vue | 101 +++++++- front/src/views/postulante/LoginView.vue | 240 +++++++++--------- front/src/views/postulante/MisProcesos.vue | 13 +- front/src/views/postulante/PortalView.vue | 4 +- front/src/views/postulante/Test.vue | 2 +- 9 files changed, 345 insertions(+), 136 deletions(-) diff --git a/back/app/Http/Controllers/PostulanteAuthController.php b/back/app/Http/Controllers/PostulanteAuthController.php index 02206c8..e1729a6 100644 --- a/back/app/Http/Controllers/PostulanteAuthController.php +++ b/back/app/Http/Controllers/PostulanteAuthController.php @@ -340,4 +340,78 @@ public function obtenerAvanceProcesoPostulante(Request $request, $idProceso) } } +public function miObservacion(Request $request) +{ + $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://test-admision.unap.edu.pe/service_observados/api/v1/observaciones/dni/{$dni}"; + + try { + $response = Http::timeout(15)->get($url); + + if (!$response->successful()) { + return response()->json([ + 'success' => false, + 'message' => 'No se pudo consultar observaciones', + 'status_http' => $response->status(), + ], 502); + } + + $payload = $response->json(); + + // si no viene, asumimos false + $esObservado = (bool)($payload['es_observado'] ?? false); + + // opcional: traer una observación "principal" + $detalle = null; + if (!empty($payload['observados']) && is_array($payload['observados'])) { + $o = $payload['observados'][0]; + $detalle = [ + 'tipo_observacion' => $o['tipo_observacion'] ?? null, + 'categoria' => $o['categoria'] ?? null, + 'observaciones' => $o['observaciones'] ?? null, + 'fecha_sancion' => $o['fecha_sancion'] ?? null, + 'fecha_fin' => $o['fecha_fin'] ?? null, + ]; + } + + return response()->json([ + 'success' => true, + 'dni' => $dni, + 'es_observado' => $esObservado, + 'mensaje' => $esObservado + ? 'Usted tiene una observación. Acérquese a la Dirección de Admisión para regularizar su situación.' + : 'Usted está apto para postular en este proceso.', + 'detalle' => $detalle, // si no lo quieres, bórralo + ]); + + } catch (\Throwable $e) { + Log::error('Error consultando observaciones del postulante', [ + 'dni' => $dni, + 'error' => $e->getMessage() + ]); + + return response()->json([ + 'success' => false, + 'message' => 'Error interno consultando observaciones' + ], 500); + } +} + } diff --git a/back/routes/api.php b/back/routes/api.php index de7c509..3c1476c 100644 --- a/back/routes/api.php +++ b/back/routes/api.php @@ -226,4 +226,6 @@ Route::middleware('auth:sanctum')->prefix('admin')->group(function () { Route::delete('/comunicados/{id}', [ComunicadoController::class, 'destroy']); Route::patch('/comunicados/{id}/toggle-activo', [ComunicadoController::class, 'toggleActivo']); Route::delete('/comunicados/imagenes/{imagenId}', [ComunicadoController::class, 'destroyImagen']); -}); \ No newline at end of file +}); + +Route::middleware('auth:sanctum')->get('/postulante/observacion', [PostulanteAuthController::class, 'miObservacion']); \ No newline at end of file diff --git a/front/src/store/postulanteStore.js b/front/src/store/postulanteStore.js index 7eba30f..4c0b1bc 100644 --- a/front/src/store/postulanteStore.js +++ b/front/src/store/postulanteStore.js @@ -112,6 +112,25 @@ export const useAuthStore = defineStore('auth', () => { return false } + const getObservacion = async () => { + try { + loading.value = true + error.value = null + + const response = await api.get('/postulante/observacion', { + headers: { Authorization: `Bearer ${token.value}` } + }) + + return response.data + } catch (err) { + error.value = err.response?.data?.message || 'Error consultando observación' + return { success: false, message: error.value } + } finally { + loading.value = false + } +} + + return { token, @@ -128,6 +147,7 @@ export const useAuthStore = defineStore('auth', () => { register, login, logout, - checkAuth + checkAuth, + getObservacion, } }) diff --git a/front/src/views/postulante/AvanceProceso.vue b/front/src/views/postulante/AvanceProceso.vue index 4bb91c7..ed5e98a 100644 --- a/front/src/views/postulante/AvanceProceso.vue +++ b/front/src/views/postulante/AvanceProceso.vue @@ -6,7 +6,7 @@
- Proceso 31: Examen General 2026-I + Proceso: Examen General 2026-I
@@ -124,13 +124,24 @@ const normalizar = (txt) => 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")) + 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."; + } + + // Registro + foto + huella (1 solo paso) + if (st.includes("registro") || st.includes("huella") || st.includes("foto") || st.includes("biometr")) { + return "Estás en inscripción final (registro, foto y huella). Completa lo solicitado para obtener tu constancia."; + } + + // Constancia generada -> recién aquí mostrar examen + if (st.includes("constancia")) { + return "Ya generaste tu constancia. Prepárate para rendir el examen el 14 de febrero. Debes estar antes de las 9:30 a. m.; un minuto tarde y no podrás ingresar."; + } + + return "Sigue revisando esta sección para ver el siguiente paso."; }); onMounted(() => { diff --git a/front/src/views/postulante/Dashboard.vue b/front/src/views/postulante/Dashboard.vue index 7bbe0eb..b38073e 100644 --- a/front/src/views/postulante/Dashboard.vue +++ b/front/src/views/postulante/Dashboard.vue @@ -8,6 +8,24 @@
Bienvenido, {{ authStore.userName }}
DNI: {{ authStore.userDni || "No registrado" }}
+ + +
+
+ + + {{ esObservado ? "Observado" : "Apto" }} + +
+ +
+ {{ mensaje }} +
+
@@ -30,7 +48,7 @@
- Si tu proceso pide secuencia, también puedes usar la del pago de tu + Si el test pide secuencia, también puedes usar la del pago de tu Carpeta de Postulante (no pagas extra por este test).
@@ -95,7 +113,7 @@ type="primary" block class="actionBtnPrimary" - :disabled="!record.link_preinscripcion" + :disabled="!record.link_preinscripcion || esObservado === true" @click="onApply(record)" > Postular @@ -118,7 +136,7 @@ @@ -219,6 +253,13 @@ onMounted(fetchDashboard); align-items: flex-start; } +.topbarRight { + display: grid; + gap: 8px; + justify-items: end; + text-align: right; +} + .hello { font-size: 28px; font-weight: 900; @@ -233,6 +274,48 @@ onMounted(fetchDashboard); color: #64748b; } +/* estado */ +.statusPill { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-radius: 999px; + font-weight: 900; + font-size: 12px; + border: 1px solid rgba(15, 23, 42, 0.12); + background: rgba(15, 23, 42, 0.03); +} + +.statusDot { + width: 10px; + height: 10px; + border-radius: 999px; + background: currentColor; +} + +.statusOk { + color: #16a34a; + border-color: rgba(22, 163, 74, 0.25); + background: rgba(22, 163, 74, 0.08); +} + +.statusBad { + color: #dc2626; + border-color: rgba(220, 38, 38, 0.25); + background: rgba(220, 38, 38, 0.08); +} + +.statusMsg { + max-width: 340px; + font-size: 12px; + color: #64748b; + line-height: 1.35; +} + +.msgOk { color: #166534; } +.msgBad { color: #7f1d1d; } + /* ====== TEST CARD ====== */ .testBox { border: 1px solid rgba(15, 23, 42, 0.08); @@ -382,6 +465,16 @@ onMounted(fetchDashboard); .testHeadRight { justify-items: stretch; } + + .topbarRight { + width: 100%; + justify-items: start; + text-align: left; + } + + .statusMsg { + max-width: 100%; + } } @media (max-width: 768px) { @@ -405,7 +498,7 @@ onMounted(fetchDashboard); font-size: 22px; } - /* card full width en móvil (pro) */ + /* card full width en móvil */ .testBox, .tableWrap { border-radius: 0; diff --git a/front/src/views/postulante/LoginView.vue b/front/src/views/postulante/LoginView.vue index 0408c24..f658349 100644 --- a/front/src/views/postulante/LoginView.vue +++ b/front/src/views/postulante/LoginView.vue @@ -4,10 +4,9 @@
- +
-
- + @@ -61,30 +68,45 @@ v-model:value="formState.name" size="large" placeholder="Ingrese su nombre completo" + autocomplete="name" > - - + - + + + +
    +
  • Mínimo 8 caracteres
  • +
  • Al menos 1 mayúscula (A-Z)
  • +
  • Al menos 1 minúscula (a-z)
  • +
  • Al menos 1 número (0-9)
  • +
  • Al menos 1 símbolo (@$!%*?&)
  • +
- + - - + + Recordarme - + ¿Olvidó su contraseña? @@ -137,68 +150,57 @@
- -
-
- Universidad Nacional del Altiplano – Puno - - - {{ isRegister ? "Registro de Postulante" : "Portal del Postulante" }} - - - - {{ - isRegister - ? "Crea tu cuenta para participar en el proceso de admisión y acceder a todos los servicios del portal." - : "Ingresa al portal para gestionar tu inscripción, revisar procesos disponibles y rendir un test de referencia." - }} - -
+ + +
+
+ Universidad Nacional del Altiplano – Puno -
-
- {{ isRegister ? "Al registrarte podrás" : "Al ingresar podrás" }} -
- -
- -
- - Rendir un test de referencia. -
-
- - Ver procesos disponibles según tu modalidad. -
- - - -
- - Consultar tu estado de inscripción y seguimiento del proceso. -
- - - -
- - Revisar comunicados oficiales del proceso de admisión. -
-
-
+ + {{ isRegister ? "Registro de Postulante" : "Portal del Postulante" }} + -
- - Plataforma oficial de admisión • Soporte en horario institucional - -
+ + {{ + isRegister + ? "Crea tu cuenta para participar en el proceso de admisión y acceder a los servicios del portal." + : "Ingresa al portal para ver tu estado de inscripción, revisar procesos disponibles y rendir un test de referencia." + }} + +
+ +
+
+ {{ isRegister ? "Al registrarte podrás" : "Al ingresar podrás" }}
- +
+
+ + Rendir un test de referencia. +
+
+ + Ver procesos disponibles. +
+
+ + Consultar tu estado de inscripción y seguimiento del proceso. +
+
+ + Revisar comunicados oficiales del proceso de admisión. +
+
+
+
+ + Plataforma oficial de admisión • Soporte en horario institucional + +
+
+
@@ -221,7 +223,6 @@ const isRegister = ref(false); const rememberMe = ref(false); const loading = ref(false); - const logoSrc = "/logotiny.png"; const logoError = ref(false); @@ -243,6 +244,17 @@ watch(isRegister, () => { formRef.value?.clearValidate(); }); +const onDniInput = () => { + formState.dni = (formState.dni || "").replace(/\D/g, "").slice(0, 8); +}; + +// Requisitos visuales (mismos del backend) +const hasMinLength = computed(() => (formState.password || "").length >= 8); +const hasUpper = computed(() => /[A-Z]/.test(formState.password || "")); +const hasLower = computed(() => /[a-z]/.test(formState.password || "")); +const hasNumber = computed(() => /\d/.test(formState.password || "")); +const hasSpecial = computed(() => /[@$!%*?&]/.test(formState.password || "")); + const rules = computed(() => ({ dni: [ { required: isRegister.value, message: "Ingrese su DNI", trigger: "blur" }, @@ -258,20 +270,36 @@ const rules = computed(() => ({ ], password: [ { required: true, message: "Ingrese su contraseña", trigger: "blur" }, - { min: 6, message: "La contraseña debe tener al menos 6 caracteres", trigger: "blur" }, - ], - password_confirmation: [ - { required: isRegister.value, message: "Confirme su contraseña", trigger: "blur" }, { + // Solo en registro validamos composición; en login no molestamos. validator: (rule, value) => { if (!isRegister.value) return Promise.resolve(); - if (!value) return Promise.reject("Confirme su contraseña"); - if (value !== formState.password) return Promise.reject("Las contraseñas no coinciden"); - return Promise.resolve(); + + const v = value || ""; + const ok = + v.length >= 8 && + /[A-Z]/.test(v) && + /[a-z]/.test(v) && + /\d/.test(v) && + /[@$!%*?&]/.test(v); + + return ok ? Promise.resolve() : Promise.reject("Contraseña inválida"); }, trigger: "blur", }, ], +password_confirmation: [ + { required: isRegister.value, message: "Confirme su contraseña", trigger: "blur" }, + { + validator: (rule, value) => { + if (!isRegister.value) return Promise.resolve(); + if (!value) return Promise.resolve(); // el required ya se encarga + if (value !== formState.password) return Promise.reject("Las contraseñas no coinciden"); + return Promise.resolve(); + }, + trigger: "blur", + }, +], })); const showNotification = (type, message, description = "") => { @@ -288,13 +316,18 @@ const handleForgotPassword = () => { showNotification("info", "Recuperación de contraseña", "Por favor, contacte al administrador del sistema"); }; +const reqStyle = (ok) => ({ + color: ok ? "#16a34a" : "#6b7280", + fontWeight: ok ? "700" : "500", +}); + const handleSubmit = async () => { loading.value = true; try { if (isRegister.value) { const result = await authStore.register({ ...formState }); if (result.success) { - showNotification("success", "¡Registro exitoso!", "Tu cuenta ha sido creada correctamente"); + showNotification("success", "¡Registro exitoso!"); toggleMode(); } else { showNotification("error", "Error en registro", result.error); @@ -377,10 +410,6 @@ checkExistingAuth(); gap: 14px; } -.pane-inner-info { - justify-content: space-between; -} - /* Branding */ .brand { display: flex; @@ -426,10 +455,6 @@ checkExistingAuth(); font-size: 0.95rem; } -.auth-header { - text-align: left; -} - .auth-divider { margin: 6px 0 14px; } @@ -474,29 +499,15 @@ checkExistingAuth(); font-weight: 800; } -.info-top { - text-align: left; -} - .info-tag { border: 0; - background: color-mix(in srgb, var(--ant-colorPrimary) 14%, transparent); + background: rgba(22, 119, 255, 0.12); color: var(--ant-colorPrimary, #1677ff); font-weight: 800; border-radius: 999px; padding: 6px 12px; } -.info-section { - margin-top: 8px; -} - -.info-section-title { - font-weight: 800; - color: var(--ant-colorTextHeading, #111827); - margin: 6px 0 10px; -} - .info-list { margin-top: 10px; display: grid; @@ -525,6 +536,11 @@ checkExistingAuth(); border-top: 1px solid var(--ant-colorBorderSecondary, rgba(0, 0, 0, 0.06)); } +.pwd-req { + margin: 8px 0 0 16px; + font-size: 12px; +} + @media (max-width: 768px) { .auth-pane { padding: 22px; @@ -537,10 +553,4 @@ checkExistingAuth(); min-height: auto; } } - -@supports not (color: color-mix(in srgb, white 50%, black)) { - .info-tag { - background: rgba(22, 119, 255, 0.12); - } -} \ No newline at end of file diff --git a/front/src/views/postulante/MisProcesos.vue b/front/src/views/postulante/MisProcesos.vue index cc2044f..0f36a26 100644 --- a/front/src/views/postulante/MisProcesos.vue +++ b/front/src/views/postulante/MisProcesos.vue @@ -27,7 +27,6 @@ Total {{ procesosFiltrados.length }}
-
@@ -63,7 +62,7 @@
@@ -104,7 +103,7 @@ - + @@ -126,6 +125,10 @@ const columns = [ { title: "Acciones", key: "acciones", width: 160 }, ]; +const emptyMessage = computed(() => { + return "Aún no tienes procesos anteriores registrados. Si participaste recientemente, esta sección puede demorar en actualizarse."; +}); + const obtenerProcesos = async () => { loading.value = true; try { @@ -225,10 +228,6 @@ onMounted(() => { font-size: 18px; } -.search { - border-radius: 12px; -} - .tableWrap { width: 100%; overflow-x: auto; diff --git a/front/src/views/postulante/PortalView.vue b/front/src/views/postulante/PortalView.vue index e82b156..a119f0b 100644 --- a/front/src/views/postulante/PortalView.vue +++ b/front/src/views/postulante/PortalView.vue @@ -124,7 +124,7 @@ --> - Mis Procesos + Mis procesos anteriores