last_updated

main
elmer-20 2 months ago
parent 7a528bd7bf
commit 618d9b4bb7

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Administracion; namespace App\Http\Controllers\Administracion;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Noticia; use App\Models\Noticia;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@ -18,13 +17,12 @@ class NoticiaController extends Controller
$query = Noticia::query(); $query = Noticia::query();
// filtros opcionales
if ($request->filled('publicado')) { if ($request->filled('publicado')) {
$query->where('publicado', $request->boolean('publicado')); $query->where('publicado', $request->boolean('publicado'));
} }
if ($request->filled('categoria')) { if ($request->filled('categoria')) {
$query->where('categoria', $request->string('categoria')); $query->where('categoria', (string) $request->get('categoria'));
} }
if ($request->filled('q')) { if ($request->filled('q')) {
@ -44,7 +42,7 @@ class NoticiaController extends Controller
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'data' => $data->items(), 'data' => $data->items(), // incluye imagen_url por el accessor/appends
'meta' => [ 'meta' => [
'current_page' => $data->currentPage(), 'current_page' => $data->currentPage(),
'last_page' => $data->lastPage(), 'last_page' => $data->lastPage(),
@ -59,22 +57,22 @@ class NoticiaController extends Controller
{ {
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'data' => $noticia, 'data' => $noticia, // incluye imagen_url por el accessor/appends
]); ]);
} }
// GET /api/noticias/{noticia}
public function showPublic(Noticia $noticia)
{
abort_unless($noticia->publicado, 404);
return response()->json([ // GET /api/noticias-publicas/{noticia} (o la ruta que uses)
'success' => true, public function showPublic(Noticia $noticia)
'data' => $noticia, {
]); abort_unless($noticia->publicado, 404);
}
return response()->json([
'success' => true,
'data' => $noticia,
]);
}
// POST /api/noticias (multipart/form-data si viene imagen) // POST /api/noticias (multipart/form-data si viene imagen)
public function store(Request $request) public function store(Request $request)
{ {
$data = $request->validate([ $data = $request->validate([
@ -84,9 +82,12 @@ public function showPublic(Noticia $noticia)
'contenido' => ['nullable', 'string'], 'contenido' => ['nullable', 'string'],
'categoria' => ['nullable', 'string', 'max:80'], 'categoria' => ['nullable', 'string', 'max:80'],
'tag_color' => ['nullable', 'string', 'max:30'], 'tag_color' => ['nullable', 'string', 'max:30'],
// ✅ dos formas de imagen
'imagen' => ['nullable', 'file', 'mimes:jpg,jpeg,png,webp', 'max:4096'], 'imagen' => ['nullable', 'file', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
'imagen_path' => ['nullable', 'string', 'max:255'], 'imagen_url' => ['nullable', 'url', 'max:600'],
'link_url' => ['nullable', 'string', 'max:600'],
'link_url' => ['nullable', 'url', 'max:600'],
'link_texto' => ['nullable', 'string', 'max:120'], 'link_texto' => ['nullable', 'string', 'max:120'],
'fecha_publicacion' => ['nullable', 'date'], 'fecha_publicacion' => ['nullable', 'date'],
'publicado' => ['nullable', 'boolean'], 'publicado' => ['nullable', 'boolean'],
@ -94,15 +95,19 @@ public function showPublic(Noticia $noticia)
'orden' => ['nullable', 'integer'], 'orden' => ['nullable', 'integer'],
]); ]);
// slug por defecto // slug por defecto (igual tu modelo lo genera, pero aquí lo dejamos por consistencia)
if (empty($data['slug'])) { if (empty($data['slug'])) {
$data['slug'] = Str::slug($data['titulo']); $data['slug'] = Str::slug($data['titulo']);
} }
// subir imagen si viene // si viene archivo, manda a storage y prioriza archivo
if ($request->hasFile('imagen')) { if ($request->hasFile('imagen')) {
$path = $request->file('imagen')->store('noticias', 'public'); $path = $request->file('imagen')->store('noticias', 'public');
$data['imagen_path'] = $path; $data['imagen_path'] = $path;
$data['imagen_url'] = null; // ✅ evita conflicto con url externa
} else {
// si viene imagen_url externa, no debe haber imagen_path
$data['imagen_path'] = null;
} }
// si publican sin fecha, poner ahora // si publican sin fecha, poner ahora
@ -114,7 +119,7 @@ public function showPublic(Noticia $noticia)
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'data' => $noticia, 'data' => $noticia->fresh(),
], 201); ], 201);
} }
@ -128,9 +133,12 @@ public function showPublic(Noticia $noticia)
'contenido' => ['sometimes', 'nullable', 'string'], 'contenido' => ['sometimes', 'nullable', 'string'],
'categoria' => ['sometimes', 'nullable', 'string', 'max:80'], 'categoria' => ['sometimes', 'nullable', 'string', 'max:80'],
'tag_color' => ['sometimes', 'nullable', 'string', 'max:30'], 'tag_color' => ['sometimes', 'nullable', 'string', 'max:30'],
// ✅ dos formas de imagen
'imagen' => ['sometimes', 'nullable', 'file', 'mimes:jpg,jpeg,png,webp', 'max:4096'], 'imagen' => ['sometimes', 'nullable', 'file', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
'imagen_path' => ['sometimes', 'nullable', 'string', 'max:255'], 'imagen_url' => ['sometimes', 'nullable', 'url', 'max:600'],
'link_url' => ['sometimes', 'nullable', 'string', 'max:600'],
'link_url' => ['sometimes', 'nullable', 'url', 'max:600'],
'link_texto' => ['sometimes', 'nullable', 'string', 'max:120'], 'link_texto' => ['sometimes', 'nullable', 'string', 'max:120'],
'fecha_publicacion' => ['sometimes', 'nullable', 'date'], 'fecha_publicacion' => ['sometimes', 'nullable', 'date'],
'publicado' => ['sometimes', 'boolean'], 'publicado' => ['sometimes', 'boolean'],
@ -138,17 +146,36 @@ public function showPublic(Noticia $noticia)
'orden' => ['sometimes', 'integer'], 'orden' => ['sometimes', 'integer'],
]); ]);
// si llega imagen, reemplazar // Si llega imagen archivo, reemplaza la anterior y limpia imagen_url externa
if ($request->hasFile('imagen')) { if ($request->hasFile('imagen')) {
if ($noticia->imagen_path && Storage::disk('public')->exists($noticia->imagen_path)) { if ($noticia->imagen_path && Storage::disk('public')->exists($noticia->imagen_path)) {
Storage::disk('public')->delete($noticia->imagen_path); Storage::disk('public')->delete($noticia->imagen_path);
} }
$path = $request->file('imagen')->store('noticias', 'public'); $path = $request->file('imagen')->store('noticias', 'public');
$data['imagen_path'] = $path; $data['imagen_path'] = $path;
$data['imagen_url'] = null; // ✅ prioridad archivo
} }
// Si llega imagen_url (externa), borra la imagen física anterior y limpia imagen_path
if (array_key_exists('imagen_url', $data) && !empty($data['imagen_url'])) {
if ($noticia->imagen_path && Storage::disk('public')->exists($noticia->imagen_path)) {
Storage::disk('public')->delete($noticia->imagen_path);
}
$data['imagen_path'] = null;
}
// Si explícitamente mandan imagen_url = null (quitar url) y no mandan archivo,
// no tocamos imagen_path (se queda como está). Si quieres que también limpie path,
// dime y lo cambiamos.
// si se marca publicado y no hay fecha, set now // si se marca publicado y no hay fecha, set now
if (array_key_exists('publicado', $data) && $data['publicado'] && empty($noticia->fecha_publicacion) && empty($data['fecha_publicacion'])) { if (
array_key_exists('publicado', $data) &&
$data['publicado'] &&
empty($noticia->fecha_publicacion) &&
empty($data['fecha_publicacion'])
) {
$data['fecha_publicacion'] = now(); $data['fecha_publicacion'] = now();
} }
@ -168,7 +195,6 @@ public function showPublic(Noticia $noticia)
// DELETE /api/noticias/{noticia} // DELETE /api/noticias/{noticia}
public function destroy(Noticia $noticia) public function destroy(Noticia $noticia)
{ {
// opcional: borrar imagen al eliminar
if ($noticia->imagen_path && Storage::disk('public')->exists($noticia->imagen_path)) { if ($noticia->imagen_path && Storage::disk('public')->exists($noticia->imagen_path)) {
Storage::disk('public')->delete($noticia->imagen_path); Storage::disk('public')->delete($noticia->imagen_path);
} }

@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class Noticia extends Model class Noticia extends Model
@ -20,6 +21,7 @@ class Noticia extends Model
'categoria', 'categoria',
'tag_color', 'tag_color',
'imagen_path', 'imagen_path',
'imagen_url', // ✅ agrega esto si también lo guardas en BD
'link_url', 'link_url',
'link_texto', 'link_texto',
'fecha_publicacion', 'fecha_publicacion',
@ -35,15 +37,24 @@ class Noticia extends Model
'orden' => 'integer', 'orden' => 'integer',
]; ];
// ✅ se incluirá en el JSON
protected $appends = ['imagen_url']; protected $appends = ['imagen_url'];
public function getImagenUrlAttribute(): ?string public function getImagenUrlAttribute(): ?string
{ {
if (!$this->imagen_path) return null; // 1) Si en BD hay una URL externa, úsala
return asset('storage/' . ltrim($this->imagen_path, '/')); if (!empty($this->attributes['imagen_url'])) {
return $this->attributes['imagen_url'];
}
// 2) Si hay imagen en storage, genera URL absoluta
if (!empty($this->imagen_path)) {
return url(Storage::disk('public')->url($this->imagen_path));
}
return null;
} }
// Auto-generar slug si no viene
protected static function booted(): void protected static function booted(): void
{ {
static::saving(function (Noticia $noticia) { static::saving(function (Noticia $noticia) {

@ -79,7 +79,7 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () {
Route::delete('/noticias/{noticia}', [NoticiaController::class, 'destroy']); Route::delete('/noticias/{noticia}', [NoticiaController::class, 'destroy']);
}); });
Route::get('/noticias', [NoticiaController::class, 'index']); Route::get('/noticias', [NoticiaController::class, 'index']);
Route::get('/noticias/{noticia}', [NoticiaController::class, 'showPublic']); Route::get('/noticias/{noticia:slug}', [NoticiaController::class, 'showPublic']);
Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () {

@ -9,7 +9,7 @@ const api = axios.create({
} }
}); });
// Request interceptor
api.interceptors.request.use( api.interceptors.request.use(
(config) => { (config) => {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
@ -25,7 +25,7 @@ api.interceptors.request.use(
} }
) )
// Response interceptor
api.interceptors.response.use( api.interceptors.response.use(
(response) => { (response) => {
return response return response
@ -33,21 +33,19 @@ api.interceptors.response.use(
async (error) => { async (error) => {
const originalRequest = error.config const originalRequest = error.config
// Si el error es 401 y no es un intento de re-autenticación
if (error.response?.status === 401 && !originalRequest._retry) { if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true originalRequest._retry = true
// Limpiar autenticación
localStorage.removeItem('token') localStorage.removeItem('token')
localStorage.removeItem('user') localStorage.removeItem('user')
// Redirigir a login router.push('account/auth/login')
router.push('/login')
return Promise.reject(error) return Promise.reject(error)
} }
// Manejar otros errores
if (error.response?.status === 403) { if (error.response?.status === 403) {
router.push('/unauthorized') router.push('/unauthorized')
} }

@ -13,7 +13,7 @@
<p class="footer-text"> <p class="footer-text">
Institución pública de educación superior comprometida con la Institución pública de educación superior comprometida con la
formación académica, científica y humanística de la región. formación académica de la región.
</p> </p>
</div> </div>
@ -40,8 +40,8 @@
<h4>Contacto</h4> <h4>Contacto</h4>
<ul> <ul>
<li>Av. Floral N° 1153 Puno</li> <li>Av. Floral N° 1153 Puno</li>
<li>📞 (051) 123-456</li> <li>📞 (+51) 957 734 361</li>
<li> admision@unap.edu.pe</li> <li> dgadmision@unap.edu.pe</li>
</ul> </ul>
</div> </div>
</div> </div>

@ -2,7 +2,6 @@
<section class="process-section" aria-labelledby="faq-title"> <section class="process-section" aria-labelledby="faq-title">
<div class="section-container"> <div class="section-container">
<!-- Header -->
<div class="section-header"> <div class="section-header">
<h2 id="faq-title" class="section-title"> <h2 id="faq-title" class="section-title">
Preguntas Frecuentes Preguntas Frecuentes
@ -15,7 +14,6 @@
<div class="process-card"> <div class="process-card">
<!-- FAQ -->
<a-collapse accordion class="modern-collapse"> <a-collapse accordion class="modern-collapse">
<a-collapse-panel key="1" header="01. ¿Puedo postular con DNI caducado?"> <a-collapse-panel key="1" header="01. ¿Puedo postular con DNI caducado?">
@ -76,7 +74,6 @@
</template> </template>
<style scoped> <style scoped>
/* reutiliza la misma base visual de ProcessSection */
.process-section { .process-section {
padding: 30px 0; padding: 30px 0;
@ -116,7 +113,7 @@
background: #fff; background: #fff;
} }
/* Ilustración */
.illustration-box { .illustration-box {
text-align: center; text-align: center;
margin-bottom: 20px; margin-bottom: 20px;
@ -132,7 +129,6 @@
color: #6b7280; color: #6b7280;
} }
/* Collapse estilo limpio */
.modern-collapse { .modern-collapse {
background: transparent; background: transparent;
} }
@ -154,7 +150,6 @@
border-top: 1px solid #eef2f7; border-top: 1px solid #eef2f7;
} }
/* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.section-title { .section-title {
font-size: 1.6rem; font-size: 1.6rem;

@ -15,7 +15,6 @@
<a-skeleton v-if="store.loading" active :paragraph="{ rows: 8 }" /> <a-skeleton v-if="store.loading" active :paragraph="{ rows: 8 }" />
<div v-else class="convocatorias-grid"> <div v-else class="convocatorias-grid">
<!-- PRINCIPAL -->
<a-card v-if="store.procesoPrincipal" class="main-convocatoria-card"> <a-card v-if="store.procesoPrincipal" class="main-convocatoria-card">
<div class="card-badge">Principal</div> <div class="card-badge">Principal</div>
@ -103,14 +102,13 @@
<a-divider class="custom-divider" /> <a-divider class="custom-divider" />
<!-- PREINSCRIPCION -->
<div class="preinscripcion-section"> <div class="preinscripcion-section">
<div class="preinscripcion-info"> <div class="preinscripcion-info">
<h4 class="subheading">Preinscripción en Línea</h4> <h4 class="subheading">Preinscripción en Línea</h4>
<p>Completa tu preinscripción de manera virtual y segura</p> <p>Completa tu preinscripción de manera virtual y segura</p>
</div> </div>
<!-- Botón centrado abajo y estilo igual a Portal del Postulante -->
<div v-if="store.procesoPrincipal?.link_preinscripcion" class="preinscripcion-btn-wrap"> <div v-if="store.procesoPrincipal?.link_preinscripcion" class="preinscripcion-btn-wrap">
<a-button <a-button
type="primary" type="primary"
@ -138,7 +136,6 @@
</div> </div>
</a-card> </a-card>
<!-- SECUNDARIAS (hardcode por ahora) -->
<div class="secondary-list"> <div class="secondary-list">
<a-card class="secondary-convocatoria-card"> <a-card class="secondary-convocatoria-card">
@ -192,7 +189,6 @@
</div> </div>
</section> </section>
<!-- MODAL (para detalles por tipo) -->
<a-modal <a-modal
v-model:open="modalVisible" v-model:open="modalVisible"
:title="tituloModal" :title="tituloModal"
@ -206,7 +202,6 @@
:key="detalle.id" :key="detalle.id"
style="margin-bottom: 25px" style="margin-bottom: 25px"
> >
<!-- <h3>{{ detalle.titulo_detalle }}</h3> -->
<p v-if="detalle.descripcion"> <p v-if="detalle.descripcion">
{{ detalle.descripcion }} {{ detalle.descripcion }}
@ -494,14 +489,12 @@ const abrirPorTipo = (tipo) => {
color: #666; color: #666;
} }
/* fila inferior centrada */
.preinscripcion-btn-wrap { .preinscripcion-btn-wrap {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 6px; margin-top: 6px;
} }
/* botón claro estilo AntDV */
.preinscripcion-btn-light { .preinscripcion-btn-light {
height: 46px; height: 46px;
padding: 0 18px; padding: 0 18px;

@ -5,12 +5,10 @@
:style="{ backgroundImage: `url(${heroImg})` }" :style="{ backgroundImage: `url(${heroImg})` }"
aria-label="Sección principal de admisión" aria-label="Sección principal de admisión"
> >
<!-- Overlay oscuro -->
<div class="hero-overlay"></div> <div class="hero-overlay"></div>
<div class="hero-container"> <div class="hero-container">
<!-- CONTENIDO -->
<div class="hero-content"> <div class="hero-content">
<div class="hero-badges"> <div class="hero-badges">
<a-tag class="hero-tag">Convocatoria 2026</a-tag> <a-tag class="hero-tag">Convocatoria 2026</a-tag>
@ -93,7 +91,6 @@ defineEmits(["scroll-to-convocatoria", "virtual-tour"]);
<style scoped> <style scoped>
/* 🔥 TODA LA SECCIÓN EN TIMES */
.hero, .hero,
.hero * { .hero * {
font-family: "Times New Roman", Times, serif; font-family: "Times New Roman", Times, serif;
@ -107,7 +104,7 @@ defineEmits(["scroll-to-convocatoria", "virtual-tour"]);
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
overflow: hidden; overflow: hidden;
--text: rgba(245, 247, 255, 0.92); /* blanco suave */ --text: rgba(245, 247, 255, 0.92);
--muted: rgba(229, 235, 255, 0.72); --muted: rgba(229, 235, 255, 0.72);
} }
.hero-container { .hero-container {
@ -147,7 +144,6 @@ defineEmits(["scroll-to-convocatoria", "virtual-tour"]);
font-weight: bold; font-weight: bold;
} }
/* TITULO */
.hero-title { .hero-title {
font-size: 3.5rem; font-size: 3.5rem;
font-weight: bold; font-weight: bold;
@ -211,7 +207,7 @@ defineEmits(["scroll-to-convocatoria", "virtual-tour"]);
background: rgba(190, 200, 228, 0.55); background: rgba(190, 200, 228, 0.55);
padding: 12px 24px; padding: 12px 24px;
border-radius: 16px; border-radius: 16px;
width: fit-content; /* 👈 se ajusta al contenido */ width: fit-content;
max-width: 100%; max-width: 100%;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
} }

@ -1,7 +1,6 @@
<!-- src/components/web/NewsSection.vue --> <!-- src/components/web/NewsSection.vue -->
<template> <template>
<!-- Si no hay noticias, NO renderiza NADA --> <section class="news-section">
<section class="news-section">
<div class="container"> <div class="container">
<!-- Header centrado --> <!-- Header centrado -->
<div class="header"> <div class="header">
@ -19,84 +18,145 @@
<a-divider class="divider" /> <a-divider class="divider" />
<!-- Grid --> <!-- Grid -->
<div v-if="mappedNoticias.length"> <div v-if="mappedNoticias.length">
<a-row :gutter="[24, 24]"> <a-row :gutter="[24, 24]">
<a-col <a-col
v-for="noticia in mappedNoticias" v-for="noticia in mappedNoticias"
:key="noticia.id" :key="noticia.id"
:xs="24" :xs="24"
:sm="12" :sm="12"
:lg="8" :lg="8"
>
<a-badge-ribbon
:text="noticia.categoria || 'Noticia'"
:color="noticia.tagColor"
> >
<a-card hoverable class="card" :bodyStyle="{ padding: '16px' }"> <a-badge-ribbon
<!-- Cover SOLO si hay imagen --> :text="noticia.categoria || 'Noticia'"
<template v-if="noticia.imagen" #cover> :color="noticia.tagColor"
<div >
class="cover"
:style="{ backgroundImage: `url(${noticia.imagen})` }" <a-card
> hoverable
<div class="cover-overlay" /> class="card"
:bodyStyle="{ padding: '16px' }"
<div class="date-pill"> @click="openModal(noticia)"
>
<template v-if="noticia.imagen" #cover>
<div
class="cover"
:style="{ backgroundImage: `url(${noticia.imagen})` }"
>
<div class="cover-overlay" />
<div class="date-pill">
<CalendarOutlined />
<span>{{ noticia.fecha }}</span>
</div>
</div>
</template>
<a-space direction="vertical" size="small" class="content">
<div v-if="!noticia.imagen" class="date-inline">
<CalendarOutlined /> <CalendarOutlined />
<span>{{ noticia.fecha }}</span> <span>{{ noticia.fecha }}</span>
</div> </div>
</div>
</template> <a-typography-title
:level="4"
<a-space direction="vertical" size="small" class="content"> class="card-title"
<!-- Si NO hay imagen, mostramos la fecha aquí --> :content="noticia.titulo"
<div v-if="!noticia.imagen" class="date-inline"> :ellipsis="{ rows: 2, tooltip: noticia.titulo }"
<CalendarOutlined /> />
<span>{{ noticia.fecha }}</span>
</div> <a-typography-paragraph
class="desc"
<a-typography-title :content="noticia.descripcion"
:level="4" :ellipsis="{ rows: 3, tooltip: noticia.descripcion }"
class="card-title" />
:content="noticia.titulo"
:ellipsis="{ rows: 2, tooltip: noticia.titulo }" <div class="actions">
/>
<a-button
<a-typography-paragraph type="link"
class="desc" class="read-more"
:content="noticia.descripcion" @click.stop="openModal(noticia)"
:ellipsis="{ rows: 3, tooltip: noticia.descripcion }" >
/> Leer más
<ArrowRightOutlined />
<div class="actions"> </a-button>
<a-button type="link" class="read-more" @click="handleLeerMas(noticia)">
Leer más <a-tag v-if="noticia.destacado" color="gold" class="tag-soft">
<ArrowRightOutlined /> Destacado
</a-button> </a-tag>
</div>
<a-tag v-if="noticia.destacado" color="gold" class="tag-soft"> </a-space>
Destacado </a-card>
</a-tag> </a-badge-ribbon>
</div> </a-col>
</a-space> </a-row>
</a-card>
</a-badge-ribbon>
</a-col>
</a-row>
</div> </div>
<a-modal
v-model:open="modalOpen"
:title="modalTitle"
:footer="null"
:width="860"
destroyOnClose
@afterClose="onAfterClose"
>
<div v-if="noticiasStore.loadingOne" style="padding: 8px 0">
<a-skeleton active :paragraph="{ rows: 6 }" />
</div>
<a-alert
v-else-if="noticiasStore.error"
type="error"
show-icon
:message="noticiasStore.error"
/>
<div v-else>
<div v-if="modalImage" class="modal-cover">
<img :src="modalImage" alt="Imagen de noticia" />
</div>
<div class="modal-meta">
<a-tag :color="modalTagColor">{{ modalCategoria }}</a-tag>
<span class="modal-date">
<CalendarOutlined />
{{ modalFecha }}
</span>
</div>
<a-typography-paragraph v-if="modalDescripcion" class="modal-desc">
{{ modalDescripcion }}
</a-typography-paragraph>
<div v-if="modalContenido" class="modal-content" v-html="modalContenido" />
<a-empty v-else description="No hay contenido disponible para esta noticia." />
<div v-if="modalLinkUrl" class="modal-link">
<a-button
type="primary"
:href="modalLinkUrl"
target="_blank"
rel="noopener"
>
{{ modalLinkTexto }}
</a-button>
</div>
</div>
</a-modal>
</div> </div>
</section> </section>
</template> </template>
<script setup> <script setup>
import { computed, onMounted } from "vue" import { computed, onMounted, ref } from "vue"
import { CalendarOutlined, ArrowRightOutlined } from "@ant-design/icons-vue" import { CalendarOutlined, ArrowRightOutlined } from "@ant-design/icons-vue"
import { useNoticiasPublicasStore } from "../../store/noticiasPublicas.store" import { useNoticiasPublicasStore } from "../../store/noticiasPublicas.store"
const noticiasStore = useNoticiasPublicasStore() const noticiasStore = useNoticiasPublicasStore()
onMounted(() => { onMounted(() => {
// si ya está cargado, no vuelve a pedir
if (!noticiasStore.noticias.length) noticiasStore.fetchNoticias() if (!noticiasStore.noticias.length) noticiasStore.fetchNoticias()
}) })
@ -105,10 +165,7 @@ const mappedNoticias = computed(() => {
const titulo = n.titulo ?? "Sin título" const titulo = n.titulo ?? "Sin título"
const descripcion = n.descripcion_corta ?? n.descripcion ?? "Sin descripción" const descripcion = n.descripcion_corta ?? n.descripcion ?? "Sin descripción"
// SIN imagen por defecto (NO /images/extra.jpg) const imagen = n.imagen_url ?? null
const imagen =
n.imagen_url ||
(n.imagen_path ? `http://localhost:8000/storage/${n.imagen_path}` : null)
return { return {
id: n.id, id: n.id,
@ -125,10 +182,55 @@ const mappedNoticias = computed(() => {
}) })
}) })
const handleLeerMas = (noticia) => { const modalOpen = ref(false)
// aquí tú decides: navegar, abrir modal, etc. const selectedNoticia = ref(null)
// por ahora solo lo dejamos listo para que conectes tu acción
console.log("Leer más:", noticia.slug || noticia.id) const modalTitle = computed(() => selectedNoticia.value?.titulo ?? "Detalle de noticia")
const detail = computed(() => noticiasStore.noticiaActual)
const modalImage = computed(() => {
const d = detail.value
return d?.imagen_url ?? selectedNoticia.value?.imagen ?? null
})
const modalFecha = computed(() => {
const d = detail.value
return formatFecha(d?.fecha_publicacion ?? selectedNoticia.value?.raw?.fecha_publicacion)
})
const modalCategoria = computed(() => {
return detail.value?.categoria ?? selectedNoticia.value?.categoria ?? "General"
})
const modalTagColor = computed(() => {
return normalizeTagColor(detail.value?.tag_color ?? selectedNoticia.value?.raw?.tag_color)
})
const modalDescripcion = computed(() => {
const d = detail.value
return d?.descripcion_corta ?? selectedNoticia.value?.descripcion ?? ""
})
const modalContenido = computed(() => {
return detail.value?.contenido ?? ""
})
const modalLinkUrl = computed(() => detail.value?.link_url ?? null)
const modalLinkTexto = computed(() => detail.value?.link_texto ?? "Leer más")
const openModal = async (noticia) => {
selectedNoticia.value = noticia
modalOpen.value = true
noticiasStore.clearNoticiaActual()
const identifier = noticia.slug || noticia.id
await noticiasStore.fetchNoticia(identifier)
}
const onAfterClose = () => {
noticiasStore.clearNoticiaActual()
selectedNoticia.value = null
} }
const formatFecha = (iso) => { const formatFecha = (iso) => {
@ -162,6 +264,7 @@ const normalizeTagColor = (c) => {
} }
</script> </script>
<style scoped> <style scoped>
.news-section { .news-section {
position: relative; position: relative;
@ -257,8 +360,12 @@ const normalizeTagColor = (c) => {
.cover { .cover {
position: relative; position: relative;
height: 200px; height: 200px;
background-size: cover;
background-size: contain;
background-position: center; background-position: center;
background-repeat: no-repeat;
background-color: rgba(17, 26, 86, 0.06);
} }
.cover-overlay { .cover-overlay {
@ -333,6 +440,70 @@ const normalizeTagColor = (c) => {
font-weight: 700; font-weight: 700;
} }
.modal-cover {
width: 100%;
height: 360px;
overflow: hidden;
border-radius: 14px;
margin-bottom: 14px;
border: 1px solid rgba(17, 26, 86, 0.10);
background: rgba(17, 26, 86, 0.05);
display: flex;
align-items: center;
justify-content: center;
}
.modal-cover img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.modal-meta {
display: flex;
align-items: center;
gap: 10px;
margin: 8px 0 10px;
}
.modal-date {
display: inline-flex;
align-items: center;
gap: 8px;
color: rgba(0, 0, 0, 0.55);
font-weight: 700;
}
.modal-desc {
color: rgba(0, 0, 0, 0.70);
line-height: 1.7;
margin-bottom: 12px !important;
}
.modal-content {
line-height: 1.8;
color: rgba(0, 0, 0, 0.80);
}
.modal-content :deep(img) {
max-width: 100%;
height: auto;
display: block;
}
.modal-content :deep(p) {
margin: 0 0 12px;
}
.modal-content :deep(img) {
max-width: 100%;
border-radius: 12px;
}
.modal-link {
margin-top: 16px;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.news-section { .news-section {
padding: 64px 0; padding: 64px 0;

@ -120,11 +120,9 @@ import { useWebAdmisionStore } from "../../store/web"
const store = useWebAdmisionStore() const store = useWebAdmisionStore()
/** Responsive */
const isMobile = ref(false) const isMobile = ref(false)
const checkScreen = () => (isMobile.value = window.innerWidth < 768) const checkScreen = () => (isMobile.value = window.innerWidth < 768)
/** reloj (para refrescar estados “activo hoy”) */
const now = ref(new Date()) const now = ref(new Date())
let timer = null let timer = null
@ -142,7 +140,6 @@ onUnmounted(() => {
if (timer) clearInterval(timer) if (timer) clearInterval(timer)
}) })
/** Helpers fecha */
const toDate = (value) => { const toDate = (value) => {
if (!value) return null if (!value) return null
const d = new Date(value) const d = new Date(value)
@ -196,18 +193,13 @@ const fmtRange = (start, end) => {
return "Por definir" return "Por definir"
} }
/** Título */
const tituloProceso = computed(() => { const tituloProceso = computed(() => {
const p = store.procesoPrincipal const p = store.procesoPrincipal
if (!p) return "Proceso de Admisión" if (!p) return "Proceso de Admisión"
return p.titulo || p.tipo_proceso || "Proceso de Admisión" return p.titulo || p.tipo_proceso || "Proceso de Admisión"
}) })
/**
* REGLA IMPORTANTE:
* Preinscripción dura hasta el fin de inscripción:
* inicio_preinscripcion -> fin_inscripcion
*/
const active = computed(() => { const active = computed(() => {
const p = store.procesoPrincipal const p = store.procesoPrincipal
if (!p) return { pre: false, ins: false, exa: false, res: false, bio: false } if (!p) return { pre: false, ins: false, exa: false, res: false, bio: false }
@ -240,7 +232,6 @@ const noActive = computed(() => {
return !a.pre && !a.ins && !a.exa && !a.res && !a.bio return !a.pre && !a.ins && !a.exa && !a.res && !a.bio
}) })
/** Status por step: permite que PRE e INS sean "process" a la vez */
const getStepStatus = (index, p) => { const getStepStatus = (index, p) => {
const n = now.value const n = now.value
@ -282,7 +273,7 @@ const getStepStatus = (index, p) => {
return "wait" return "wait"
} }
/** Poner un “🟢” cuando esté activo */
const withActiveBadge = (label, isActive) => (isActive ? `🟢 ${label}` : label) const withActiveBadge = (label, isActive) => (isActive ? `🟢 ${label}` : label)
const stepsItems = computed(() => { const stepsItems = computed(() => {
@ -327,7 +318,6 @@ const stepsItems = computed(() => {
] ]
}) })
/** “Qué hacer hoy” (claro para jóvenes) */
const tareasHoy = computed(() => { const tareasHoy = computed(() => {
const p = store.procesoPrincipal const p = store.procesoPrincipal
if (!p) return [] if (!p) return []
@ -486,7 +476,6 @@ const tareasHoy = computed(() => {
flex-shrink: 0; flex-shrink: 0;
} }
/* ====== Guía rápida ====== */
.help-box { .help-box {
margin-top: 14px; margin-top: 14px;
border-top: 1px dashed #e5e7eb; border-top: 1px dashed #e5e7eb;
@ -586,7 +575,6 @@ const tareasHoy = computed(() => {
line-height: 1.5; line-height: 1.5;
} }
/* Responsive */
@media (max-width: 992px) { @media (max-width: 992px) {
.section-title { .section-title {
font-size: 1.85rem; font-size: 1.85rem;

@ -1,4 +1,3 @@
<!-- components/programas/ProgramasSection.vue (SOLO ÁREAS, estilo AntDV) -->
<template> <template>
<section class="areas-section"> <section class="areas-section">
<div class="section-container"> <div class="section-container">

@ -1,4 +1,3 @@
<!-- components/stats/StatsSection.vue -->
<template> <template>
<section class="stats-section" aria-labelledby="stats-title"> <section class="stats-section" aria-labelledby="stats-title">
<div class="section-container"> <div class="section-container">

@ -22,7 +22,7 @@
/> />
</nav> </nav>
<div class="right-actions desktop-only"> <!-- <div class="right-actions desktop-only">
<router-link <router-link
v-if="!authStore.isAuthenticated" v-if="!authStore.isAuthenticated"
to="/login-postulante" to="/login-postulante"
@ -40,7 +40,7 @@
Mi Portal Mi Portal
</a-button> </a-button>
</router-link> </router-link>
</div> </div> -->
<a-button class="mobile-menu-btn mobile-only" type="text" @click="drawerOpen = true"> <a-button class="mobile-menu-btn mobile-only" type="text" @click="drawerOpen = true">
@ -81,7 +81,7 @@
:openKeys="mobileOpenKeys" :openKeys="mobileOpenKeys"
/> />
<div class="drawer-auth"> <!-- <div class="drawer-auth">
<router-link <router-link
v-if="!authStore.isAuthenticated" v-if="!authStore.isAuthenticated"
to="/login-postulante" to="/login-postulante"
@ -105,7 +105,7 @@
Mi Portal Mi Portal
</a-button> </a-button>
</router-link> </router-link>
</div> </div> -->
</div> </div>
</a-drawer> </a-drawer>
</a-layout-header> </a-layout-header>
@ -144,7 +144,7 @@ const navItems = computed(() => [
{ key: "sociales", label: "Sociales" }, { key: "sociales", label: "Sociales" },
], ],
}, },
{ key: "procesos", label: "Procesos" },
{ {
key: "modalidades", key: "modalidades",
label: "Modalidades", label: "Modalidades",
@ -165,11 +165,10 @@ watch(drawerOpen, (open) => {
const routesByKey = { const routesByKey = {
inicio: "/", inicio: "/",
programas: "/programas", programas: "",
ingenierias: "/programas/ingenierias", ingenierias: "",
biomedicas: "/programas/biomedicas", biomedicas: "",
sociales: "/programas/sociales", sociales: "",
procesos: "/procesos",
modalidades: "/modalidades", modalidades: "/modalidades",
cepreuna: "/modalidades/cepreuna", cepreuna: "/modalidades/cepreuna",
extraordinario: "/modalidades/extraordinario", extraordinario: "/modalidades/extraordinario",

@ -10,7 +10,7 @@ const routes = [
{ path: '/', component: WebPage }, { path: '/', component: WebPage },
{ path: '/login', component: Login, meta: { guest: true } }, { path: '/account/auth/login', component: Login, meta: { guest: true } },
{ {
path: '/login-postulante', path: '/login-postulante',

@ -8,8 +8,8 @@ export const useExamenStore = defineStore('examenStore', {
examenActual: null, examenActual: null,
preguntas: [], preguntas: [],
cargando: false, cargando: false,
calificando: false, // ✅ nuevo calificando: false,
resultado: null, // ✅ nuevo (para guardar resultados_examenes) resultado: null,
error: null, error: null,
}), }),
@ -105,7 +105,7 @@ export const useExamenStore = defineStore('examenStore', {
if (index !== -1 && data.success) { if (index !== -1 && data.success) {
this.preguntas[index].respuesta = respuesta this.preguntas[index].respuesta = respuesta
this.preguntas[index].es_correcta = data.correcta // 1, 0 o 2 this.preguntas[index].es_correcta = data.correcta
this.preguntas[index].puntaje = data.puntaje this.preguntas[index].puntaje = data.puntaje
} }
@ -116,7 +116,6 @@ export const useExamenStore = defineStore('examenStore', {
} }
}, },
// ✅ NUEVO: Calificar examen
async calificarExamen(examenId) { async calificarExamen(examenId) {
try { try {
this.error = null this.error = null
@ -182,8 +181,8 @@ export const useExamenStore = defineStore('examenStore', {
this.examenActual = null this.examenActual = null
this.preguntas = [] this.preguntas = []
this.cargando = false this.cargando = false
this.calificando = false // ✅ nuevo this.calificando = false
this.resultado = null // ✅ nuevo this.resultado = null
this.error = null this.error = null
} }
} }

@ -1,4 +1,3 @@
// src/store/noticiasPublicas.store.js
import { defineStore } from "pinia" import { defineStore } from "pinia"
import api from "../axiosPostulante" import api from "../axiosPostulante"
@ -16,7 +15,6 @@ export const useNoticiasPublicasStore = defineStore("noticiasPublicas", {
this.loading = true this.loading = true
this.error = null this.error = null
try { try {
// ✅ usa TU ruta real
const res = await api.get("/noticias", { const res = await api.get("/noticias", {
params: { publicado: true, per_page: 9999 }, params: { publicado: true, per_page: 9999 },
}) })
@ -32,6 +30,7 @@ export const useNoticiasPublicasStore = defineStore("noticiasPublicas", {
async fetchNoticia(identifier) { async fetchNoticia(identifier) {
this.loadingOne = true this.loadingOne = true
this.error = null this.error = null
this.noticiaActual = null
try { try {
const res = await api.get(`/noticias/${identifier}`) const res = await api.get(`/noticias/${identifier}`)
this.noticiaActual = res.data?.data ?? null this.noticiaActual = res.data?.data ?? null

@ -104,7 +104,7 @@ export const useUserStore = defineStore('user', {
console.error('Error en logout:', error) console.error('Error en logout:', error)
} finally { } finally {
this.clearAuth() this.clearAuth()
router.push('/login') router.push('/account/auth/login')
} }
}, },
@ -125,7 +125,7 @@ export const useUserStore = defineStore('user', {
redirectByRole() { redirectByRole() {
if (!this.user || !this.user.roles?.length) { if (!this.user || !this.user.roles?.length) {
router.push('/login') router.push('/account/auth/login')
return return
} }

@ -1,10 +1,10 @@
<template> <template>
<div class="login-container"> <div class="login-container">
<div class="login-card"> <div class="login-card">
<!-- Logo y título -->
<div class="login-header"> <div class="login-header">
<div class="logo"> <div class="logo">
<!-- <img src="/logo.png" alt="Logo" /> --> <img src="/logotiny.png" alt="Logo" />
</div> </div>
<h2>{{ isRegister ? 'Crear Cuenta' : 'Iniciar Sesión' }}</h2> <h2>{{ isRegister ? 'Crear Cuenta' : 'Iniciar Sesión' }}</h2>
<p class="subtitle"> <p class="subtitle">
@ -15,7 +15,6 @@
</p> </p>
</div> </div>
<!-- Formulario -->
<a-form <a-form
ref="formRef" ref="formRef"
:model="formState" :model="formState"
@ -24,8 +23,7 @@
layout="vertical" layout="vertical"
class="login-form" class="login-form"
> >
<!-- Nombre (solo registro) -->
<!-- Nombre (solo registro) -->
<a-form-item v-if="isRegister" label="Nombre completo" name="name"> <a-form-item v-if="isRegister" label="Nombre completo" name="name">
<a-input <a-input
v-model:value="formState.name" v-model:value="formState.name"
@ -39,7 +37,7 @@
</a-input> </a-input>
</a-form-item> </a-form-item>
<!-- Email -->
<a-form-item label="Correo electrónico" name="email"> <a-form-item label="Correo electrónico" name="email">
<a-input <a-input
v-model:value="formState.email" v-model:value="formState.email"
@ -53,7 +51,7 @@
</a-input> </a-input>
</a-form-item> </a-form-item>
<!-- Contraseña -->
<a-form-item label="Contraseña" name="password"> <a-form-item label="Contraseña" name="password">
<a-input-password <a-input-password
v-model:value="formState.password" v-model:value="formState.password"
@ -67,7 +65,7 @@
</a-input-password> </a-input-password>
</a-form-item> </a-form-item>
<!-- Confirmar contraseña (solo registro) -->
<a-form-item v-if="isRegister" label="Confirmar contraseña" name="password_confirmation"> <a-form-item v-if="isRegister" label="Confirmar contraseña" name="password_confirmation">
<a-input-password <a-input-password
v-model:value="formState.password_confirmation" v-model:value="formState.password_confirmation"
@ -81,7 +79,7 @@
</a-input-password> </a-input-password>
</a-form-item> </a-form-item>
<!-- Recordarme (solo login) -->
<div v-if="!isRegister" class="remember-forgot"> <div v-if="!isRegister" class="remember-forgot">
<a-checkbox v-model:checked="rememberMe"> <a-checkbox v-model:checked="rememberMe">
Recordarme Recordarme
@ -91,7 +89,6 @@
</a-button> </a-button>
</div> </div>
<!-- Botón principal -->
<a-form-item> <a-form-item>
<a-button <a-button
type="primary" type="primary"
@ -105,7 +102,6 @@
</a-button> </a-button>
</a-form-item> </a-form-item>
<!-- Cambiar modo -->
<div class="toggle-mode"> <div class="toggle-mode">
<span> <span>
{{ isRegister {{ isRegister
@ -118,7 +114,6 @@
</a-button> </a-button>
</div> </div>
<!-- Términos (solo registro) -->
<div v-if="isRegister" class="terms"> <div v-if="isRegister" class="terms">
<p> <p>
Al registrarse, acepta nuestros Al registrarse, acepta nuestros
@ -153,19 +148,18 @@ const formState = reactive({
password_confirmation: '' password_confirmation: ''
}) })
// Configurar notificación para que aparezca en el centro superior
notification.config({ notification.config({
placement: 'top', placement: 'top',
duration: 2, duration: 2,
maxCount: 1, maxCount: 1,
}) })
// Función para mostrar toast/notification
const showToast = (type, message, description = '') => { const showToast = (type, message, description = '') => {
const config = { const config = {
message, message,
description, description,
duration: 2, // 2 segundos duration: 2,
placement: 'top', placement: 'top',
} }
@ -187,7 +181,6 @@ const showToast = (type, message, description = '') => {
} }
} }
// Reglas de validación
const rules = { const rules = {
name: [ name: [
{ {
@ -237,18 +230,15 @@ const rules = {
] ]
} }
// Cambiar entre login/registro
const toggleMode = () => { const toggleMode = () => {
isRegister.value = !isRegister.value isRegister.value = !isRegister.value
formRef.value?.resetFields() formRef.value?.resetFields()
} }
// Olvidó contraseña
const handleForgotPassword = () => { const handleForgotPassword = () => {
showToast('info', 'Recuperación de contraseña', 'Contacte al administrador del sistema') showToast('info', 'Recuperación de contraseña', 'Contacte al administrador del sistema')
} }
// Enviar formulario
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
loading.value = true loading.value = true
@ -259,7 +249,7 @@ const handleSubmit = async () => {
'¡Registro exitoso!', '¡Registro exitoso!',
'Tu cuenta ha sido creada correctamente' 'Tu cuenta ha sido creada correctamente'
) )
toggleMode() // Cambiar a login toggleMode()
} else { } else {
await userStore.login(formState.email, formState.password) await userStore.login(formState.email, formState.password)
showToast('success', '¡Bienvenido!', 'Inicio de sesión exitoso') showToast('success', '¡Bienvenido!', 'Inicio de sesión exitoso')
@ -267,7 +257,7 @@ const handleSubmit = async () => {
} catch (error) { } catch (error) {
console.error('Error:', error) console.error('Error:', error)
// Extraer mensaje de error del backend
let errorMessage = 'Ocurrió un error inesperado' let errorMessage = 'Ocurrió un error inesperado'
let errorDetails = '' let errorDetails = ''
@ -317,7 +307,7 @@ const handleSubmit = async () => {
errorDetails = 'Verifique su conexión a internet' errorDetails = 'Verifique su conexión a internet'
} }
// Mostrar error en toast
showToast('error', errorMessage, errorDetails) showToast('error', errorMessage, errorDetails)
} finally { } finally {

@ -8,7 +8,6 @@
@cancel="handleCancel" @cancel="handleCancel"
class="course-modal" class="course-modal"
> >
<!-- Barra de búsqueda -->
<div class="search-section"> <div class="search-section">
<a-input-search <a-input-search
v-model:value="searchText" v-model:value="searchText"
@ -22,7 +21,6 @@
</a-input-search> </a-input-search>
</div> </div>
<!-- Lista de cursos -->
<div class="courses-container"> <div class="courses-container">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div class="courses-list"> <div class="courses-list">
@ -124,15 +122,12 @@ const filteredCursos = computed(() => {
) )
}) })
// Contador de cursos seleccionados
const selectedCursosCount = computed(() => selectedCursos.value.length) const selectedCursosCount = computed(() => selectedCursos.value.length)
// Verificar si un curso está seleccionado
const isCursoSelected = (cursoId) => { const isCursoSelected = (cursoId) => {
return selectedCursos.value.includes(cursoId) return selectedCursos.value.includes(cursoId)
} }
// Toggle de selección de curso
const toggleCurso = (cursoId) => { const toggleCurso = (cursoId) => {
const index = selectedCursos.value.indexOf(cursoId) const index = selectedCursos.value.indexOf(cursoId)
if (index > -1) { if (index > -1) {
@ -142,20 +137,17 @@ const toggleCurso = (cursoId) => {
} }
} }
// Buscar cursos
const handleSearch = () => { const handleSearch = () => {
// La búsqueda se maneja en computed filteredCursos
} }
// Cancelar
const handleCancel = () => { const handleCancel = () => {
visible.value = false visible.value = false
selectedCursos.value = [] selectedCursos.value = []
searchText.value = '' searchText.value = ''
areaStore.clearState() // Limpiar estado areaStore.clearState()
} }
// Guardar cambios
const handleSave = async () => { const handleSave = async () => {
try { try {
const result = await areaStore.vincularCursos(props.areaId, selectedCursos.value) const result = await areaStore.vincularCursos(props.areaId, selectedCursos.value)
@ -171,7 +163,6 @@ const handleSave = async () => {
} }
} }
// Función para cargar cursos
const loadCursos = async () => { const loadCursos = async () => {
if (props.areaId) { if (props.areaId) {
await areaStore.fetchCursosPorArea(props.areaId) await areaStore.fetchCursosPorArea(props.areaId)
@ -180,26 +171,21 @@ const loadCursos = async () => {
} }
} }
// Cargar cursos cuando se abre el modal
watch(() => props.open, async (newVal) => { watch(() => props.open, async (newVal) => {
if (newVal && props.areaId) { if (newVal && props.areaId) {
// Usar nextTick para asegurar que el modal esté montado
await nextTick() await nextTick()
await loadCursos() await loadCursos()
} }
}) })
// También cargar cuando cambia el áreaId (por si cambia mientras el modal está abierto)
watch(() => props.areaId, async (newVal) => { watch(() => props.areaId, async (newVal) => {
if (props.open && newVal) { if (props.open && newVal) {
await loadCursos() await loadCursos()
} }
}) })
// Limpiar al cerrar
watch(() => props.open, (newVal) => { watch(() => props.open, (newVal) => {
if (!newVal) { if (!newVal) {
// Pequeño delay para permitir que la animación de cierre termine
setTimeout(() => { setTimeout(() => {
selectedCursos.value = [] selectedCursos.value = []
searchText.value = '' searchText.value = ''
@ -208,7 +194,6 @@ watch(() => props.open, (newVal) => {
} }
}) })
// También cargar cursos cuando el componente se monta si ya está abierto
onMounted(() => { onMounted(() => {
if (props.open && props.areaId) { if (props.open && props.areaId) {
loadCursos() loadCursos()
@ -302,7 +287,6 @@ onMounted(() => {
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
} }
/* Scrollbar personalizado */
.courses-container::-webkit-scrollbar { .courses-container::-webkit-scrollbar {
width: 6px; width: 6px;
} }

@ -8,7 +8,6 @@
@cancel="handleCancel" @cancel="handleCancel"
class="process-modal" class="process-modal"
> >
<!-- Barra de búsqueda -->
<div class="search-section"> <div class="search-section">
<a-input-search <a-input-search
v-model:value="searchText" v-model:value="searchText"
@ -22,7 +21,6 @@
</a-input-search> </a-input-search>
</div> </div>
<!-- Lista de procesos -->
<div class="processes-container"> <div class="processes-container">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div v-if="procesosDisponibles.length === 0 && !loading" class="empty-state"> <div v-if="procesosDisponibles.length === 0 && !loading" class="empty-state">
@ -69,7 +67,7 @@
</a-spin> </a-spin>
</div> </div>
<!-- Resumen -->
<div class="summary-section"> <div class="summary-section">
<a-alert <a-alert
:message="`${selectedProcesosCount} procesos seleccionados de ${procesosDisponibles.length} disponibles`" :message="`${selectedProcesosCount} procesos seleccionados de ${procesosDisponibles.length} disponibles`"
@ -78,7 +76,6 @@
/> />
</div> </div>
<!-- Acciones -->
<div class="modal-footer"> <div class="modal-footer">
<a-button @click="handleCancel">Cancelar</a-button> <a-button @click="handleCancel">Cancelar</a-button>
<a-button type="primary" @click="handleSave" :loading="loading"> <a-button type="primary" @click="handleSave" :loading="loading">
@ -125,7 +122,6 @@ const loading = computed(() => areaStore.loading)
const procesosDisponibles = computed(() => areaStore.procesosDisponibles || []) const procesosDisponibles = computed(() => areaStore.procesosDisponibles || [])
const procesosVinculados = computed(() => areaStore.procesosVinculados || []) const procesosVinculados = computed(() => areaStore.procesosVinculados || [])
/* ================= FILTRO ================= */
const filteredProcesos = computed(() => { const filteredProcesos = computed(() => {
if (!searchText.value) return procesosDisponibles.value if (!searchText.value) return procesosDisponibles.value
@ -150,7 +146,6 @@ const toggleProceso = (id) => {
: selectedProcesos.value.push(id) : selectedProcesos.value.push(id)
} }
/* ================= ACCIONES ================= */
const handleCancel = () => { const handleCancel = () => {
visible.value = false visible.value = false
resetModal() resetModal()
@ -172,7 +167,7 @@ const handleSave = async () => {
} }
const handleSearch = () => { const handleSearch = () => {
// La búsqueda se maneja en computed filteredProcesos
} }
const loadProcesos = async () => { const loadProcesos = async () => {
@ -187,7 +182,6 @@ const resetModal = () => {
selectedProcesos.value = [] selectedProcesos.value = []
} }
/* ================= WATCHERS ================= */
watch(() => props.open, async (open) => { watch(() => props.open, async (open) => {
if (open && props.areaId) { if (open && props.areaId) {
await nextTick() await nextTick()
@ -201,14 +195,12 @@ watch(() => props.areaId, async (id) => {
} }
}) })
// También cargar cursos cuando el componente se monta si ya está abierto
onMounted(() => { onMounted(() => {
if (props.open && props.areaId) { if (props.open && props.areaId) {
loadProcesos() loadProcesos()
} }
}) })
// Limpiar al cerrar
watch(() => props.open, (newVal) => { watch(() => props.open, (newVal) => {
if (!newVal) { if (!newVal) {
setTimeout(() => { setTimeout(() => {

@ -1,7 +1,6 @@
<template> <template>
<div class="postulantes-container"> <div class="postulantes-container">
<!-- Header -->
<div class="header"> <div class="header">
<div> <div>
<h2 class="page-title">Gestión de Postulantes</h2> <h2 class="page-title">Gestión de Postulantes</h2>
@ -16,7 +15,6 @@
/> />
</div> </div>
<!-- Tabla -->
<a-card> <a-card>
<a-table <a-table
:columns="columns" :columns="columns"
@ -28,27 +26,22 @@
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<!-- Nombre -->
<template v-if="column.key === 'name'"> <template v-if="column.key === 'name'">
<strong>{{ record.name }}</strong> <strong>{{ record.name }}</strong>
</template> </template>
<!-- Email -->
<template v-else-if="column.key === 'email'"> <template v-else-if="column.key === 'email'">
{{ record.email }} {{ record.email }}
</template> </template>
<!-- DNI -->
<template v-else-if="column.key === 'dni'"> <template v-else-if="column.key === 'dni'">
<a-tag color="blue">{{ record.dni }}</a-tag> <a-tag color="blue">{{ record.dni }}</a-tag>
</template> </template>
<!-- Última actividad -->
<template v-else-if="column.key === 'last_activity'"> <template v-else-if="column.key === 'last_activity'">
{{ record.last_activity ?? 'Sin actividad' }} {{ record.last_activity ?? 'Sin actividad' }}
</template> </template>
<!-- Acciones -->
<template v-else-if="column.key === 'acciones'"> <template v-else-if="column.key === 'acciones'">
<a-space> <a-space>
<a-button type="text" @click="editar(record)"> <a-button type="text" @click="editar(record)">
@ -61,7 +54,6 @@
</a-table> </a-table>
</a-card> </a-card>
<!-- Modal Editar -->
<a-modal <a-modal
v-model:open="showModal" v-model:open="showModal"
title="Editar Postulante" title="Editar Postulante"

@ -505,7 +505,7 @@ const updatePageInfo = (key) => {
const logout = () => { const logout = () => {
userStore.logout() userStore.logout()
router.push('/login') router.push('/account/auth/login')
} }
const irPerfil = () => { const irPerfil = () => {

@ -1,4 +1,3 @@
<!-- DashboardPostulante.vue (test + procesos activos) -->
<template> <template>
<a-spin :spinning="loading"> <a-spin :spinning="loading">
@ -15,7 +14,6 @@
<a-divider class="softDivider" /> <a-divider class="softDivider" />
<!-- TEST -->
<a-card :bordered="false" class="testBox"> <a-card :bordered="false" class="testBox">
@ -55,7 +53,6 @@
<a-divider class="softDivider" /> <a-divider class="softDivider" />
<!-- PROCESOS ACTIVOS -->
<div class="sectionHead"> <div class="sectionHead">
<div> <div>
@ -114,7 +111,6 @@
</div> </div>
</div> </div>
<!-- Mensaje cuando no hay procesos -->
<a-empty <a-empty
v-if="!loading && state.processes.length === 0" v-if="!loading && state.processes.length === 0"
class="mt12" class="mt12"
@ -130,9 +126,9 @@ import { Modal, message } from "ant-design-vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useAuthStore } from "../../store/postulanteStore"; import { useAuthStore } from "../../store/postulanteStore";
// Ajusta a tus rutas reales
const ROUTE_TEST_PANEL = { name: "PanelTest" }; // tu panel del test const ROUTE_TEST_PANEL = { name: "PanelTest" };
const ROUTE_PROCESS_DETAIL = (id) => ({ name: "ProcesoDetalle", params: { id } }); // opcional const ROUTE_PROCESS_DETAIL = (id) => ({ name: "ProcesoDetalle", params: { id } });
const router = useRouter(); const router = useRouter();
const authStore = useAuthStore(); const authStore = useAuthStore();
@ -140,8 +136,8 @@ const authStore = useAuthStore();
const loading = ref(false); const loading = ref(false);
const state = reactive({ const state = reactive({
test: { hasAssigned: true }, // viene de tu backend/store test: { hasAssigned: true },
processes: [], // procesos activos processes: [],
}); });
const canGoTest = computed(() => !!state.test.hasAssigned); const canGoTest = computed(() => !!state.test.hasAssigned);
@ -153,7 +149,7 @@ const processColumns = computed(() => [
{ title: "Acciones", key: "actions" }, { title: "Acciones", key: "actions" },
]); ]);
// Mock: reemplaza por tu store / api real
const api = { const api = {
async getDashboard() { async getDashboard() {
return new Promise((resolve) => { return new Promise((resolve) => {
@ -217,7 +213,7 @@ function goToTest() {
} }
function onViewProcess(process) { function onViewProcess(process) {
// router.push(ROUTE_PROCESS_DETAIL(process.id));
message.info(`Abrir detalle del proceso: ${process.name}`); message.info(`Abrir detalle del proceso: ${process.name}`);
} }
@ -245,9 +241,7 @@ onMounted(fetchDashboard);
</script> </script>
<style scoped> <style scoped>
/* =========================
BASE estilo Convocatorias
========================= */
.dashboard-modern { .dashboard-modern {
position: relative; position: relative;
padding: 40px 0; padding: 40px 0;
@ -289,7 +283,7 @@ onMounted(fetchDashboard);
padding: 0 24px; padding: 0 24px;
} }
/* Layout container */
.page { .page {
width: 100%; width: 100%;
max-width: 1120px; max-width: 1120px;
@ -297,14 +291,12 @@ onMounted(fetchDashboard);
padding: 0; padding: 0;
} }
/* Helpers */
.mt12 { margin-top: 12px; } .mt12 { margin-top: 12px; }
.microHelp { font-size: 0.95rem; color: #666; margin-top: 6px; } .microHelp { font-size: 0.95rem; color: #666; margin-top: 6px; }
.softDivider { margin: 18px 0; } .softDivider { margin: 18px 0; }
/* =========================
Topbar
========================= */
.topbar { .topbar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -339,9 +331,7 @@ onMounted(fetchDashboard);
font-weight: 700; font-weight: 700;
} }
/* =========================
Test destacado (card principal)
========================= */
.testBox { .testBox {
position: relative; position: relative;
border: none; border: none;
@ -354,7 +344,6 @@ onMounted(fetchDashboard);
padding: 28px; padding: 28px;
} }
/* Badge tipo convocatorias */
.cardBadge { .cardBadge {
position: absolute; position: absolute;
top: -12px; top: -12px;
@ -424,9 +413,7 @@ onMounted(fetchDashboard);
flex: 0 0 auto; flex: 0 0 auto;
} }
/* =========================
Section header
========================= */
.sectionHead { .sectionHead {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -458,9 +445,6 @@ onMounted(fetchDashboard);
border-radius: 999px; border-radius: 999px;
} }
/* =========================
Table "card look"
========================= */
.tableWrap { .tableWrap {
width: 100%; width: 100%;
overflow-x: auto; overflow-x: auto;
@ -489,7 +473,6 @@ onMounted(fetchDashboard);
color: #666; color: #666;
} }
/* Status pill estilo convocatorias */
.status-tag { .status-tag {
font-weight: 700; font-weight: 700;
padding: 4px 12px; padding: 4px 12px;
@ -497,7 +480,6 @@ onMounted(fetchDashboard);
white-space: nowrap; white-space: nowrap;
} }
/* Actions */
.actions { .actions {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@ -516,46 +498,40 @@ onMounted(fetchDashboard);
font-weight: 700; font-weight: 700;
} }
/* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.hide-mobile{ .hide-mobile{
display: none !important; display: none !important;
} }
/* ✅ Quita el padding superior del wrapper (era 40px) */
.test-modern{ .test-modern{
padding-top: 0 !important; padding-top: 0 !important;
padding-bottom: 24px; /* opcional */ padding-bottom: 24px;
} }
/* ✅ Quita padding lateral del container para que sea edge-to-edge */
.section-container{ .section-container{
max-width: none; max-width: none;
padding: 0 !important; padding: 0 !important;
margin: 0 !important; margin: 0 !important;
} }
/* ✅ Card principal a todo el ancho y pegado arriba */
.hero{ .hero{
grid-template-columns: 1fr; grid-template-columns: 1fr;
width: 100%; width: 100%;
margin: 0 !important; margin: 0 !important;
border-radius: 0 !important; border-radius: 0 !important;
/* importante: sin “espacio arriba” */
padding: 14px 16px 18px !important; padding: 14px 16px 18px !important;
} }
/* ✅ Asegura que el primer texto no empuje hacia abajo */
.heroKicker{ margin-top: 0 !important; } .heroKicker{ margin-top: 0 !important; }
.heroTitle{ margin-top: 6px !important; } .heroTitle{ margin-top: 6px !important; }
.heroText{ margin-top: 8px !important; } .heroText{ margin-top: 8px !important; }
/* Para que la sección “Tu camino” no se pegue a los bordes */
.section{ .section{
padding: 0 16px; padding: 0 16px;
} }
/* Facts */
.heroFacts{ .heroFacts{
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }

@ -4,10 +4,10 @@
<a-col :xs="22" :sm="20" :md="20" :lg="16" :xl="14"> <a-col :xs="22" :sm="20" :md="20" :lg="16" :xl="14">
<div class="auth-shell"> <div class="auth-shell">
<a-row :gutter="[0, 0]" class="auth-layout"> <a-row :gutter="[0, 0]" class="auth-layout">
<!-- PANEL IZQUIERDO: FORM -->
<a-col :xs="24" :md="12" class="auth-pane auth-pane-form"> <a-col :xs="24" :md="12" class="auth-pane auth-pane-form">
<div class="pane-inner"> <div class="pane-inner">
<!-- Branding -->
<div class="brand"> <div class="brand">
<div class="brand-mark"> <div class="brand-mark">
<img <img
@ -56,7 +56,6 @@
</a-input> </a-input>
</a-form-item> </a-form-item>
<!-- Nombre -->
<a-form-item v-if="isRegister" label="Nombre completo" name="name"> <a-form-item v-if="isRegister" label="Nombre completo" name="name">
<a-input <a-input
v-model:value="formState.name" v-model:value="formState.name"
@ -67,14 +66,14 @@
</a-input> </a-input>
</a-form-item> </a-form-item>
<!-- Email -->
<a-form-item label="Correo electrónico" name="email"> <a-form-item label="Correo electrónico" name="email">
<a-input v-model:value="formState.email" size="large" placeholder="correo@ejemplo.com"> <a-input v-model:value="formState.email" size="large" placeholder="correo@ejemplo.com">
<template #prefix><MailOutlined /></template> <template #prefix><MailOutlined /></template>
</a-input> </a-input>
</a-form-item> </a-form-item>
<!-- Password -->
<a-form-item label="Contraseña" name="password"> <a-form-item label="Contraseña" name="password">
<a-input-password <a-input-password
v-model:value="formState.password" v-model:value="formState.password"
@ -85,7 +84,7 @@
</a-input-password> </a-input-password>
</a-form-item> </a-form-item>
<!-- Confirm Password -->
<a-form-item <a-form-item
v-if="isRegister" v-if="isRegister"
label="Confirmar contraseña" label="Confirmar contraseña"
@ -100,7 +99,7 @@
</a-input-password> </a-input-password>
</a-form-item> </a-form-item>
<!-- Recordarme -->
<a-row <a-row
v-if="!isRegister" v-if="!isRegister"
justify="space-between" justify="space-between"
@ -138,7 +137,6 @@
</div> </div>
</a-col> </a-col>
<!-- PANEL DERECHO: INFO (CORTO Y DINÁMICO) -->
<a-col :xs="24" :md="12" class="auth-pane auth-pane-info"> <a-col :xs="24" :md="12" class="auth-pane auth-pane-info">
<div class="pane-inner pane-inner-info"> <div class="pane-inner pane-inner-info">
<div class="info-top"> <div class="info-top">
@ -223,10 +221,7 @@ const isRegister = ref(false);
const rememberMe = ref(false); const rememberMe = ref(false);
const loading = ref(false); const loading = ref(false);
/**
* Logo en /public (recomendado):
* public/logotiny.png -> "/logotiny.png"
*/
const logoSrc = "/logotiny.png"; const logoSrc = "/logotiny.png";
const logoError = ref(false); const logoError = ref(false);
@ -479,7 +474,6 @@ checkExistingAuth();
font-weight: 800; font-weight: 800;
} }
/* Info panel corto */
.info-top { .info-top {
text-align: left; text-align: left;
} }
@ -531,7 +525,6 @@ checkExistingAuth();
border-top: 1px solid var(--ant-colorBorderSecondary, rgba(0, 0, 0, 0.06)); border-top: 1px solid var(--ant-colorBorderSecondary, rgba(0, 0, 0, 0.06));
} }
/* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.auth-pane { .auth-pane {
padding: 22px; padding: 22px;
@ -545,7 +538,6 @@ checkExistingAuth();
} }
} }
/* Fallback si no hay color-mix */
@supports not (color: color-mix(in srgb, white 50%, black)) { @supports not (color: color-mix(in srgb, white 50%, black)) {
.info-tag { .info-tag {
background: rgba(22, 119, 255, 0.12); background: rgba(22, 119, 255, 0.12);

@ -23,7 +23,6 @@
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<!-- Tools -->
<div class="tools"> <div class="tools">
<div class="counter"> <div class="counter">
<span class="counterLabel">Total</span> <span class="counterLabel">Total</span>
@ -38,7 +37,6 @@
/> />
</div> </div>
<!-- ================= DESKTOP / TABLE ================= -->
<div class="tableWrap desktopOnly"> <div class="tableWrap desktopOnly">
<a-table <a-table
:dataSource="procesosFiltrados" :dataSource="procesosFiltrados"
@ -79,7 +77,7 @@
</a-table> </a-table>
</div> </div>
<!-- ================= MOBILE / CARDS ================= -->
<div class="cards mobileOnly"> <div class="cards mobileOnly">
<template v-if="procesosFiltrados.length"> <template v-if="procesosFiltrados.length">
<div <div
@ -189,7 +187,7 @@ onMounted(() => {
<style scoped> <style scoped>
/* Card */
.card { .card {
width: 100%; width: 100%;
max-width: 1100px; max-width: 1100px;
@ -197,7 +195,7 @@ onMounted(() => {
border-radius: 14px; border-radius: 14px;
} }
/* Header */
.header { .header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -219,7 +217,6 @@ onMounted(() => {
margin-top: 4px; margin-top: 4px;
} }
/* Tools */
.tools { .tools {
display: grid; display: grid;
grid-template-columns: 220px 1fr; grid-template-columns: 220px 1fr;
@ -244,7 +241,7 @@ onMounted(() => {
border-radius: 12px; border-radius: 12px;
} }
/* Table */
.tableWrap { .tableWrap {
width: 100%; width: 100%;
overflow-x: auto; overflow-x: auto;
@ -264,7 +261,7 @@ onMounted(() => {
color: #6b7280; color: #6b7280;
} }
/* Status */
.statusPill { .statusPill {
padding: 4px 10px; padding: 4px 10px;
border-radius: 999px; border-radius: 999px;
@ -336,7 +333,7 @@ onMounted(() => {
margin-top: 12px; margin-top: 12px;
} }
/* Responsive */
.desktopOnly { display: block; } .desktopOnly { display: block; }
.mobileOnly { display: none; } .mobileOnly { display: none; }

@ -17,7 +17,6 @@
</template> </template>
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<!-- Tools -->
<div class="tools"> <div class="tools">
<div class="counter"> <div class="counter">
<span class="counterLabel">Total</span> <span class="counterLabel">Total</span>
@ -32,7 +31,6 @@
/> />
</div> </div>
<!-- Desktop/tablet: tabla -->
<div v-if="!isMobile" class="tableWrap"> <div v-if="!isMobile" class="tableWrap">
<a-table <a-table
class="table" class="table"
@ -78,7 +76,7 @@
</a-table> </a-table>
</div> </div>
<!-- Mobile: cards -->
<div v-else class="cards"> <div v-else class="cards">
<template v-if="pagosFiltrados.length"> <template v-if="pagosFiltrados.length">
<div v-for="p in pagosFiltrados" :key="p.key" class="itemCard"> <div v-for="p in pagosFiltrados" :key="p.key" class="itemCard">
@ -122,7 +120,6 @@
<a-empty v-else description="No se encontraron pagos" /> <a-empty v-else description="No se encontraron pagos" />
</div> </div>
<!-- Nota (sin colores fuertes) -->
<div class="note"> <div class="note">
<div class="noteTitle">Nota</div> <div class="noteTitle">Nota</div>
<div class="noteText"> <div class="noteText">
@ -211,7 +208,6 @@ const pagosFiltrados = computed(() => {
}); });
}); });
/* ✅ Responsive real: móvil = cards */
const isMobile = ref(false); const isMobile = ref(false);
let mq = null; let mq = null;
@ -235,14 +231,13 @@ onBeforeUnmount(() => {
</script> </script>
<style scoped> <style scoped>
/* Formal 17+ | sin degradados | un solo acento: Primary */
.card { .card {
max-width: 1100px; max-width: 1100px;
margin: 16px auto; margin: 16px auto;
border-radius: 14px; border-radius: 14px;
} }
/* Header */
.header { .header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -274,7 +269,6 @@ onBeforeUnmount(() => {
border-radius: 10px; border-radius: 10px;
} }
/* Tools */
.tools { .tools {
display: grid; display: grid;
grid-template-columns: 220px 1fr; grid-template-columns: 220px 1fr;
@ -305,7 +299,6 @@ onBeforeUnmount(() => {
border-radius: 12px; border-radius: 12px;
} }
/* Table */
.tableWrap { .tableWrap {
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
@ -315,7 +308,6 @@ onBeforeUnmount(() => {
overflow: hidden; overflow: hidden;
} }
/* Pills (neutras, sin verde/azul/naranja por tipo) */
.pill { .pill {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -334,11 +326,10 @@ onBeforeUnmount(() => {
} }
.pill--soft { .pill--soft {
border-color: rgba(22,119,255,.25); border-color: rgba(22,119,255,.25);
background: rgba(22,119,255,.08); /* ✅ único acento */ background: rgba(22,119,255,.08);
color: var(--ant-colorTextHeading, #111827); color: var(--ant-colorTextHeading, #111827);
} }
/* Cells */
.codigoCell { .codigoCell {
display: flex; display: flex;
gap: 8px; gap: 8px;
@ -368,14 +359,13 @@ onBeforeUnmount(() => {
.monto { .monto {
font-weight: 900; font-weight: 900;
color: var(--ant-colorPrimary, #1677ff); /* ✅ único acento */ color: var(--ant-colorPrimary, #1677ff);
} }
.fecha { .fecha {
font-weight: 800; font-weight: 800;
color: var(--ant-colorText, #374151); color: var(--ant-colorText, #374151);
} }
/* Mobile cards */
.cards { .cards {
display: grid; display: grid;
gap: 12px; gap: 12px;
@ -438,7 +428,6 @@ onBeforeUnmount(() => {
color: var(--ant-colorPrimary, #1677ff); color: var(--ant-colorPrimary, #1677ff);
} }
/* Nota */
.note { .note {
margin-top: 14px; margin-top: 14px;
border-radius: 14px; border-radius: 14px;
@ -457,7 +446,6 @@ onBeforeUnmount(() => {
line-height: 1.5; line-height: 1.5;
} }
/* Responsive */
@media (max-width: 640px) { @media (max-width: 640px) {
.card { .card {
margin: 0; margin: 0;

@ -1,4 +1,3 @@
<!-- views/PortalView.vue -->
<template> <template>
<a-layout class="portal-layout"> <a-layout class="portal-layout">
<!-- Header --> <!-- Header -->
@ -19,7 +18,7 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<!-- Perfil -->
<a-dropdown :trigger="['click']" placement="bottomRight"> <a-dropdown :trigger="['click']" placement="bottomRight">
<div class="profile-trigger"> <div class="profile-trigger">
<a-avatar <a-avatar
@ -72,19 +71,17 @@
</div> </div>
</a-layout-header> </a-layout-header>
<!-- Main layout -->
<a-layout <a-layout
class="main-layout" class="main-layout"
:class="{ 'layout-collapsed': sidebarCollapsed && !isMobile }" :class="{ 'layout-collapsed': sidebarCollapsed && !isMobile }"
> >
<!-- Backdrop móvil -->
<div <div
v-if="isMobile && !sidebarCollapsed" v-if="isMobile && !sidebarCollapsed"
class="sidebar-backdrop" class="sidebar-backdrop"
@click="sidebarCollapsed = true" @click="sidebarCollapsed = true"
/> />
<!-- Sidebar -->
<a-layout-sider <a-layout-sider
v-model:collapsed="sidebarCollapsed" v-model:collapsed="sidebarCollapsed"
:width="sidebarWidth" :width="sidebarWidth"
@ -179,7 +176,6 @@
</div> </div>
</a-layout-sider> </a-layout-sider>
<!-- Content -->
<a-layout-content class="content"> <a-layout-content class="content">
<div class="content-container"> <div class="content-container">
<a-card class="content-card" :bordered="false"> <a-card class="content-card" :bordered="false">
@ -253,7 +249,6 @@ const getAvatarColor = (name) => {
const handleMenuSelect = ({ key }) => { const handleMenuSelect = ({ key }) => {
selectedKeys.value = [key] selectedKeys.value = [key]
// Ajusta aquí tus rutas reales
const routes = { const routes = {
'dashboard-postulante': { name: 'DashboardPostulante' }, 'dashboard-postulante': { name: 'DashboardPostulante' },
'test-postulante': { name: 'TestPostulante' }, 'test-postulante': { name: 'TestPostulante' },
@ -279,10 +274,6 @@ const handleLogout = async () => {
} }
} }
const goToProfile = () => {
message.info('Perfil del postulante')
// router.push('/portal/perfil')
}
const openHelp = () => { const openHelp = () => {
message.info('Centro de ayuda disponible') message.info('Centro de ayuda disponible')
@ -302,19 +293,16 @@ onUnmounted(() => {
<style scoped> <style scoped>
/* Tipografía institucional */
.portal-layout, .portal-layout,
.portal-layout * { .portal-layout * {
font-family: "Times New Roman", Times, serif; font-family: "Times New Roman", Times, serif;
} }
/* Layout base */
.portal-layout { .portal-layout {
min-height: 100vh; min-height: 100vh;
background: var(--ant-colorBgLayout, #f5f5f5); background: var(--ant-colorBgLayout, #f5f5f5);
} }
/* ===== Header ===== */
.header { .header {
height: 64px; height: 64px;
padding: 0; padding: 0;
@ -331,8 +319,8 @@ onUnmounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 32px; /* más aire */ padding: 0 32px;
width: 100%; /* ocupa todo */ width: 100%;
} }
.header-left { .header-left {
@ -387,7 +375,6 @@ onUnmounted(() => {
margin-top: 2px; margin-top: 2px;
} }
/* Profile trigger */
.header-right { .header-right {
display: flex; display: flex;
align-items: center; align-items: center;
@ -416,8 +403,8 @@ onUnmounted(() => {
.profile-text { .profile-text {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0; /* ✅ elimina separación */ gap: 0;
line-height: 1.05; /* ✅ compacto */ line-height: 1.05;
} }
@ -425,7 +412,7 @@ onUnmounted(() => {
font-size: 13px; font-size: 13px;
font-weight: 800; font-weight: 800;
color: var(--ant-colorText, #374151); color: var(--ant-colorText, #374151);
margin: 0; /* ✅ sin margen */ margin: 0;
padding: 0; padding: 0;
} }
@ -434,9 +421,9 @@ onUnmounted(() => {
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
color: var(--ant-colorTextSecondary, #6b7280); color: var(--ant-colorTextSecondary, #6b7280);
margin: 0; /* ✅ sin margen */ margin: 0;
padding: 0; padding: 0;
transform: translateY(-1px); /* ✅ sube 1px (opcional) */ transform: translateY(-1px);
} }
.dropdown-chevron { .dropdown-chevron {
font-size: 12px; font-size: 12px;
@ -518,7 +505,6 @@ onUnmounted(() => {
color: #ff4d4f; color: #ff4d4f;
} }
/* ===== Sidebar ===== */
.sidebar { .sidebar {
background: var(--ant-colorBgContainer, #fff); background: var(--ant-colorBgContainer, #fff);
border-right: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06)); border-right: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06));
@ -560,7 +546,6 @@ onUnmounted(() => {
letter-spacing: .5px; letter-spacing: .5px;
} }
/* ✅ AntDV menu selected/hover correctos */
.sidebar-menu :deep(.ant-menu-item) { .sidebar-menu :deep(.ant-menu-item) {
border-radius: 12px; border-radius: 12px;
margin: 4px 6px; margin: 4px 6px;
@ -588,7 +573,6 @@ onUnmounted(() => {
margin-left: auto; margin-left: auto;
} }
/* Sidebar Footer */
.sidebar-footer { .sidebar-footer {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@ -634,7 +618,6 @@ onUnmounted(() => {
margin-top: 2px; margin-top: 2px;
} }
/* ===== Main + Content ===== */
.main-layout { .main-layout {
margin-left: 280px; margin-left: 280px;
transition: margin-left .25s ease; transition: margin-left .25s ease;
@ -691,7 +674,6 @@ onUnmounted(() => {
opacity: 0.55; opacity: 0.55;
} }
/* ===== Mobile ===== */
.sidebar-backdrop { .sidebar-backdrop {
position: fixed; position: fixed;
inset: 0; inset: 0;
@ -699,18 +681,15 @@ onUnmounted(() => {
z-index: 998; z-index: 998;
} }
/* Sidebar en móvil */
.sidebar.sidebar-mobile { .sidebar.sidebar-mobile {
top: 64px; top: 64px;
height: calc(100vh - 64px); height: calc(100vh - 64px);
} }
/* AntDV collapsed mobile (width 0) ya lo oculta, pero reforzamos por UX */
.sidebar.sidebar-mobile :deep(.ant-layout-sider-children) { .sidebar.sidebar-mobile :deep(.ant-layout-sider-children) {
height: 100%; height: 100%;
} }
/* Responsive */
@media (max-width: 992px) { @media (max-width: 992px) {
.main-layout, .main-layout,
.main-layout.layout-collapsed { .main-layout.layout-collapsed {

@ -644,7 +644,7 @@ const finalizarExamen = async () => {
}) })
setTimeout(() => modal.destroy(), 2000) setTimeout(() => modal.destroy(), 2000)
} finally { } finally {
// evita doble click mientras califica
setTimeout(() => { setTimeout(() => {
finalizando.value = false finalizando.value = false
}, 5000) }, 5000)
@ -652,7 +652,7 @@ const finalizarExamen = async () => {
} }
/* TIMER */
const finalizarExamenAutomaticamente = () => { const finalizarExamenAutomaticamente = () => {
message.error("Tiempo agotado. El examen se finalizará."); message.error("Tiempo agotado. El examen se finalizará.");
finalizarExamen(); finalizarExamen();
@ -669,7 +669,7 @@ const calcularTiempoRestante = () => {
} }
}; };
/* INIT */
const iniciarSesionExamen = async () => { const iniciarSesionExamen = async () => {
if (initOnce.value) return; if (initOnce.value) return;
initOnce.value = true; initOnce.value = true;
@ -716,16 +716,16 @@ onBeforeUnmount(() => {
</script> </script>
<style scoped> <style scoped>
/* ============ LAYOUT SERIO ============ */
.exam-page { .exam-page {
max-width: 920px; max-width: 920px;
margin: 0 auto; margin: 0 auto;
padding: 16px; padding: 16px;
background: #f6f7f9; /* sobrio */ background: #f6f7f9;
min-height: 100vh; min-height: 100vh;
} }
/* ============ HEADER ============ */
.top-card { .top-card {
border-radius: 14px; border-radius: 14px;
background: #ffffff; background: #ffffff;
@ -789,7 +789,7 @@ onBeforeUnmount(() => {
color: #6b7280; color: #6b7280;
} }
/* Timer sobrio */
.top-right { .top-right {
min-width: 180px; min-width: 180px;
text-align: right; text-align: right;
@ -805,10 +805,10 @@ onBeforeUnmount(() => {
.timer :deep(.ant-statistic-content) { .timer :deep(.ant-statistic-content) {
font-size: 26px; font-size: 26px;
font-weight: 900; font-weight: 900;
color: #111827; /* serio, sin rojo chillón */ color: #111827;
} }
/* ============ CARDS ============ */
.card { .card {
margin-top: 12px; margin-top: 12px;
border-radius: 16px; border-radius: 16px;
@ -820,7 +820,6 @@ onBeforeUnmount(() => {
padding-bottom: 6px; padding-bottom: 6px;
} }
/* ============ PREGUNTA ============ */
.q-header { .q-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -857,7 +856,6 @@ onBeforeUnmount(() => {
color: #374151; color: #374151;
} }
/* Enunciado serio, legible */
.enunciado { .enunciado {
margin-top: 12px; margin-top: 12px;
padding: 14px 14px; padding: 14px 14px;
@ -881,7 +879,6 @@ onBeforeUnmount(() => {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
/* ============ RESPUESTAS ============ */
.answer { .answer {
margin-top: 14px; margin-top: 14px;
} }
@ -923,7 +920,6 @@ onBeforeUnmount(() => {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
/* ============ NAV (SOLO 2 BOTONES) ============ */
.nav { .nav {
margin-top: 16px; margin-top: 16px;
display: flex; display: flex;
@ -942,7 +938,7 @@ onBeforeUnmount(() => {
min-width: 200px; min-width: 200px;
} }
/* Nota discreta */
.note { .note {
margin-top: 12px; margin-top: 12px;
font-size: 12px; font-size: 12px;
@ -950,7 +946,7 @@ onBeforeUnmount(() => {
line-height: 1.5; line-height: 1.5;
} }
/* Responsive */
@media (max-width: 640px) { @media (max-width: 640px) {
.top-right { .top-right {
text-align: left; text-align: left;

@ -1,14 +1,12 @@
<!-- PanelResultados.vue (DISEÑO ALTERNATIVO: "Scoreboard" moderno, full responsive) --> <!-- PanelResultados.vue (DISEÑO ALTERNATIVO: "Scoreboard" moderno, full responsive) -->
<template> <template>
<div class="page alt"> <div class="page alt">
<!-- Header minimal -->
<div class="head"> <div class="head">
<div> <div>
<div class="hTitle">Resultados del examen</div> <div class="hTitle">Resultados del examen</div>
<div class="hSub">Resumen claro + detalle por curso.</div> <div class="hSub">Resumen claro + detalle por curso.</div>
</div> </div>
<!-- (opcional) acciones -->
<div class="headActions"> <div class="headActions">
<a-space class="headSpace"> <a-space class="headSpace">
<a-button class="btnSoft" @click="volver" :disabled="cargando">Volver</a-button> <a-button class="btnSoft" @click="volver" :disabled="cargando">Volver</a-button>
@ -21,9 +19,7 @@
<a-spin :spinning="cargando" tip="Cargando resultados..."> <a-spin :spinning="cargando" tip="Cargando resultados...">
<template v-if="resultado"> <template v-if="resultado">
<!-- TOP: Scoreboard -->
<a-row :gutter="[16, 16]"> <a-row :gutter="[16, 16]">
<!-- Puntaje principal -->
<a-col :xs="24" :lg="10"> <a-col :xs="24" :lg="10">
<a-card class="card scoreCard" :bordered="false"> <a-card class="card scoreCard" :bordered="false">
<div class="scoreTop"> <div class="scoreTop">
@ -58,7 +54,6 @@
</a-card> </a-card>
</a-col> </a-col>
<!-- KPIs -->
<a-col :xs="24" :lg="14"> <a-col :xs="24" :lg="14">
<a-row :gutter="[16, 16]"> <a-row :gutter="[16, 16]">
<a-col :xs="24" :sm="12" :md="8"> <a-col :xs="24" :sm="12" :md="8">
@ -115,7 +110,6 @@
</a-col> </a-col>
</a-row> </a-row>
<!-- DETALLE: tabla por curso -->
<a-row :gutter="[16, 16]" class="mt16"> <a-row :gutter="[16, 16]" class="mt16">
<a-col :xs="24"> <a-col :xs="24">
<a-card title="Desempeño por curso" class="card" :bordered="false"> <a-card title="Desempeño por curso" class="card" :bordered="false">
@ -292,7 +286,6 @@ onMounted(async () => {
</script> </script>
<style scoped> <style scoped>
/* Layout */
.page { .page {
padding: 16px; padding: 16px;
max-width: 1120px; max-width: 1120px;
@ -300,12 +293,10 @@ onMounted(async () => {
} }
.mt16 { margin-top: 16px; } .mt16 { margin-top: 16px; }
/* Diseño alternativo: limpio, tipo "scoreboard" */
.alt { .alt {
background: transparent; background: transparent;
} }
/* Header */
.head { .head {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -334,7 +325,6 @@ onMounted(async () => {
} }
.btnSoft, .btnPrimary { border-radius: 12px; font-weight: 900; } .btnSoft, .btnPrimary { border-radius: 12px; font-weight: 900; }
/* Cards */
.card { .card {
border-radius: 18px; border-radius: 18px;
border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06)); border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06));
@ -463,7 +453,6 @@ onMounted(async () => {
.courseName { font-weight: 900; color: #111827; line-height: 1.2; } .courseName { font-weight: 900; color: #111827; line-height: 1.2; }
.courseMeta { margin-top: 2px; font-size: 12px; color: #6b7280; } .courseMeta { margin-top: 2px; font-size: 12px; color: #6b7280; }
/* Pills numéricas (en vez de tags de color) */
.pill { .pill {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -482,10 +471,8 @@ onMounted(async () => {
.pill.bad { border-color: rgba(0,0,0,.10); background: rgba(0,0,0,.04); } .pill.bad { border-color: rgba(0,0,0,.10); background: rgba(0,0,0,.04); }
.pill.neutral { opacity: .92; } .pill.neutral { opacity: .92; }
/* Empty */
.empty { margin-top: 12px; } .empty { margin-top: 12px; }
/* ✅ Responsive completo */
@media (max-width: 768px) { @media (max-width: 768px) {
.headActions { width: 100%; min-width: 0; justify-content: flex-start; } .headActions { width: 100%; min-width: 0; justify-content: flex-start; }
.headSpace { .headSpace {

@ -10,25 +10,16 @@ import voucherBn from "../../assets/images/boletabn.jpg";
const router = useRouter(); const router = useRouter();
const examenStore = useExamenStore(); const examenStore = useExamenStore();
/** =========================
* UI State
* ========================= */
const showModal = ref(false); const showModal = ref(false);
const iniciandoExamen = ref(false); const iniciandoExamen = ref(false);
const creandoExamen = ref(false); const creandoExamen = ref(false);
const formRef = ref(null); const formRef = ref(null);
/**
* SEPARA LOADINGS:
* - loadingPage: solo para cargar data inicial de la página (no debe tumbar modales)
* - loadingAreas: solo para cargar áreas cuando cambias de proceso (se usa en el select)
*/
const loadingPage = ref(false); const loadingPage = ref(false);
const loadingAreas = ref(false); const loadingAreas = ref(false);
/** =========================
* Form
* ========================= */
const formState = reactive({ const formState = reactive({
proceso_id: undefined, proceso_id: undefined,
area_proceso_id: undefined, area_proceso_id: undefined,
@ -36,9 +27,7 @@ const formState = reactive({
codigo_pago: "", codigo_pago: "",
}); });
/** =========================
* Modal Voucher
* ========================= */
const secuenciaModalOpen = ref(false); const secuenciaModalOpen = ref(false);
const secuenciaTipo = ref("caja"); const secuenciaTipo = ref("caja");
@ -74,9 +63,6 @@ const openSecuencia = (tipo) => {
const modalBodyStyle = computed(() => ({ maxHeight: "72vh", overflowY: "auto" })); const modalBodyStyle = computed(() => ({ maxHeight: "72vh", overflowY: "auto" }));
/** =========================
* Datos (computed)
* ========================= */
const hasExamen = computed(() => !!examenStore.examenActual); const hasExamen = computed(() => !!examenStore.examenActual);
const procesoNombre = computed(() => examenStore.examenActual?.proceso?.nombre || "No asignado"); const procesoNombre = computed(() => examenStore.examenActual?.proceso?.nombre || "No asignado");
@ -90,7 +76,7 @@ const estadoTexto = computed(() => {
return yaDioTest.value ? "Completado" : "Listo para iniciar"; return yaDioTest.value ? "Completado" : "Listo para iniciar";
}); });
/** Progreso */
const stepCurrent = computed(() => { const stepCurrent = computed(() => {
if (!hasExamen.value) return 0; if (!hasExamen.value) return 0;
return yaDioTest.value ? 2 : 1; return yaDioTest.value ? 2 : 1;
@ -113,14 +99,12 @@ const estadoAlertDesc = computed(() => {
: "Son 10 preguntas. Al finalizar verás tu resultado al instante."; : "Son 10 preguntas. Al finalizar verás tu resultado al instante.";
}); });
/** ✅ Botones separados (visibilidad) */
const canIniciar = computed(() => hasExamen.value && !yaDioTest.value); const canIniciar = computed(() => hasExamen.value && !yaDioTest.value);
const canVerResultados = computed(() => hasExamen.value && yaDioTest.value); const canVerResultados = computed(() => hasExamen.value && yaDioTest.value);
const openSeleccionArea = () => { const openSeleccionArea = () => {
showModal.value = true; showModal.value = true;
}; };
/** Options */
const procesoOptions = computed(() => const procesoOptions = computed(() =>
(examenStore.procesos || []).map((p) => ({ (examenStore.procesos || []).map((p) => ({
value: p.id, value: p.id,
@ -142,13 +126,9 @@ const tipoPagoOptions = [
{ value: "caja", label: "Caja" }, { value: "caja", label: "Caja" },
]; ];
/**
* IMPORTANTE:
* La condición de pago debe funcionar aunque venga 1/"1"/true/"true"
*/
const normalizeRequierePago = (v) => v === 1 || v === "1" || v === true || v === "true"; const normalizeRequierePago = (v) => v === 1 || v === "1" || v === true || v === "true";
/** Si estás eligiendo proceso en el modal */
const procesoRequierePago = computed(() => { const procesoRequierePago = computed(() => {
const pid = formState.proceso_id; const pid = formState.proceso_id;
if (pid === undefined || pid === null || pid === "") return false; if (pid === undefined || pid === null || pid === "") return false;
@ -161,7 +141,7 @@ const procesoRequierePago = computed(() => {
/** Validación (condicional) */
const rules = { const rules = {
proceso_id: [{ required: true, message: "Selecciona un proceso", trigger: "change" }], proceso_id: [{ required: true, message: "Selecciona un proceso", trigger: "change" }],
area_proceso_id: [{ required: true, message: "Selecciona un área", trigger: "change" }], area_proceso_id: [{ required: true, message: "Selecciona un área", trigger: "change" }],
@ -187,9 +167,7 @@ const rules = {
], ],
}; };
/** =========================
* Actions
* ========================= */
const refrescar = async () => { const refrescar = async () => {
try { try {
await examenStore.fetchExamenActual(); await examenStore.fetchExamenActual();
@ -199,19 +177,14 @@ const refrescar = async () => {
} }
}; };
/**
* SOLO una forma de cargar áreas: por @change
* (NO uses watch(proceso_id) + @change a la vez)
*/
const handleProcesoChange = async (procesoId) => { const handleProcesoChange = async (procesoId) => {
// reset dependientes
formState.area_proceso_id = undefined; formState.area_proceso_id = undefined;
// reset pago cuando cambia proceso
formState.tipo_pago = undefined; formState.tipo_pago = undefined;
formState.codigo_pago = ""; formState.codigo_pago = "";
// limpiar validaciones relacionadas
formRef.value?.clearValidate?.(["area_proceso_id", "tipo_pago", "codigo_pago"]); formRef.value?.clearValidate?.(["area_proceso_id", "tipo_pago", "codigo_pago"]);
if (!procesoId) { if (!procesoId) {
@ -236,10 +209,8 @@ const crearExamen = async () => {
try { try {
creandoExamen.value = true; creandoExamen.value = true;
// valida con reglas condicionales
if (formRef.value) await formRef.value.validate(); if (formRef.value) await formRef.value.validate();
// si requiere pago, se manda en plano al store (payload final lo arma el store)
const pagoData = procesoRequierePago.value const pagoData = procesoRequierePago.value
? { tipo_pago: formState.tipo_pago, codigo_pago: formState.codigo_pago } ? { tipo_pago: formState.tipo_pago, codigo_pago: formState.codigo_pago }
: null; : null;
@ -316,7 +287,6 @@ watch(showModal, (open) => {
if (!open) resetModal(); if (!open) resetModal();
}); });
/** Lifecycle */
onMounted(async () => { onMounted(async () => {
loadingPage.value = true; loadingPage.value = true;
try { try {
@ -329,7 +299,6 @@ onMounted(async () => {
</script> </script>
<template> <template>
<!-- WRAPPER estilo Convocatorias -->
<div class="heroKicker">Test diagnóstico</div> <div class="heroKicker">Test diagnóstico</div>
<div class="heroTitle">Practica y conoce tu nivel</div> <div class="heroTitle">Practica y conoce tu nivel</div>
@ -368,7 +337,6 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- BOTONES DE BANCO -->
<div class="bankActions"> <div class="bankActions">
<div class="bankHead"> <div class="bankHead">
<div class="bankTitle">Requisito de acceso</div> <div class="bankTitle">Requisito de acceso</div>
@ -447,7 +415,6 @@ No se realiza ningún pago adicional.
<a-divider /> <a-divider />
<!-- Progreso -->
<section class="section"> <section class="section">
<div class="sectionTitle">Tu camino</div> <div class="sectionTitle">Tu camino</div>
<div class="sectionSub">Simple, rápido y sin estrés.</div> <div class="sectionSub">Simple, rápido y sin estrés.</div>
@ -467,7 +434,6 @@ No se realiza ningún pago adicional.
<a-divider /> <a-divider />
<!-- ================== MODAL SECUENCIA ================== -->
<a-modal <a-modal
v-model:open="secuenciaModalOpen" v-model:open="secuenciaModalOpen"
:title="secuenciaTitle" :title="secuenciaTitle"
@ -515,7 +481,6 @@ No se realiza ningún pago adicional.
</a-row> </a-row>
</a-modal> </a-modal>
<!-- ================== MODAL SELECCIÓN ÁREA ================== -->
<a-modal <a-modal
v-model:open="showModal" v-model:open="showModal"
title="Seleccionar área" title="Seleccionar área"
@ -585,9 +550,7 @@ No se realiza ningún pago adicional.
</template> </template>
<style scoped> <style scoped>
/* =========================
BASE (estilo Convocatorias)
========================= */
.test-modern { .test-modern {
position: relative; position: relative;
padding: 40px 0; padding: 40px 0;
@ -630,15 +593,13 @@ No se realiza ningún pago adicional.
padding: 0 24px; padding: 0 24px;
} }
/* Helpers */
.mt12 { margin-top: 12px; } .mt12 { margin-top: 12px; }
.mt16 { margin-top: 16px; } .mt16 { margin-top: 16px; }
.mb12 { margin-bottom: 12px; } .mb12 { margin-bottom: 12px; }
.muted { color: #666; line-height: 1.6; } .muted { color: #666; line-height: 1.6; }
/* =========================
HERO (tarjeta principal tipo convocatorias)
========================= */
.hero { .hero {
position: relative; position: relative;
border: none; border: none;
@ -653,7 +614,6 @@ No se realiza ningún pago adicional.
overflow: hidden; overflow: hidden;
} }
/* Badge "Principal" estilo convocatorias */
.hero::after { .hero::after {
position: absolute; position: absolute;
top: -12px; top: -12px;
@ -688,7 +648,6 @@ No se realiza ningún pago adicional.
color: #666; color: #666;
} }
/* Facts */
.heroFacts { .heroFacts {
margin-top: 16px; margin-top: 16px;
display: grid; display: grid;
@ -719,7 +678,7 @@ No se realiza ningún pago adicional.
white-space: nowrap; white-space: nowrap;
} }
/* Banco */
.bankActions { .bankActions {
margin-top: 18px; margin-top: 18px;
padding: 16px; padding: 16px;
@ -751,7 +710,6 @@ No se realiza ningún pago adicional.
font-weight: 700; font-weight: 700;
} }
/* CTA */
.ctaCard { .ctaCard {
border: none; border: none;
border-radius: 16px; border-radius: 16px;
@ -784,7 +742,6 @@ No se realiza ningún pago adicional.
line-height: 1.45; line-height: 1.45;
} }
/* Status pill */
.statusTag { .statusTag {
font-weight: 700; font-weight: 700;
padding: 4px 12px; padding: 4px 12px;
@ -813,7 +770,6 @@ No se realiza ningún pago adicional.
color: #777; color: #777;
} }
/* Botones */
.ctaBtn { .ctaBtn {
height: 52px; height: 52px;
border-radius: 12px; border-radius: 12px;
@ -832,7 +788,6 @@ No se realiza ningún pago adicional.
.ctaDivider { margin: 14px 0 0; } .ctaDivider { margin: 14px 0 0; }
/* Secciones inferiores */
.sectionTitle { .sectionTitle {
font-size: 1.35rem; font-size: 1.35rem;
font-weight: 700; font-weight: 700;
@ -852,7 +807,6 @@ No se realiza ningún pago adicional.
color: #666; color: #666;
} }
/* Modales */
:deep(.voucher-modal .ant-modal-content), :deep(.voucher-modal .ant-modal-content),
:deep(.select-modal .ant-modal-content) { :deep(.select-modal .ant-modal-content) {
border-radius: 16px; border-radius: 16px;
@ -863,7 +817,6 @@ No se realiza ningún pago adicional.
max-width: 92vw; max-width: 92vw;
} }
/* Pago */
.pay-block { .pay-block {
margin-top: 10px; margin-top: 10px;
padding: 12px; padding: 12px;
@ -872,7 +825,6 @@ No se realiza ningún pago adicional.
background: rgba(13, 27, 82, 0.03); background: rgba(13, 27, 82, 0.03);
} }
/* Voucher */
.voucher-caption { .voucher-caption {
margin-bottom: 10px; margin-bottom: 10px;
color: #666; color: #666;
@ -893,52 +845,42 @@ No se realiza ningún pago adicional.
gap: 10px; gap: 10px;
} }
/* Opcional: cards ant-design más “suaves” */
:deep(.ant-card) { border-radius: 12px; } :deep(.ant-card) { border-radius: 12px; }
:deep(.ant-card-bordered) { border: 1px solid rgba(13, 27, 82, 0.10); } :deep(.ant-card-bordered) { border: 1px solid rgba(13, 27, 82, 0.10); }
/* =========================
FULL WIDTH + TOP 0 EN MÓVIL
========================= */
@media (max-width: 768px) { @media (max-width: 768px) {
.hide-mobile{ .hide-mobile{
display: none !important; display: none !important;
} }
/* ✅ Quita el padding superior del wrapper (era 40px) */
.test-modern{ .test-modern{
padding-top: 0 !important; padding-top: 0 !important;
padding-bottom: 24px; /* opcional */ padding-bottom: 24px; /* opcional */
} }
/* ✅ Quita padding lateral del container para que sea edge-to-edge */
.section-container{ .section-container{
max-width: none; max-width: none;
padding: 0 !important; padding: 0 !important;
margin: 0 !important; margin: 0 !important;
} }
/* ✅ Card principal a todo el ancho y pegado arriba */
.hero{ .hero{
grid-template-columns: 1fr; grid-template-columns: 1fr;
width: 100%; width: 100%;
margin: 0 !important; margin: 0 !important;
border-radius: 0 !important; border-radius: 0 !important;
/* importante: sin “espacio arriba” */
padding: 14px 16px 18px !important; padding: 14px 16px 18px !important;
} }
/* ✅ Asegura que el primer texto no empuje hacia abajo */
.heroKicker{ margin-top: 0 !important; } .heroKicker{ margin-top: 0 !important; }
.heroTitle{ margin-top: 6px !important; } .heroTitle{ margin-top: 6px !important; }
.heroText{ margin-top: 8px !important; } .heroText{ margin-top: 8px !important; }
/* Para que la sección “Tu camino” no se pegue a los bordes */
.section{ .section{
padding: 0 16px; padding: 0 16px;
} }
/* Facts */
.heroFacts{ .heroFacts{
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }

@ -1,4 +1,3 @@
<!-- views/Dashboard.vue -->
<template> <template>
<div style="padding: 24px;"> <div style="padding: 24px;">
<a-result <a-result

Loading…
Cancel
Save