+
-
-
-
-
-
-
-
-
+ :items="stepsItems"
+ />
+
+
+
+
📌 Guía rápida (para no perderte)
+
+
+
+
+
1) ¿Qué etapa está activa hoy?
+
+
+
+ Preinscripción activa (virtual / en línea)
+
+
+
+ Inscripción activa (presencial en Campus Universitario)
+
+
+
+ Hoy es el Examen
+
+
+
+ Hoy salen Resultados
+
+
+
+ Biométrico activo (solo ingresantes)
+
+
+
+ Aún no inicia o ya terminó una etapa. Revisa las fechas del proceso.
+
+
+
+
+ Tip: Si ves “🟢” en una fecha, significa que esa etapa está activa hoy.
+
+
+
+
+
+
2) ¿Qué debo hacer ahora?
+
+
+
+
+
+
+ Iniciar Preinscripción
+
+
+
+ Ver Reglamento
+
+
+
+ Ver Resultados
+
+
+
+
+
+
+
+ 🏫 Importante: La Inscripción se realiza de forma presencial en el
+ Campus Universitario. Lleva tu DNI y los requisitos solicitados.
+
+
+
+
- Fechas referenciales. Verifica el cronograma oficial de la Dirección de Admisión
+ Cargando proceso...
@@ -33,22 +115,254 @@
@@ -85,7 +399,6 @@ onUnmounted(() => {
line-height: 1.4;
}
-
.process-card {
border: 1px solid #e5e7eb;
border-radius: 14px;
@@ -94,32 +407,30 @@ onUnmounted(() => {
background: #fff;
}
-
.modern-steps {
padding: 8px 8px;
}
.modern-steps :deep(.ant-steps-item-title) {
- white-space: normal !important;
- overflow: visible !important;
- text-overflow: clip !important;
+ white-space: normal !important;
+ overflow: visible !important;
+ text-overflow: clip !important;
max-width: none !important;
}
.modern-steps :deep(.ant-steps-item-content) {
- min-width: 0;
+ min-width: 0;
width: 100%;
}
.modern-steps :deep(.ant-steps-item-container) {
- align-items: flex-start;
+ align-items: flex-start;
}
.modern-steps :deep(.ant-steps-item) {
- flex: 1 1 0;
+ flex: 1 1 0;
}
-
.modern-steps :deep(.ant-steps-item-title) {
font-size: 0.95rem;
font-weight: 700;
@@ -134,7 +445,6 @@ onUnmounted(() => {
line-height: 1.25;
}
-
.modern-steps :deep(.ant-steps-item-icon) {
width: 30px;
height: 30px;
@@ -142,19 +452,16 @@ onUnmounted(() => {
font-size: 13px;
}
-
.modern-steps :deep(.ant-steps-item-tail::after) {
height: 2px;
background: #dfe6e9;
}
-
.modern-steps :deep(.ant-steps-item-process .ant-steps-item-icon) {
background-color: #1e3a8a;
border-color: #1e3a8a;
}
-
.modern-steps :deep(.ant-steps-item-finish .ant-steps-item-icon > .ant-steps-icon) {
color: #1e3a8a;
}
@@ -162,7 +469,6 @@ onUnmounted(() => {
border-color: #1e3a8a;
}
-
.process-note {
display: flex;
align-items: center;
@@ -180,7 +486,107 @@ onUnmounted(() => {
flex-shrink: 0;
}
+/* ====== Guía rápida ====== */
+.help-box {
+ margin-top: 14px;
+ border-top: 1px dashed #e5e7eb;
+ padding-top: 12px;
+}
+
+.help-title {
+ font-weight: 700;
+ color: #1e3a8a;
+ margin-bottom: 10px;
+}
+
+.help-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+}
+
+.help-item {
+ border: 1px solid #eef2ff;
+ background: #fbfcff;
+ border-radius: 12px;
+ padding: 12px;
+}
+
+.help-label {
+ font-weight: 700;
+ color: #2c3e50;
+ margin-bottom: 8px;
+}
+
+.tiny-hint {
+ margin-top: 10px;
+ font-size: 0.86rem;
+ color: #6b7280;
+}
+
+.badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+.badge {
+ font-size: 0.82rem;
+ padding: 6px 10px;
+ border-radius: 999px;
+ font-weight: 700;
+}
+
+.badge-blue {
+ background: rgba(30, 58, 138, 0.08);
+ color: #1e3a8a;
+ border: 1px solid rgba(30, 58, 138, 0.18);
+}
+
+.badge-green {
+ background: rgba(16, 185, 129, 0.08);
+ color: #047857;
+ border: 1px solid rgba(16, 185, 129, 0.18);
+}
+
+.badge-orange {
+ background: rgba(245, 158, 11, 0.10);
+ color: #92400e;
+ border: 1px solid rgba(245, 158, 11, 0.22);
+}
+
+.badge-gray {
+ background: rgba(107, 114, 128, 0.08);
+ color: #374151;
+ border: 1px solid rgba(107, 114, 128, 0.18);
+}
+
+.help-list {
+ margin: 0;
+ padding-left: 18px;
+ color: #4b5563;
+ line-height: 1.55;
+}
+
+.help-actions {
+ margin-top: 10px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+/* Nota campus */
+.campus-note {
+ margin-top: 12px;
+ padding: 10px 12px;
+ border-radius: 12px;
+ background: #f8fafc;
+ border: 1px solid #e5e7eb;
+ color: #374151;
+ line-height: 1.5;
+}
+
+/* Responsive */
@media (max-width: 992px) {
.section-title {
font-size: 1.85rem;
@@ -190,28 +596,26 @@ onUnmounted(() => {
}
}
-
@media (max-width: 768px) {
.process-section {
padding: 24px 0;
}
-
.section-title {
font-size: 1.55rem;
}
-
.process-card {
padding: 12px 10px 10px;
}
-
.modern-steps {
padding: 4px 4px;
}
-
.modern-steps :deep(.ant-steps-item-icon) {
width: 28px;
height: 28px;
line-height: 28px;
}
+ .help-grid {
+ grid-template-columns: 1fr;
+ }
}
-
\ No newline at end of file
+
diff --git a/front/src/components/WebPageSections/navbarcontent/Cepreuna.vue b/front/src/components/WebPageSections/navbarcontent/Cepreuna.vue
new file mode 100644
index 0000000..ccbe066
--- /dev/null
+++ b/front/src/components/WebPageSections/navbarcontent/Cepreuna.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+ CEPREUNA
+
+
+
+
+
+
+
+
+ Evaluación
+
+ El postulante rinde un examen de conocimientos con contenidos alineados al perfil del ingresante
+ establecido por la UNA-Puno, en la fecha indicada en el cronograma de admisión.
+
+
+
+
+ Asignación de vacantes
+
+ Las vacantes se cubren de acuerdo con el puntaje alcanzado, hasta completar el número ofertado
+ por los programas de estudio, según lo dispuesto por el reglamento.
+
+
+
+
+ Requisitos y documentos
+
+
+ Presentar la constancia de no adeudar al CEPREUNA con la debida anticipación.
+
+
+ Presentar los documentos exigidos por el reglamento para esta modalidad (según requisitos generales).
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front/src/components/WebPageSections/navbarcontent/Extraordinario.vue b/front/src/components/WebPageSections/navbarcontent/Extraordinario.vue
new file mode 100644
index 0000000..f76a892
--- /dev/null
+++ b/front/src/components/WebPageSections/navbarcontent/Extraordinario.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+ Extraordinario
+
+
+
+
+ Evaluación
+
+ El postulante rinde un examen de conocimientos con temas y contenidos alineados al perfil del ingresante,
+ en la fecha prevista en el cronograma de admisión.
+
+
+
+
+ Asignación de vacantes
+
+ Las vacantes se cubren de acuerdo con el puntaje alcanzado hasta completar el número ofertado por los
+ programas de estudio, conforme a lo establecido en el reglamento.
+
+
+
+
+
+ Formas de postulación
+
+
+
+
+ Dirigido a estudiantes que obtuvieron los primeros lugares del orden de mérito en secundaria, con vigencia
+ definida por el reglamento; incluye egresados del COAR según corresponda.
+
+
+
+ Comprobantes de pago por admisión y carpeta de postulante.
+ Documento de identidad (original y copia).
+ Examen médico y aptitud vocacional solo para programas que lo exigen.
+ Solicitud generada tras la preinscripción virtual.
+ Certificados que acrediten estudios y condición de mérito según corresponda.
+
+
+
+
+
+ Comprobantes de pago por admisión y carpeta.
+ Documento de identidad (original y copia).
+ Solicitud generada tras la preinscripción virtual.
+ Grado o título certificado por la institución de origen.
+ Para extranjeros: legalizaciones requeridas según normativa.
+ Constancias de institución de origen o verificación según corresponda.
+
+
+
+
+
+ Comprobantes de pago por admisión y carpeta.
+ Documento de identidad (original y copia).
+ Solicitud indicando programa de origen y programa al que postula, según afinidad.
+ Historial académico y constancias de matrícula según requisitos establecidos.
+
+
+
+
+
+ Comprobantes de pago por admisión y carpeta.
+ Documento de identidad (DNI / carné de extranjería / pasaporte) según corresponda.
+ Solicitud de postulación al mismo programa de estudio.
+ Certificados de estudios visados; en internacional, requisitos legales adicionales.
+ Constancia de matrícula vigente del semestre anterior o similar.
+
+
+
+
+
+ Comprobantes de pago por admisión y carpeta.
+ Documento de identidad (original y copia).
+ Resolución y récord deportivo documentado conforme a la normativa aplicable.
+ Certificado de estudios secundarios.
+
+
+
+
+
+ Solicitud generada tras la preinscripción virtual.
+ Documento de identidad (original y copia).
+ Constancias de registro correspondientes (RUV/REBRED u otras).
+ Examen médico y aptitud vocacional solo para programas que lo exigen.
+ Certificado de estudios secundarios.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front/src/components/WebPageSections/navbarcontent/General.vue b/front/src/components/WebPageSections/navbarcontent/General.vue
new file mode 100644
index 0000000..57a62d4
--- /dev/null
+++ b/front/src/components/WebPageSections/navbarcontent/General.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+ General
+
+
+
+
+
+
+
+
+ A quién está dirigido
+
+ Dirigido a estudiantes egresados que hayan concluido educación secundaria en EBR, EBA y COAR.
+
+
+
+
+ Evaluación
+
+ Examen de conocimientos basado en contenidos alineados al perfil del ingresante, según el cronograma de admisión.
+
+
+
+
+ Asignación de vacantes
+
+ Se asignan por puntaje hasta completar el número ofertado por los programas de estudio, conforme al reglamento.
+
+
+
+
+ Documentación
+
+ El postulante debe presentar los documentos exigidos por el reglamento para esta modalidad.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front/src/components/WebPageSections/navbarcontent/Resultados.vue b/front/src/components/WebPageSections/navbarcontent/Resultados.vue
new file mode 100644
index 0000000..72c9f90
--- /dev/null
+++ b/front/src/components/WebPageSections/navbarcontent/Resultados.vue
@@ -0,0 +1,411 @@
+
+
+
+
+
+
+
+
+
+
+
+ Resultados
+
+
+
+ {{ errorMsg }}
+
+
+
+
+
+ Aún no hay resultados disponibles para mostrar.
+
+
+
+
+
+ Resultados disponibles organizados por año:
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front/src/components/nabvar.vue b/front/src/components/nabvar.vue
index 865fb09..c880af9 100644
--- a/front/src/components/nabvar.vue
+++ b/front/src/components/nabvar.vue
@@ -149,9 +149,9 @@ const navItems = computed(() => [
key: "modalidades",
label: "Modalidades",
children: [
- { key: "ordinario", label: "Ordinario" },
+ { key: "cepreuna", label: "Cepreuna" },
{ key: "extraordinario", label: "Extraordinario" },
- { key: "sedes", label: "Sedes" },
+ { key: "general", label: "General" },
],
},
{ key: "resultados", label: "Resultados" },
@@ -171,9 +171,9 @@ const routesByKey = {
sociales: "/programas/sociales",
procesos: "/procesos",
modalidades: "/modalidades",
- ordinario: "/modalidades/ordinario",
+ cepreuna: "/modalidades/cepreuna",
extraordinario: "/modalidades/extraordinario",
- sedes: "/modalidades/sedes",
+ general: "/modalidades/general",
resultados: "/resultados",
}
diff --git a/front/src/router/index.js b/front/src/router/index.js
index d5fb3ac..60198d2 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -1,16 +1,16 @@
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/Login.vue'
-import Hello from '../components/WebPage.vue'
+import WebPage from '../components/WebPage.vue'
import { useUserStore } from '../store/user'
import { useAuthStore as usePostulanteStore } from '../store/postulanteStore'
const routes = [
- { path: '/', component: Hello },
+ { path: '/', component: WebPage },
{ path: '/login', component: Login, meta: { guest: true } },
-
+
{
path: '/login-postulante',
name: 'login-postulante',
@@ -18,6 +18,29 @@ const routes = [
meta: { guest: true },
},
+ {
+ path: '/resultados',
+ name: 'Resultados',
+ component: () => import('../components/WebPageSections/navbarcontent/Resultados.vue')
+ },
+ {
+ path: '/modalidades/cepreuna',
+ name: 'cepreuna',
+ component: () => import('../components/WebPageSections/navbarcontent/Cepreuna.vue')
+ },
+
+ {
+ path: '/modalidades/extraordinario',
+ name: 'extraordinario',
+ component: () => import('../components/WebPageSections/navbarcontent/Extraordinario.vue')
+ },
+ {
+ path: '/modalidades/general',
+ name: 'general',
+ component: () => import('../components/WebPageSections/navbarcontent/General.vue')
+ },
+
+
{
path: '/portal-postulante',
@@ -138,7 +161,14 @@ const routes = [
path: '/admin/dashboard/lista-postulantes',
name: 'PostulantesList',
component: () => import('../views/administrador/estudiantes/ListPostulantes.vue')
+ },
+
+ {
+ path: '/admin/dashboard/noticias',
+ name: 'NoticiasAdmisionList',
+ component: () => import('../views/administrador/procesoadmision/NoticiasAdmin.vue')
}
+
]
},
diff --git a/front/src/store/examen.store.js b/front/src/store/examen.store.js
index 28c0c3c..1a3314e 100644
--- a/front/src/store/examen.store.js
+++ b/front/src/store/examen.store.js
@@ -1,7 +1,6 @@
import { defineStore } from 'pinia'
import api from '../axiosPostulante'
-
export const useExamenStore = defineStore('examenStore', {
state: () => ({
procesos: [],
@@ -9,33 +8,32 @@ export const useExamenStore = defineStore('examenStore', {
examenActual: null,
preguntas: [],
cargando: false,
+ calificando: false, // ✅ nuevo
+ resultado: null, // ✅ nuevo (para guardar resultados_examenes)
error: null,
}),
+
actions: {
- async fetchProcesos() {
- try {
- this.cargando = true
- const { data } = await api.get('/examen/procesos')
-
- // ✅ normaliza
- this.procesos = (data || []).map(p => ({
- ...p,
- requiere_pago: p.requiere_pago === 1 || p.requiere_pago === '1' || p.requiere_pago === true
- }))
- } catch (e) {
- this.error = e.response?.data?.message || e.message
- } finally {
- this.cargando = false
- }
- },
+ async fetchProcesos() {
+ try {
+ this.cargando = true
+ const { data } = await api.get('/examen/procesos')
+ this.procesos = (data || []).map(p => ({
+ ...p,
+ requiere_pago: p.requiere_pago === 1 || p.requiere_pago === '1' || p.requiere_pago === true
+ }))
+ } catch (e) {
+ this.error = e.response?.data?.message || e.message
+ } finally {
+ this.cargando = false
+ }
+ },
async fetchAreas(proceso_id) {
try {
this.cargando = true
- const { data } = await api.get('/examen/areas', {
- params: { proceso_id }
- })
+ const { data } = await api.get('/examen/areas', { params: { proceso_id } })
this.areas = data
} catch (e) {
this.error = e.response?.data?.message || e.message
@@ -59,7 +57,6 @@ export const useExamenStore = defineStore('examenStore', {
}
},
-
async fetchExamenActual() {
try {
this.cargando = true
@@ -100,36 +97,78 @@ export const useExamenStore = defineStore('examenStore', {
}
},
+ async responderPregunta(preguntaId, respuesta) {
+ try {
+ const { data } = await api.post(`/examen/pregunta/${preguntaId}/responder`, { respuesta })
-async responderPregunta(preguntaId, respuesta) {
- try {
- const { data } = await api.post(
- `/examen/pregunta/${preguntaId}/responder`,
- { respuesta }
- )
-
- const index = this.preguntas.findIndex(p => p.id === preguntaId)
-
- if (index !== -1 && data.success) {
- this.preguntas[index].respuesta = respuesta
- this.preguntas[index].es_correcta = data.correcta // 1, 0 o 2
- this.preguntas[index].puntaje = data.puntaje
- }
+ const index = this.preguntas.findIndex(p => p.id === preguntaId)
- return data
+ if (index !== -1 && data.success) {
+ this.preguntas[index].respuesta = respuesta
+ this.preguntas[index].es_correcta = data.correcta // 1, 0 o 2
+ this.preguntas[index].puntaje = data.puntaje
+ }
- } catch (e) {
- this.error = e.response?.data?.message || e.message
- return { success: false, message: this.error }
- }
-},
+ return data
+ } catch (e) {
+ this.error = e.response?.data?.message || e.message
+ return { success: false, message: this.error }
+ }
+ },
+ // ✅ NUEVO: Calificar examen
+ async calificarExamen(examenId) {
+ try {
+ this.error = null
+ this.calificando = true
+
+ const { data } = await api.post(`/examen/${examenId}/calificar`)
+
+ if (data?.success) {
+ this.resultado = {
+ examen_id: data.examen_id,
+ proceso_id: data.proceso_id,
+ total_puntos: data.total_puntos,
+ total_correctas: data.total_correctas,
+ total_incorrectas: data.total_incorrectas,
+ total_nulas: data.total_nulas,
+ porcentaje_correctas: data.porcentaje_correctas,
+ calificacion_sobre_20: data.calificacion_sobre_20,
+ orden_merito: data.orden_merito,
+ correctas_por_curso: data.correctas_por_curso,
+ incorrectas_por_curso: data.incorrectas_por_curso,
+ preguntas_totales_por_curso: data.preguntas_totales_por_curso,
+ }
+
+ return data
+ }
+
+ this.error = data?.mensaje || data?.message || 'No se pudo calificar el examen.'
+ return { success: false, message: this.error }
+ } catch (e) {
+ const msg = e.response?.data?.mensaje || e.response?.data?.message || e.message
+ this.error = msg
+ return { success: false, message: msg, status: e.response?.status }
+ } finally {
+ this.calificando = false
+ }
+ },
async finalizarExamen(examenId) {
try {
const { data } = await api.post(`/examen/${examenId}/finalizar`)
- this.examenActual = null
- this.preguntas = []
+
+ if (data?.success) {
+ if (this.examenActual) {
+ this.examenActual.estado = 'finalizado'
+ this.examenActual.hora_fin = new Date().toISOString()
+ }
+
+ this.examenActual = null
+ this.preguntas = []
+ this.error = null
+ }
+
return data
} catch (e) {
this.error = e.response?.data?.message || e.message
@@ -143,6 +182,8 @@ async responderPregunta(preguntaId, respuesta) {
this.examenActual = null
this.preguntas = []
this.cargando = false
+ this.calificando = false // ✅ nuevo
+ this.resultado = null // ✅ nuevo
this.error = null
}
}
diff --git a/front/src/store/noticiasPublicas.store.js b/front/src/store/noticiasPublicas.store.js
new file mode 100644
index 0000000..a551a48
--- /dev/null
+++ b/front/src/store/noticiasPublicas.store.js
@@ -0,0 +1,50 @@
+// src/store/noticiasPublicas.store.js
+import { defineStore } from "pinia"
+import api from "../axiosPostulante"
+
+export const useNoticiasPublicasStore = defineStore("noticiasPublicas", {
+ state: () => ({
+ noticias: [],
+ noticiaActual: null,
+ loading: false,
+ loadingOne: false,
+ error: null,
+ }),
+
+ actions: {
+ async fetchNoticias() {
+ this.loading = true
+ this.error = null
+ try {
+ // ✅ usa TU ruta real
+ const res = await api.get("/noticias", {
+ params: { publicado: true, per_page: 9999 },
+ })
+ this.noticias = res.data?.data ?? []
+ } catch (err) {
+ this.error = err.response?.data?.message || "Error al cargar noticias"
+ this.noticias = []
+ } finally {
+ this.loading = false
+ }
+ },
+
+ async fetchNoticia(identifier) {
+ this.loadingOne = true
+ this.error = null
+ try {
+ const res = await api.get(`/noticias/${identifier}`)
+ this.noticiaActual = res.data?.data ?? null
+ } catch (err) {
+ this.error = err.response?.data?.message || "Error al cargar la noticia"
+ this.noticiaActual = null
+ } finally {
+ this.loadingOne = false
+ }
+ },
+
+ clearNoticiaActual() {
+ this.noticiaActual = null
+ },
+ },
+})
diff --git a/front/src/store/noticiasStore.js b/front/src/store/noticiasStore.js
new file mode 100644
index 0000000..f7e63a1
--- /dev/null
+++ b/front/src/store/noticiasStore.js
@@ -0,0 +1,207 @@
+// src/store/noticiasStore.js
+import { defineStore } from "pinia"
+import api from "../axios" // <-- cambia a tu axios (admin) si tienes otro
+
+export const useNoticiasStore = defineStore("noticias", {
+ state: () => ({
+ noticias: [],
+ noticia: null,
+
+ loading: false,
+ saving: false,
+ deleting: false,
+
+ error: null,
+
+ // paginación (si tu back manda meta)
+ meta: {
+ current_page: 1,
+ last_page: 1,
+ per_page: 9,
+ total: 0,
+ },
+
+ // filtros
+ filters: {
+ publicado: null, // true/false/null
+ categoria: "",
+ q: "",
+ per_page: 9,
+ page: 1,
+ },
+ }),
+
+ getters: {
+ // para el público: solo publicadas (si ya las filtras en backend, esto es opcional)
+ publicadas: (state) => state.noticias.filter((n) => n.publicado),
+
+ // para ordenar en frontend (opcional)
+ ordenadas: (state) =>
+ [...state.noticias].sort((a, b) => {
+ const da = a.fecha_publicacion ? new Date(a.fecha_publicacion).getTime() : 0
+ const db = b.fecha_publicacion ? new Date(b.fecha_publicacion).getTime() : 0
+ return db - da
+ }),
+ },
+
+ actions: {
+ // ========= LISTAR =========
+ async cargarNoticias(extraParams = {}) {
+ this.loading = true
+ this.error = null
+
+ try {
+ const params = {
+ per_page: this.filters.per_page,
+ page: this.filters.page,
+ ...extraParams,
+ }
+
+ if (this.filters.publicado !== null && this.filters.publicado !== "") {
+ params.publicado = this.filters.publicado ? 1 : 0
+ }
+ if (this.filters.categoria) params.categoria = this.filters.categoria
+ if (this.filters.q) params.q = this.filters.q
+
+ // Ruta agrupada:
+ // GET /api/administracion/noticias
+ const res = await api.get("/admin/noticias", { params })
+
+ this.noticias = res.data?.data ?? []
+ this.meta = res.data?.meta ?? this.meta
+ } catch (err) {
+ this.error = err.response?.data?.message || "Error al cargar noticias"
+ console.error(err)
+ } finally {
+ this.loading = false
+ }
+ },
+
+ // ========= VER 1 =========
+ async cargarNoticia(id) {
+ this.loading = true
+ this.error = null
+ try {
+ const res = await api.get(`/admin/noticias/${id}`)
+ this.noticia = res.data?.data ?? res.data
+ } catch (err) {
+ this.error = err.response?.data?.message || "Error al cargar la noticia"
+ console.error(err)
+ } finally {
+ this.loading = false
+ }
+ },
+
+ // ========= CREAR =========
+ // payload: { titulo, descripcion_corta, contenido, categoria, tag_color, fecha_publicacion, publicado, destacado, orden, link_url, link_texto, imagen(File) }
+ async crearNoticia(payload) {
+ this.saving = true
+ this.error = null
+
+ try {
+ const form = this._toFormData(payload)
+
+ const res = await api.post("/admin/noticias", form, {
+ headers: { "Content-Type": "multipart/form-data" },
+ })
+
+ const noticia = res.data?.data ?? null
+ if (noticia) {
+ // opcional: agrega arriba
+ this.noticias = [noticia, ...this.noticias]
+ }
+ return noticia
+ } catch (err) {
+ this.error = err.response?.data?.message || "Error al crear noticia"
+ console.error(err)
+ throw err
+ } finally {
+ this.saving = false
+ }
+ },
+
+ // ========= ACTUALIZAR =========
+ async actualizarNoticia(id, payload) {
+ this.saving = true
+ this.error = null
+
+ try {
+ // Si hay imagen (File), conviene multipart
+ const hasFile = payload?.imagen instanceof File
+ let res
+
+ if (hasFile) {
+ const form = this._toFormData(payload)
+
+ // Si tu ruta es PUT, axios con multipart PUT funciona.
+ res = await api.put(`/admin/noticias/${id}`, form, {
+ headers: { "Content-Type": "multipart/form-data" },
+ })
+ } else {
+ // JSON normal
+ res = await api.put(`/administracion/noticias/${id}`, payload)
+ }
+
+ const updated = res.data?.data ?? null
+
+ if (updated) {
+ this.noticias = this.noticias.map((n) => (n.id === id ? updated : n))
+ if (this.noticia?.id === id) this.noticia = updated
+ }
+
+ return updated
+ } catch (err) {
+ this.error = err.response?.data?.message || "Error al actualizar noticia"
+ console.error(err)
+ throw err
+ } finally {
+ this.saving = false
+ }
+ },
+
+ // ========= ELIMINAR =========
+ async eliminarNoticia(id) {
+ this.deleting = true
+ this.error = null
+ try {
+ await api.delete(`/admin/noticias/${id}`)
+ this.noticias = this.noticias.filter((n) => n.id !== id)
+ if (this.noticia?.id === id) this.noticia = null
+ return true
+ } catch (err) {
+ this.error = err.response?.data?.message || "Error al eliminar noticia"
+ console.error(err)
+ throw err
+ } finally {
+ this.deleting = false
+ }
+ },
+
+ // ========= Helpers =========
+ setFiltro(key, value) {
+ this.filters[key] = value
+ },
+
+ resetFiltros() {
+ this.filters = { publicado: null, categoria: "", q: "", per_page: 9, page: 1 }
+ },
+
+ _toFormData(payload = {}) {
+ const form = new FormData()
+
+ Object.entries(payload).forEach(([k, v]) => {
+ if (v === undefined || v === null) return
+
+ // booleans como 1/0 (Laravel feliz)
+ if (typeof v === "boolean") {
+ form.append(k, v ? "1" : "0")
+ return
+ }
+
+ form.append(k, v)
+ })
+
+ return form
+ },
+ },
+})
diff --git a/front/src/store/web.js b/front/src/store/web.js
new file mode 100644
index 0000000..7899991
--- /dev/null
+++ b/front/src/store/web.js
@@ -0,0 +1,41 @@
+// src/store/web.js
+import { defineStore } from "pinia"
+import api from "../axiosPostulante"
+
+export const useWebAdmisionStore = defineStore("procesoAdmision", {
+ state: () => ({
+ procesos: [],
+ loading: false,
+ error: null,
+ }),
+
+ getters: {
+ // Si hay uno VIGENTE, úsalo como principal; si no, usa el primero.
+ procesoPrincipal: (state) => {
+ if (!state.procesos?.length) return null
+ return state.procesos.find((p) => p.estado === "publicado") ?? state.procesos[0]
+ },
+
+ // Por si lo necesitas después
+ ultimoProceso: (state) => {
+ return state.procesos?.length ? state.procesos[0] : null
+ },
+ },
+
+ actions: {
+ async cargarProcesos() {
+ this.loading = true
+ this.error = null
+
+ try {
+ const response = await api.get("/procesos-admision")
+ this.procesos = response.data?.data ?? response.data ?? []
+ } catch (err) {
+ this.error = err.response?.data?.message || "Error al cargar procesos"
+ console.error(err)
+ } finally {
+ this.loading = false
+ }
+ },
+ },
+})
diff --git a/front/src/views/administrador/cursos/MarkdownLatex.vue b/front/src/views/administrador/cursos/MarkdownLatex.vue
index a169ddf..715da1c 100644
--- a/front/src/views/administrador/cursos/MarkdownLatex.vue
+++ b/front/src/views/administrador/cursos/MarkdownLatex.vue
@@ -121,11 +121,23 @@ onMounted(() => {
list-style-type: decimal;
}
-/* Fórmulas centradas */
.markdown-content :deep(.katex-display) {
- margin: 1em 0;
+ margin: 0.75em 0;
text-align: center;
- overflow-x: auto;
+
+ overflow-x: auto; /* si es largo, que haga scroll horizontal */
+ overflow-y: hidden; /* IMPORTANTE: no scroll vertical -> no flechas */
+}
+/* Quitar botones/flechas del scrollbar (Chrome/Edge) */
+.markdown-content :deep(.katex-display::-webkit-scrollbar-button) {
+ display: none;
+}
+.markdown-content :deep(.katex-display::-webkit-scrollbar) {
+ height: 0px; /* Chrome/Edge */
+}
+/* Si quieres ocultar completamente la barra horizontal (opcional) */
+.markdown-content :deep(.katex-display) {
+ scrollbar-width: none; /* Firefox */
}
.markdown-content :deep(.latex-error) {
diff --git a/front/src/views/administrador/cursos/PreguntasCursoView.vue b/front/src/views/administrador/cursos/PreguntasCursoView.vue
index 82496f8..d030099 100644
--- a/front/src/views/administrador/cursos/PreguntasCursoView.vue
+++ b/front/src/views/administrador/cursos/PreguntasCursoView.vue
@@ -1,12 +1,13 @@
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
@@ -76,54 +78,57 @@
-
-
-
-
-
-
- Todas las dificultades
- Fácil
- Media
- Difícil
-
-
-
- Todos los estados
- Activo
- Inactivo
-
-
-
-
-
-
- Limpiar filtros
-
+
+
+
+
+
+
+
+ Todas las dificultades
+ Fácil
+ Media
+ Difícil
+
+
+
+ Todos los estados
+ Activo
+ Inactivo
+
+
+
+
+
+
+ Limpiar filtros
+
+
-
+
-
+
-
@@ -150,11 +157,19 @@
/>
-
+
-
+
+
+
+
+
{{ formatDate(record.created_at) }}
@@ -162,33 +177,31 @@
-
-
- Ver
-
-
- Editar
-
-
- Eliminar
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -203,23 +216,19 @@
@cancel="handleModalPreguntaCancel"
width="1200px"
class="pregunta-modal"
+ destroy-on-close
>
-
-
-
+
+
+
Enunciado de la Pregunta
-
- Editor
Markdown & LaTeX
+
+
-
- Tips: Usa **negrita**, *cursiva*, $$fórmulas$$, `código`
-
+ Tips: **negrita**, *cursiva*, $$fórmulas$$, `código`
+
+
@@ -263,10 +274,8 @@
-
-
+
+
-
-
-
+
+
Imágenes existentes:
-
+
-
-
-
+
+
+
+
+
@@ -342,48 +356,79 @@
-
-
+
+
Opciones de Respuesta
- (Mínimo 2 opciones, selecciona la correcta)
+ (Mínimo 2, marca la correcta)
-
-