main
elmer-20 2 months ago
parent a694d2f958
commit 46545d44a8

@ -1,70 +1,100 @@
<?php
namespace App\Http\Controllers;
use App\Models\ProcesoAdmision;
use App\Models\ProcesoAdmisionDetalle;
use Illuminate\Support\Carbon;
use Illuminate\Http\Request;
class WebController extends Controller
{
public function GetProcesoAdmision()
{
$procesos = ProcesoAdmision::select(
'id',
'titulo',
'subtitulo',
'descripcion',
'slug',
'tipo_proceso',
'modalidad',
'publicado',
'fecha_publicacion',
'fecha_inicio_preinscripcion',
'fecha_fin_preinscripcion',
'fecha_inicio_inscripcion',
'fecha_fin_inscripcion',
'fecha_examen1',
'fecha_examen2',
'fecha_resultados',
'fecha_inicio_biometrico',
'fecha_fin_biometrico',
'imagen_path',
'banner_path',
'brochure_path',
'link_preinscripcion',
'link_inscripcion',
'link_resultados',
'link_reglamento',
'estado',
'created_at',
'updated_at'
)
->with([
'detalles' => function ($query) {
$query->select(
'id',
'proceso_admision_id',
'tipo',
'titulo_detalle',
'descripcion',
'listas',
'meta',
'url',
'imagen_path',
'imagen_path_2',
'created_at',
'updated_at'
);
}
])
->latest()
->get();
public function GetProcesoAdmision()
{
$procesos = ProcesoAdmision::select(
'id',
'titulo',
'subtitulo',
'descripcion',
'slug',
'tipo_proceso',
'modalidad',
'publicado',
'fecha_publicacion',
'fecha_inicio_preinscripcion',
'fecha_fin_preinscripcion',
'fecha_inicio_inscripcion',
'fecha_fin_inscripcion',
'fecha_examen1',
'fecha_examen2',
'fecha_resultados',
'fecha_inicio_biometrico',
'fecha_fin_biometrico',
'imagen_path',
'banner_path',
'brochure_path',
'link_preinscripcion',
'link_inscripcion',
'link_resultados',
'link_reglamento',
'estado',
'created_at',
'updated_at'
)
->with([
'detalles' => function ($query) {
$query->select(
'id',
'proceso_admision_id',
'tipo',
'titulo_detalle',
'descripcion',
'listas',
'meta',
'url',
'imagen_path',
'imagen_path_2',
'created_at',
'updated_at'
);
}
])
->latest() // 🔥 Esto ordena por created_at DESC
->get();
return response()->json([
'success' => true,
'data' => $procesos
]);
}
return response()->json([
'success' => true,
'data' => $procesos
]);
}
public function obtenerProcesosDisponiblesPreinscripcion(Request $request)
{
$now = Carbon::now();
$postulante = $request->user();
$procesos = ProcesoAdmision::query()
->select([
'id',
'titulo',
'slug',
'link_preinscripcion',
'fecha_inicio_preinscripcion',
'fecha_fin_preinscripcion',
])
->where('publicado', 1)
->whereIn('estado', ['publicado', 'en_proceso'])
->whereNotNull('link_preinscripcion')
->whereNotNull('fecha_inicio_preinscripcion')
->whereNotNull('fecha_fin_preinscripcion')
->where('fecha_inicio_preinscripcion', '<=', $now)
->where('fecha_fin_preinscripcion', '>=', $now)
->orderByDesc('fecha_inicio_preinscripcion')
->orderBy('titulo')
->get();
return response()->json([
'success' => true,
'data' => $procesos
]);
}
}

@ -52,12 +52,9 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () {
Route::put('/areas/{id}', [AreaController::class, 'update']);
Route::delete('/areas/{id}', [AreaController::class, 'destroy']);
Route::patch('/areas/{id}/toggle', [AreaController::class, 'toggleEstado']);
Route::post('/areas/{area}/vincular-cursos', [AreaController::class, 'vincularCursosArea']);
Route::post('/areas/{area}/desvincular-curso', [AreaController::class, 'desvincularCursoArea']);
Route::get('/areas/{area}/cursos-disponibles', [AreaController::class, 'getCursosPorArea']);
Route::post('areas/{area}/vincular-procesos', [AreaController::class, 'vincularProcesosArea']);
Route::get('areas/{area}/procesos-disponibles', [AreaController::class, 'getProcesosPorArea'] );
Route::post('areas/{area}/desvincular-procesos', [AreaController::class, 'desvincularProcesoArea'] );
@ -67,19 +64,14 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () {
Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () {
// NOTICIAS
Route::get('/noticias', [NoticiaController::class, 'index']);
Route::get('/noticias/{noticia}', [NoticiaController::class, 'show']);
Route::post('/noticias', [NoticiaController::class, 'store']);
// usa SOLO UNA (PUT o PATCH). Aquí dejo PUT:
Route::put('/noticias/{noticia}', [NoticiaController::class, 'update']);
Route::delete('/noticias/{noticia}', [NoticiaController::class, 'destroy']);
});
Route::get('/noticias', [NoticiaController::class, 'index']);
Route::get('/noticias/{noticia:slug}', [NoticiaController::class, 'showPublic']);
Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () {
@ -99,8 +91,6 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () {
});
Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () {
Route::get('cursos/{cursoId}/preguntas', [PreguntaController::class, 'getPreguntasCurso']);
@ -113,7 +103,6 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () {
Route::middleware('auth:sanctum')->group(function () {
Route::get('/calificaciones', [CalificacionController::class, 'index']);
Route::post('/calificaciones', [CalificacionController::class, 'store']);
Route::get('/calificaciones/{id}', [CalificacionController::class, 'show']);
@ -122,17 +111,11 @@ Route::middleware('auth:sanctum')->group(function () {
});
Route::prefix('postulante')->group(function () {
// Registro
Route::post('/register', [PostulanteAuthController::class, 'register']);
// Login
Route::post('/login', [PostulanteAuthController::class, 'login']);
// Rutas protegidas por token
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [PostulanteAuthController::class, 'logout']);
Route::get('/me', [PostulanteAuthController::class, 'me']);
@ -142,10 +125,8 @@ Route::prefix('postulante')->group(function () {
});
});
// Route::middleware('auth:sanctum')->group(function () {
// Route::get('/procesos', [ExamenController::class, 'procesoexamen']);
// Route::get('/areas', [ExamenController::class, 'areas']);
@ -157,51 +138,35 @@ Route::prefix('postulante')->group(function () {
Route::middleware(['auth:sanctum'])->prefix('area-proceso')->group(function () {
Route::get('areasprocesos', [ReglaAreaProcesoController::class, 'areasProcesos']);
Route::prefix('{areaProcesoId}/reglas')->group(function () {
Route::get('/', [ReglaAreaProcesoController::class, 'index']); // Listar reglas
Route::post('/', [ReglaAreaProcesoController::class, 'store']); // Crear/actualizar regla individual
Route::post('/multiple', [ReglaAreaProcesoController::class, 'storeMultiple']); // Guardar múltiples reglas
Route::get('/', [ReglaAreaProcesoController::class, 'index']);
Route::post('/', [ReglaAreaProcesoController::class, 'store']);
Route::post('/multiple', [ReglaAreaProcesoController::class, 'storeMultiple']);
});
});
Route::middleware(['auth:sanctum'])->prefix('reglas')->group(function () {
Route::put('/{reglaId}', [ReglaAreaProcesoController::class, 'update']); // Editar regla
Route::delete('/{reglaId}', [ReglaAreaProcesoController::class, 'destroy']); // Eliminar regla
Route::put('/{reglaId}', [ReglaAreaProcesoController::class, 'update']);
Route::delete('/{reglaId}', [ReglaAreaProcesoController::class, 'destroy']);
});
// Examen - Flujo separado
Route::middleware(['auth:postulante'])->group(function () {
Route::middleware(['auth:sanctum'])->group(function () {
Route::get('/examen/procesos', [ExamenController::class, 'procesoexamen']);
Route::get('/examen/areas', [ExamenController::class, 'areas']);
Route::get('/examen/actual', [ExamenController::class, 'miExamenActual']);
// Crear examen (sin preguntas)
Route::post('/examen/crear', [ExamenController::class, 'crearExamen']);
// Generar preguntas
Route::post('/examen/{examen}/generar-preguntas', [ExamenController::class, 'generarPreguntas']);
// Obtener preguntas
Route::get('/examen/{examen}/preguntas', [ExamenController::class, 'obtenerPreguntas']);
// Iniciar examen (marcar hora inicio)
Route::post('/examen/iniciar', [ExamenController::class, 'iniciarExamen']);
// Responder preguntas
Route::post('/examen/pregunta/{pregunta}/responder', [ExamenController::class, 'responderPregunta']);
// Finalizar examen
Route::post('/examen/{examen}/finalizar', [ExamenController::class, 'finalizarExamen']);
Route::post('/examen/{examenId}/calificar', [ExamenController::class, 'calificarExamen']);
});
Route::middleware('auth:sanctum')->prefix('admin')->group(function () {
Route::middleware('auth:sanctum')->prefix('admin')->group(function () {
// PROCESOS
Route::prefix('procesos-admision')->group(function () {
Route::get('/', [ProcesoAdmisionController::class, 'index'])->name('procesos-admision.index');
Route::post('/', [ProcesoAdmisionController::class, 'store'])->name('procesos-admision.store');
@ -219,8 +184,16 @@ Route::middleware(['auth:postulante'])->group(function () {
Route::delete('/{id}', [ProcesoAdmisionDetalleController::class, 'destroy'])->name('detalles-admision.destroy');
});
});
});
Route::middleware('auth:sanctum')->group(function () {
Route::get('/procesos-disponibles-preinscripcion', [WebController::class, 'obtenerProcesosDisponiblesPreinscripcion']);
});
Route::get('/procesos-admision', [WebController::class, 'GetProcesoAdmision']);
Route::get('/noticias', [NoticiaController::class, 'index']);
Route::get('/noticias/{noticia:slug}', [NoticiaController::class, 'showPublic']);

@ -54,11 +54,10 @@
</div>
<div class="tiny-hint">
Tip: Si ves 🟢 en una fecha, significa que esa etapa está activa hoy.
Si ves 🟢 en una fecha, significa que esa etapa está activa hoy.
</div>
</div>
<!-- Qué hacer -->
<div class="help-item">
<div class="help-label">2) ¿Qué debo hacer ahora?</div>

@ -122,11 +122,11 @@ onMounted(() => {
}
.markdown-content :deep(.katex-display) {
margin: 0.75em 0;
text-align: center;
overflow-x: auto; /* si es largo, que haga scroll horizontal */
overflow-y: hidden; /* IMPORTANTE: no scroll vertical -> no flechas */
margin: 14px 0; /* espacio arriba/abajo */
padding: 10px 0; /* aire dentro del bloque */
line-height: 1.25; /* evita que el contenedor aplaste */
overflow-x: auto;
overflow-y: visible; /* ✅ CLAVE: que NO recorte arriba/abajo */
}
/* Quitar botones/flechas del scrollbar (Chrome/Edge) */
.markdown-content :deep(.katex-display::-webkit-scrollbar-button) {

@ -1,122 +1,118 @@
<template>
<a-spin :spinning="loading">
<div class="section-container">
<div class="page">
<!-- Topbar -->
<div class="topbar">
<div class="topbarLeft">
<div class="hello">Bienvenido, {{ authStore.userName }}</div>
<div class="sub">DNI: {{ authStore.userDni || "No registrado" }}</div>
</div>
<div class="section-container">
<div class="page">
<!-- Topbar -->
<div class="topbar">
<div class="topbarLeft">
<div class="hello">Bienvenido, {{ authStore.userName }}</div>
<div class="sub">DNI: {{ authStore.userDni || "No registrado" }}</div>
</div>
</div>
<a-divider class="softDivider" />
<a-card :bordered="false" class="testBox">
<div class="testHead">
<div class="testHeadLeft">
<div class="testTitle">Tu test diagnóstico</div>
<div class="testSub">
Es un <b>test referencial</b> para practicar y medir tu nivel. <b>No afecta</b> tu postulación oficial.
</div>
<a-divider class="softDivider" />
<div class="mt12">
<a-space direction="vertical" size="small">
<div class="bulletRow">
<span class="dot" />
<span>10 preguntas 10 min aprox. resultado inmediato</span>
</div>
<div class="bulletRow">
<span class="dot" />
<span>
Si tu proceso pide secuencia, también puedes usar la del pago de tu
<b>Carpeta de Postulante</b> (no pagas extra por este test).
</span>
</div>
</a-space>
</div>
<!-- Test -->
<a-card :bordered="false" class="testBox">
<div class="testHead">
<div class="testHeadLeft">
<div class="testTitle">Tu test diagnóstico</div>
<div class="testSub">
Es un <b>test referencial</b> para practicar y medir tu nivel. <b>No afecta</b> tu postulación oficial.
</div>
<div class="testHeadRight">
<a-button type="primary" size="large" class="btnTest" :disabled="!canGoTest" @click="goToTest" block>
Ir al test
</a-button>
<div class="microHelp" v-if="!canGoTest">Aún no tienes un test asignado.</div>
<div class="mt12">
<a-space direction="vertical" size="small">
<div class="bulletRow">
<span class="dot" />
<span>10 preguntas 10 min aprox. resultado inmediato</span>
</div>
<div class="bulletRow">
<span class="dot" />
<span>
Si tu proceso pide secuencia, también puedes usar la del pago de tu
<b>Carpeta de Postulante</b> (no pagas extra por este test).
</span>
</div>
</a-space>
</div>
</div>
</a-card>
<a-divider class="softDivider" />
<div class="sectionHead">
<div>
<div class="sectionTitle">Procesos activos</div>
<div class="sectionSub">Revisa los procesos habilitados y postula cuando corresponda.</div>
</div>
<div class="testHeadRight">
<a-button
type="primary"
size="large"
class="btnTest"
:disabled="!canGoTest"
@click="goToTest"
block
>
Ir al test
</a-button>
<div class="sectionRight">
<a-badge count="Nuevo" class="new-badge" />
</div>
<div class="microHelp" v-if="!canGoTest">Aún no tienes un test asignado.</div>
</div>
</div>
</a-card>
<div class="tableWrap mt12">
<a-table
:columns="processColumns"
:data-source="state.processes"
row-key="id"
:loading="loading"
:pagination="{ pageSize: 6 }"
:scroll="{ x: 720 }"
class="modernTable"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag class="status-tag" :color="record.statusColor">{{ record.statusText }}</a-tag>
</template>
<template v-else-if="column.key === 'eligibility'">
<a-badge
:status="record.isEligible ? 'success' : 'error'"
:text="record.isEligible ? 'Apto' : 'No apto'"
/>
</template>
<template v-else-if="column.key === 'actions'">
<div class="actions">
<a-button size="small" block class="actionBtn" @click="onViewProcess(record)">Ver</a-button>
<a-button
size="small"
type="primary"
block
class="actionBtnPrimary"
:disabled="!record.canApply"
@click="onApply(record)"
>
Postular
</a-button>
</div>
<div v-if="record.canApply === false && record.blockReason" class="microHelp">
{{ record.blockReason }}
</div>
</template>
</template>
</a-table>
</div>
<a-divider class="softDivider" />
<!-- Procesos -->
<div class="sectionHead">
<div>
<div class="sectionTitle">Procesos activos</div>
<div class="sectionSub">Revisa los procesos habilitados y postula cuando corresponda.</div>
</div>
<a-empty
v-if="!loading && state.processes.length === 0"
class="mt12"
description="No hay procesos activos por el momento."
/>
<div class="sectionRight">
<a-badge count="Nuevo" class="new-badge" />
</div>
</div>
<div class="tableWrap mt12">
<a-table
:columns="processColumns"
:data-source="state.processes"
row-key="id"
:loading="loading"
:pagination="{ pageSize: 6 }"
:scroll="{ x: 720 }"
class="modernTable"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'fecha_inicio_preinscripcion'">
{{ formatDate(record.fecha_inicio_preinscripcion) }}
</template>
<template v-else-if="column.key === 'fecha_fin_preinscripcion'">
{{ formatDate(record.fecha_fin_preinscripcion) }}
</template>
<template v-else-if="column.key === 'actions'">
<div class="actions">
<a-button
size="small"
type="primary"
block
class="actionBtnPrimary"
:disabled="!record.link_preinscripcion"
@click="onApply(record)"
>
Postular
</a-button>
</div>
</template>
</template>
</a-table>
</div>
<a-empty
v-if="!loading && state.processes.length === 0"
class="mt12"
description="No hay procesos activos por el momento."
/>
</div>
</div>
</a-spin>
</template>
@ -125,10 +121,7 @@ import { computed, onMounted, reactive, ref } from "vue";
import { Modal, message } from "ant-design-vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "../../store/postulanteStore";
const ROUTE_TEST_PANEL = { name: "PanelTest" };
const ROUTE_PROCESS_DETAIL = (id) => ({ name: "ProcesoDetalle", params: { id } });
import axios from "../../axiosPostulante";
const router = useRouter();
const authStore = useAuthStore();
@ -143,56 +136,35 @@ const state = reactive({
const canGoTest = computed(() => !!state.test.hasAssigned);
const processColumns = computed(() => [
{ title: "Proceso", dataIndex: "name", key: "name", ellipsis: true },
{ title: "Estado", key: "status", responsive: ["sm"] },
{ title: "Aptitud", key: "eligibility", responsive: ["md"] },
{ title: "Título", dataIndex: "titulo", key: "titulo", ellipsis: true },
{ title: "Inicio preinscripción", dataIndex: "fecha_inicio_preinscripcion", key: "fecha_inicio_preinscripcion" },
{ title: "Fin preinscripción", dataIndex: "fecha_fin_preinscripcion", key: "fecha_fin_preinscripcion" },
{ title: "Acciones", key: "actions" },
]);
function formatDate(val) {
if (!val) return "-";
if (typeof val === "string" && val.includes("T")) return val.split("T")[0];
const d = new Date(val);
if (isNaN(d.getTime())) return String(val);
return d.toLocaleDateString();
}
const api = {
async getDashboard() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
test: { hasAssigned: true },
processes: [
{
id: 10,
name: "Admisión 2026-I",
statusText: "ABIERTO",
statusColor: "blue",
isEligible: true,
canApply: true,
blockReason: "",
},
{
id: 11,
name: "Admisión Extraordinaria 2026",
statusText: "PRONTO",
statusColor: "gold",
isEligible: false,
canApply: false,
blockReason: "Este proceso aún no permite postular.",
},
],
});
}, 250);
});
},
async applyToProcess(processId) {
return new Promise((resolve) => setTimeout(resolve, 300));
async getProcesosActivos() {
const { data } = await axios.get("/procesos-disponibles-preinscripcion");
return data;
},
};
async function fetchDashboard() {
loading.value = true;
try {
const data = await api.getDashboard();
state.test = { ...state.test, ...data.test };
state.processes = Array.isArray(data.processes) ? data.processes : [];
} catch {
message.error("No se pudo cargar el dashboard.");
const resp = await api.getProcesosActivos();
state.processes = Array.isArray(resp?.data) ? resp.data : [];
} catch (e) {
console.error(e);
state.processes = [];
} finally {
loading.value = false;
}
@ -212,91 +184,33 @@ function goToTest() {
});
}
function onViewProcess(process) {
message.info(`Abrir detalle del proceso: ${process.name}`);
}
function onApply(process) {
if (!process.canApply) return;
Modal.confirm({
title: "Confirmar postulación",
content: `¿Deseas postular al proceso "${process.name}"?`,
okText: "Postular",
cancelText: "Cancelar",
async onOk() {
try {
await api.applyToProcess(process.id);
message.success("Postulación registrada.");
await fetchDashboard();
} catch {
message.error("No se pudo completar la postulación.");
}
},
});
if (!process.link_preinscripcion) return;
window.open(process.link_preinscripcion, "_blank", "noopener,noreferrer");
}
onMounted(fetchDashboard);
</script>
<style scoped>
.dashboard-modern {
position: relative;
padding: 40px 0;
font-family: "Times New Roman", Times, serif;
background: #fbfcff;
overflow: hidden;
}
.dashboard-modern::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
background-image:
repeating-linear-gradient(
to right,
rgba(13, 27, 82, 0.06) 0,
rgba(13, 27, 82, 0.06) 1px,
transparent 1px,
transparent 24px
),
repeating-linear-gradient(
to bottom,
rgba(13, 27, 82, 0.06) 0,
rgba(13, 27, 82, 0.06) 1px,
transparent 1px,
transparent 24px
);
opacity: 0.55;
}
/* ====== BASE ====== */
.section-container {
position: relative;
z-index: 1;
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
.page {
width: 100%;
max-width: 1120px;
margin: 0 auto;
padding: 0;
}
.mt12 { margin-top: 12px; }
.microHelp { font-size: 0.95rem; color: #666; margin-top: 6px; }
.softDivider { margin: 18px 0; }
.microHelp { font-size: 12px; color: #6b7280; margin-top: 8px; }
.softDivider { margin: 16px 0; }
/* ====== TOPBAR ====== */
.topbar {
display: flex;
justify-content: space-between;
@ -306,91 +220,62 @@ onMounted(fetchDashboard);
}
.hello {
font-size: 2rem;
font-weight: 700;
color: #0d1b52;
font-size: 28px;
font-weight: 900;
color: #0f172a;
margin: 0;
letter-spacing: 0.2px;
}
.sub {
margin-top: 6px;
font-size: 1rem;
color: #666;
}
.topbarRight {
min-width: 260px;
display: flex;
justify-content: flex-end;
}
.btnTop {
min-width: 180px;
height: 46px;
border-radius: 10px;
font-weight: 700;
font-size: 14px;
color: #64748b;
}
/* ====== TEST CARD ====== */
.testBox {
position: relative;
border: none;
border-radius: 16px;
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 18px;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
background: #fff;
}
.testBox :deep(.ant-card-body) {
padding: 28px;
}
.cardBadge {
position: absolute;
top: -12px;
left: 24px;
background: linear-gradient(45deg, #1890ff, #52c41a);
color: #fff;
padding: 6px 16px;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 700;
padding: 24px;
}
.testHead {
display: flex;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
align-items: flex-start;
display: grid;
grid-template-columns: 1fr 300px;
gap: 18px;
align-items: start;
}
.testTitle {
font-size: 1.55rem;
font-weight: 700;
color: #1a237e;
font-size: 20px;
font-weight: 900;
color: #0f172a;
}
.testSub {
margin-top: 8px;
font-size: 1rem;
color: #666;
line-height: 1.55;
font-size: 14px;
color: #475569;
line-height: 1.6;
max-width: 760px;
}
.testHeadRight {
min-width: 260px;
display: flex;
flex-direction: column;
display: grid;
gap: 8px;
align-items: flex-end;
align-content: start;
}
.btnTest {
height: 52px;
height: 48px;
border-radius: 12px;
font-weight: 700;
background: linear-gradient(135deg, #1a237e 0%, #283593 100%);
font-weight: 900;
border: none;
}
@ -399,21 +284,21 @@ onMounted(fetchDashboard);
display: flex;
gap: 10px;
align-items: flex-start;
font-size: 1rem;
color: #666;
line-height: 1.55;
font-size: 14px;
color: #475569;
line-height: 1.6;
}
.dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: #1a237e;
background: #1677ff;
margin-top: 9px;
flex: 0 0 auto;
}
/* ====== SECTION HEAD ====== */
.sectionHead {
display: flex;
justify-content: space-between;
@ -422,34 +307,33 @@ onMounted(fetchDashboard);
align-items: flex-start;
}
.sectionRight { margin-top: 4px; }
.sectionTitle {
font-size: 1.55rem;
font-weight: 700;
color: #0d1b52;
font-size: 20px;
font-weight: 900;
color: #0f172a;
margin: 0;
}
.sectionSub {
margin-top: 8px;
font-size: 1rem;
color: #666;
line-height: 1.55;
margin-top: 6px;
font-size: 14px;
color: #64748b;
line-height: 1.6;
}
/* Badge Nuevo (como convocatorias) */
.new-badge :deep(.ant-badge-count) {
background: linear-gradient(45deg, #ff6b6b, #ffd700);
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.3);
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.25);
border-radius: 999px;
}
/* ====== TABLE WRAP ====== */
.tableWrap {
width: 100%;
overflow-x: auto;
border-radius: 16px;
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.08);
border-radius: 18px;
border: 1px solid rgba(15, 23, 42, 0.08);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
background: #fff;
padding: 8px;
}
@ -460,86 +344,92 @@ onMounted(fetchDashboard);
:deep(.modernTable .ant-table-container) {
overflow-x: auto;
border-radius: 12px;
border-radius: 14px;
}
:deep(.modernTable .ant-table-thead > tr > th) {
background: rgba(13, 27, 82, 0.03);
color: #0d1b52;
font-weight: 700;
background: rgba(15, 23, 42, 0.03);
color: #0f172a;
font-weight: 900;
}
:deep(.modernTable .ant-table-tbody > tr > td) {
color: #666;
}
.status-tag {
font-weight: 700;
padding: 4px 12px;
border-radius: 999px;
white-space: nowrap;
color: #475569;
}
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: 1fr;
gap: 8px;
}
.actionBtn {
border-radius: 10px;
height: 38px;
font-weight: 700;
}
.actionBtnPrimary {
border-radius: 10px;
height: 38px;
font-weight: 700;
height: 36px;
font-weight: 900;
}
/* ====== RESPONSIVE ====== */
@media (max-width: 992px) {
.section-container {
padding: 0 16px;
}
@media (max-width: 768px) {
.hide-mobile{
display: none !important;
.testHead {
grid-template-columns: 1fr;
}
.test-modern{
padding-top: 0 !important;
padding-bottom: 24px;
.testHeadRight {
justify-items: stretch;
}
}
.section-container{
@media (max-width: 768px) {
.section-container {
max-width: none;
padding: 0 !important;
margin: 0 !important;
}
.hero{
grid-template-columns: 1fr;
width: 100%;
margin: 0 !important;
border-radius: 0 !important;
.page {
max-width: none;
margin: 0;
padding: 0;
}
.softDivider {
margin: 12px 0;
}
padding: 14px 16px 18px !important;
.hello {
font-size: 22px;
}
.heroKicker{ margin-top: 0 !important; }
.heroTitle{ margin-top: 6px !important; }
.heroText{ margin-top: 8px !important; }
/* card full width en móvil (pro) */
.testBox,
.tableWrap {
border-radius: 0;
border-left: 0;
border-right: 0;
}
.section{
padding: 0 16px;
.testBox :deep(.ant-card-body) {
padding: 14px;
}
.heroFacts{
grid-template-columns: repeat(2, minmax(0, 1fr));
.tableWrap {
padding: 6px;
}
}
@media (max-width: 576px){
.heroFacts{
grid-template-columns: 1fr;
@media (max-width: 480px) {
.hello {
font-size: 20px;
}
.testSub,
.bulletRow,
.sectionSub {
font-size: 13px;
}
}
</style>
</style>

@ -690,23 +690,16 @@ onUnmounted(() => {
height: 100%;
}
@media (max-width: 992px) {
.main-layout,
.main-layout.layout-collapsed {
margin-left: 0 !important;
}
.content-container {
max-width: 100%;
}
}
@media (max-width: 768px) {
.header,
.header-container {
height: 56px;
}
.header-container {
padding: 0 12px; /* ✅ menos padding lateral en móvil */
}
.sidebar,
.sidebar.sidebar-mobile {
top: 56px;
@ -714,8 +707,8 @@ onUnmounted(() => {
}
.logo-img {
height: 36px;
width: 36px;
height: 34px;
width: 34px;
}
.portal-title {
@ -726,12 +719,58 @@ onUnmounted(() => {
display: none;
}
/* ✅ CONTENT: padding externo pequeño (se ve pro y no encoge tanto) */
.content {
padding: 12px;
padding: 8px !important;
background: var(--ant-colorBgLayout, #f5f5f5);
}
.content-container {
max-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
}
/* ✅ CARD: sin borde/sombra pesada en móvil y radio moderado */
.content-card {
border-radius: 14px;
border-radius: 12px !important;
margin: 0 !important;
border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06)) !important;
box-shadow: 0 6px 16px rgba(0,0,0,.06) !important;
background: #fff;
overflow: hidden;
}
/* ✅ CLAVE: padding interno del body (no exagerado) */
.content-card :deep(.ant-card-body) {
padding: 12px !important;
}
/* ✅ opcional: quita el “papel cuadriculado” en móvil (se ve más limpio) */
.content-card::before {
display: none !important;
}
}
/* Extra: teléfonos chicos (más pro aún) */
@media (max-width: 480px) {
.content {
padding: 6px !important;
}
.content-card :deep(.ant-card-body) {
padding: 10px !important;
}
}
/* ===== FIX MÓVIL REAL ===== */
@media (max-width: 992px) {
.main-layout,
.main-layout.layout-collapsed {
margin-left: 0 !important;
}
.sidebar {
position: fixed !important;
left: 0;
}
}
</style>

File diff suppressed because it is too large Load Diff

@ -1,169 +1,171 @@
<!-- PanelResultados.vue (DISEÑO ALTERNATIVO: "Scoreboard" moderno, full responsive) -->
<!-- PanelResultados.vue (DASH STYLE + RESPONSIVE PRO) -->
<template>
<div class="page alt">
<div class="head">
<div>
<div class="hTitle">Resultados del examen</div>
<div class="hSub">Resumen claro + detalle por curso.</div>
</div>
<div class="headActions">
<a-space class="headSpace">
<a-button class="btnSoft" @click="volver" :disabled="cargando">Volver</a-button>
<a-button class="btnPrimary" type="primary" @click="recalcular" :loading="cargando">
Actualizar
</a-button>
</a-space>
</div>
</div>
<div class="dashboard-modern">
<a-spin :spinning="cargando" tip="Cargando resultados...">
<template v-if="resultado">
<a-row :gutter="[16, 16]">
<a-col :xs="24" :lg="10">
<a-card class="card scoreCard" :bordered="false">
<div class="scoreTop">
<div class="chipDone">
<span class="chipDot" />
Calificado
</div>
<div v-if="resultado.orden_merito != null" class="chipRank">
Puesto: <b>#{{ resultado.orden_merito }}</b>
</div>
</div>
<div class="scoreMain">{{ fmt2(resultado.total_puntos) }}</div>
<div class="scoreLabel">Puntaje total</div>
<div class="bigProgress">
<a-progress :percent="notaPercent" />
<div class="bigProgressMeta">
<!-- <span>Nota equivalente</span>
<b>{{ fmt2(resultado.calificacion_sobre_20) }}/20</b> -->
</div>
</div>
<a-alert
class="advice"
show-icon
type="success"
message="Consejo"
:description="consejo"
/>
</a-card>
</a-col>
<a-col :xs="24" :lg="14">
<div class="section-container">
<div class="page alt">
<!-- Head -->
<div class="head">
<div>
<div class="hTitle">Resultados del examen</div>
<div class="hSub">Resumen claro + detalle por curso.</div>
</div>
<div class="headActions">
<a-space class="headSpace">
<a-button class="btnSoft" @click="volver" :disabled="cargando">Volver</a-button>
<a-button class="btnPrimary" type="primary" @click="recalcular" :loading="cargando">
Actualizar
</a-button>
</a-space>
</div>
</div>
<template v-if="resultado">
<a-row :gutter="[16, 16]">
<a-col :xs="24" :sm="12" :md="8">
<a-card class="card kpi" :bordered="false">
<div class="kpiK">Correctas</div>
<div class="kpiV">{{ resultado.total_correctas }}</div>
<div class="kpiHint">Aciertos</div>
</a-card>
</a-col>
<a-col :xs="24" :lg="10">
<a-card class="card scoreCard" :bordered="false">
<div class="scoreTop">
<div class="chipDone">
<span class="chipDot" />
Calificado
</div>
<a-col :xs="24" :sm="12" :md="8">
<a-card class="card kpi" :bordered="false">
<div class="kpiK">Incorrectas</div>
<div class="kpiV">{{ resultado.total_incorrectas }}</div>
<div class="kpiHint">Errores</div>
</a-card>
</a-col>
<div v-if="resultado.orden_merito != null" class="chipRank">
Puesto: <b>#{{ resultado.orden_merito }}</b>
</div>
</div>
<div class="scoreMain">{{ fmt2(resultado.total_puntos) }}</div>
<div class="scoreLabel">Puntaje total</div>
<a-col :xs="24" :sm="12" :md="8">
<a-card class="card kpi" :bordered="false">
<div class="kpiK">Blanco/Nulas</div>
<div class="kpiV">{{ resultado.total_nulas }}</div>
<div class="kpiHint">Sin respuesta</div>
<div class="bigProgress">
<a-progress :percent="notaPercent" />
<div class="bigProgressMeta"></div>
</div>
<a-alert
class="advice"
show-icon
type="success"
message="Consejo"
:description="consejo"
/>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12">
<a-card class="card kpiWide" :bordered="false">
<div class="kpiWideRow">
<div>
<div class="kpiK">% Correctas</div>
<div class="kpiV">{{ fmt2(resultado.porcentaje_correctas) }}%</div>
</div>
<a-col :xs="24" :lg="14">
<a-row :gutter="[16, 16]">
<a-col :xs="24" :sm="12" :md="8">
<a-card class="card kpi" :bordered="false">
<div class="kpiK">Correctas</div>
<div class="kpiV">{{ resultado.total_correctas }}</div>
<div class="kpiHint">Aciertos</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="8">
<a-card class="card kpi" :bordered="false">
<div class="kpiK">Incorrectas</div>
<div class="kpiV">{{ resultado.total_incorrectas }}</div>
<div class="kpiHint">Errores</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="8">
<a-card class="card kpi" :bordered="false">
<div class="kpiK">Blanco/Nulas</div>
<div class="kpiV">{{ resultado.total_nulas }}</div>
<div class="kpiHint">Sin respuesta</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12">
<a-card class="card kpiWide" :bordered="false">
<div class="kpiWideRow">
<div>
<div class="kpiK">% Correctas</div>
<div class="kpiV">{{ fmt2(resultado.porcentaje_correctas) }}%</div>
</div>
<div class="miniCircle">
<a-progress type="circle" :percent="notaPercent" :width="96" />
<div class="miniCircleLabel">
<div class="miniCircleMain">{{ fmt2(resultado.calificacion_sobre_20) }}</div>
<div class="miniCircleSub">/20</div>
<div class="miniCircle">
<a-progress type="circle" :percent="notaPercent" :width="96" />
<div class="miniCircleLabel">
<div class="miniCircleMain">{{ fmt2(resultado.calificacion_sobre_20) }}</div>
<div class="miniCircleSub">/20</div>
</div>
</div>
</div>
</div>
</div>
</a-card>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12">
<a-card class="card noteCard" :bordered="false">
<div class="kpiK">Nota sobre 20</div>
<div class="kpiV">{{ fmt2(resultado.calificacion_sobre_20) }}/20</div>
<div class="kpiHint">Equivalencia según puntaje máximo del proceso</div>
</a-card>
</a-col>
</a-row>
</a-col>
</a-row>
<a-col :xs="24" :sm="12" :md="12">
<a-card class="card noteCard" :bordered="false">
<div class="kpiK">Nota sobre 20</div>
<div class="kpiV">{{ fmt2(resultado.calificacion_sobre_20) }}/20</div>
<div class="kpiHint">Equivalencia según puntaje máximo del proceso</div>
<a-row :gutter="[16, 16]" class="mt16">
<a-col :xs="24">
<a-card title="Desempeño por curso" class="card" :bordered="false">
<div class="tableWrap">
<a-table
:columns="columns"
:dataSource="rowsCursos"
:pagination="false"
size="middle"
rowKey="curso"
:scroll="{ x: 760 }"
class="modernTable"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'curso'">
<div class="courseCell">
<span class="courseDot"></span>
<div>
<div class="courseName">{{ record.curso }}</div>
<div class="courseMeta">{{ record.ratio }}%</div>
</div>
</div>
</template>
<template v-else-if="column.dataIndex === 'correctas'">
<span class="pill good">{{ record.correctas }}</span>
</template>
<template v-else-if="column.dataIndex === 'incorrectas'">
<span class="pill bad">{{ record.incorrectas }}</span>
</template>
<template v-else-if="column.dataIndex === 'total'">
<span class="pill neutral">{{ record.total }}</span>
</template>
<template v-else-if="column.dataIndex === 'ratio'">
<a-progress :percent="record.ratio" />
</template>
</template>
</a-table>
</div>
</a-card>
</a-col>
</a-row>
</a-col>
</a-row>
<a-row :gutter="[16, 16]" class="mt16">
<a-col :xs="24">
<a-card title="Desempeño por curso" class="card" :bordered="false">
<div class="tableWrap">
<a-table
:columns="columns"
:dataSource="rowsCursos"
:pagination="false"
size="middle"
rowKey="curso"
:scroll="{ x: 760 }"
class="modernTable"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'curso'">
<div class="courseCell">
<span class="courseDot"></span>
<div>
<div class="courseName">{{ record.curso }}</div>
<div class="courseMeta">{{ record.ratio }}%</div>
</div>
</div>
</template>
<template v-else-if="column.dataIndex === 'correctas'">
<span class="pill good">{{ record.correctas }}</span>
</template>
<template v-else-if="column.dataIndex === 'incorrectas'">
<span class="pill bad">{{ record.incorrectas }}</span>
</template>
<template v-else-if="column.dataIndex === 'total'">
<span class="pill neutral">{{ record.total }}</span>
</template>
<template v-else-if="column.dataIndex === 'ratio'">
<a-progress :percent="record.ratio" />
</template>
</template>
</a-table>
</div>
</a-card>
</a-col>
</a-row>
</template>
<template v-else>
<a-empty description="No hay resultados disponibles" class="empty">
<a-button type="primary" class="btnPrimary" @click="recalcular" :loading="cargando">
Calcular resultados
</a-button>
</a-empty>
</template>
</template>
<template v-else>
<a-empty description="No hay resultados disponibles" class="empty">
<a-button type="primary" class="btnPrimary" @click="recalcular" :loading="cargando">
Calcular resultados
</a-button>
</a-empty>
</template>
</div>
</div>
</a-spin>
</div>
</template>
@ -242,15 +244,17 @@ const rowsCursos = computed(() => {
const rows = Array.from(keys).map((k) => {
const correctas = parseCorrectas(corr?.[k] ?? 0)
const total = parseTotal(k) || (() => {
const val = corr?.[k]
if (typeof val === 'string' && val.includes('de')) {
const [, b] = val.split('de')
const y = Number(b.trim())
return Number.isFinite(y) ? y : 0
}
return 0
})()
const total =
parseTotal(k) ||
(() => {
const val = corr?.[k]
if (typeof val === 'string' && val.includes('de')) {
const [, b] = val.split('de')
const y = Number(b.trim())
return Number.isFinite(y) ? y : 0
}
return 0
})()
const incorrectas = parseIncorrectas(k)
const ratio = total > 0 ? Math.round((correctas / total) * 100) : 0
return { curso: k, correctas, incorrectas, total, ratio }
@ -262,7 +266,6 @@ const rowsCursos = computed(() => {
const recalcular = async () => {
if (!examenId.value) return
cargando.value = true
try {
const r = await examenStore.calificarExamen(examenId.value)
@ -286,17 +289,58 @@ onMounted(async () => {
</script>
<style scoped>
/* ====== MISMO BACKGROUND QUE DASH ====== */
.dashboard-modern {
position: relative;
padding: 22px 0;
font-family: "Times New Roman", Times, serif;
background: #fbfcff;
overflow: hidden;
}
.dashboard-modern::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
background-image:
repeating-linear-gradient(
to right,
rgba(13, 27, 82, 0.06) 0,
rgba(13, 27, 82, 0.06) 1px,
transparent 1px,
transparent 24px
),
repeating-linear-gradient(
to bottom,
rgba(13, 27, 82, 0.06) 0,
rgba(13, 27, 82, 0.06) 1px,
transparent 1px,
transparent 24px
);
opacity: 0.55;
}
.section-container {
position: relative;
z-index: 1;
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
.page {
padding: 16px;
width: 100%;
max-width: 1120px;
margin: 0 auto;
padding: 0;
}
.mt16 { margin-top: 16px; }
.alt {
background: transparent;
}
.mt16 { margin-top: 16px; }
.alt { background: transparent; }
/* ====== HEADER ====== */
.head {
display: flex;
justify-content: space-between;
@ -305,35 +349,77 @@ onMounted(async () => {
align-items: flex-start;
margin-bottom: 14px;
}
.hTitle {
margin: 0;
font-size: 1.85rem;
font-weight: 700;
font-weight: 900;
color: #0d1b52;
line-height: 1.15;
}
.hSub {
margin-top: 4px;
font-size: 12.5px;
color: var(--ant-colorTextSecondary, #6b7280);
line-height: 1.35;
}
.headActions {
min-width: 260px;
display: flex;
justify-content: flex-end;
}
.btnSoft, .btnPrimary { border-radius: 12px; font-weight: 900; }
.btnSoft, .btnPrimary {
border-radius: 12px;
font-weight: 900;
}
/* ====== CARD (igual dash content-card) ====== */
.card {
position: relative;
border-radius: 18px;
border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06));
box-shadow: var(--ant-boxShadowSecondary, 0 10px 28px rgba(0,0,0,.08));
background: var(--ant-colorBgContainer, #fff);
overflow: hidden;
}
.card::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
background-image:
repeating-linear-gradient(
to right,
rgba(13, 27, 82, 0.05) 0,
rgba(13, 27, 82, 0.05) 1px,
transparent 1px,
transparent 24px
),
repeating-linear-gradient(
to bottom,
rgba(13, 27, 82, 0.05) 0,
rgba(13, 27, 82, 0.05) 1px,
transparent 1px,
transparent 24px
);
opacity: 0.35;
}
.card :deep(.ant-card-body),
.card :deep(.ant-card-head) {
position: relative;
z-index: 1;
}
.card :deep(.ant-card-body) { padding: 16px; }
.scoreCard :deep(.ant-card-body) { padding: 18px; }
/* ====== SCORE ====== */
.scoreTop {
display: flex;
justify-content: space-between;
@ -342,6 +428,7 @@ onMounted(async () => {
align-items: center;
margin-bottom: 10px;
}
.chipDone, .chipRank {
display: inline-flex;
align-items: center;
@ -354,6 +441,7 @@ onMounted(async () => {
font-size: 12px;
color: #111827;
}
.chipDot {
width: 8px;
height: 8px;
@ -367,38 +455,31 @@ onMounted(async () => {
color: #111827;
line-height: 1;
}
.scoreLabel {
margin-top: 4px;
font-size: 12px;
color: #6b7280;
font-weight: 800;
font-weight: 900;
}
.bigProgress { margin-top: 12px; }
.bigProgressMeta {
margin-top: 6px;
display: flex;
justify-content: space-between;
gap: 10px;
font-size: 12px;
color: #6b7280;
}
.bigProgressMeta b { color: #111827; }
.advice { margin-top: 12px; border-radius: 14px; }
/* KPIs */
/* ====== KPIs ====== */
.kpi .kpiK, .kpiWide .kpiK, .noteCard .kpiK {
font-size: 12px;
color: #6b7280;
font-weight: 900;
}
.kpi .kpiV, .kpiWide .kpiV, .noteCard .kpiV {
margin-top: 6px;
font-size: 26px;
font-weight: 950;
color: #111827;
}
.kpiHint {
margin-top: 2px;
font-size: 12px;
@ -419,29 +500,42 @@ onMounted(async () => {
align-items: center;
justify-content: center;
}
.miniCircleLabel {
position: absolute;
text-align: center;
}
.miniCircleMain { font-weight: 950; font-size: 16px; color: #111827; line-height: 1; }
.miniCircleMain {
font-weight: 950;
font-size: 16px;
color: #111827;
line-height: 1;
}
.miniCircleSub { font-size: 12px; color: #6b7280; }
/* Tabla */
/* ====== TABLE ====== */
.tableWrap {
width: 100%;
overflow-x: auto;
border-radius: 14px;
}
.tableWrap :deep(.ant-table-container) { overflow-x: auto; }
:deep(.modernTable .ant-table-thead > tr > th) {
background: rgba(22,119,255,.04);
background: rgba(13, 27, 82, 0.03);
color: #0d1b52;
font-weight: 900;
}
.courseCell {
display: flex;
gap: 10px;
align-items: flex-start;
}
.courseDot {
width: 10px;
height: 10px;
@ -450,6 +544,7 @@ onMounted(async () => {
margin-top: 6px;
flex: 0 0 auto;
}
.courseName { font-weight: 900; color: #111827; line-height: 1.2; }
.courseMeta { margin-top: 2px; font-size: 12px; color: #6b7280; }
@ -467,27 +562,43 @@ onMounted(async () => {
background: rgba(0,0,0,.03);
color: #111827;
}
.pill.good { border-color: rgba(22,119,255,.25); background: rgba(22,119,255,.08); }
.pill.bad { border-color: rgba(0,0,0,.10); background: rgba(0,0,0,.04); }
.pill.neutral { opacity: .92; }
.empty { margin-top: 12px; }
/* ====== MOBILE: FULL WIDTH, SIN ESPACIOS ====== */
@media (max-width: 768px) {
.dashboard-modern { padding: 0 0 14px; }
.section-container { max-width: none; padding: 0 !important; margin: 0 !important; }
.page { max-width: none; padding: 0; }
.head { padding: 0 16px; margin-bottom: 10px; }
.headActions { width: 100%; min-width: 0; justify-content: flex-start; }
.headSpace {
width: 100%;
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
.btnSoft, .btnPrimary { width: 100%; }
/* cards full width */
.card {
border-radius: 0 !important;
border-left: 0 !important;
border-right: 0 !important;
}
.scoreMain { font-size: 36px; }
}
@media (max-width: 480px) {
.page { padding: 12px; }
.head { padding: 0 12px; }
.scoreMain { font-size: 32px; }
.bigProgressMeta { flex-direction: column; align-items: flex-start; }
}
</style>
</style>

@ -550,10 +550,10 @@ No se realiza ningún pago adicional.
</template>
<style scoped>
/* ====== BASE (igual dash) ====== */
.test-modern {
position: relative;
padding: 40px 0;
padding: 32px 0;
font-family: "Times New Roman", Times, serif;
background: #fbfcff;
overflow: hidden;
@ -565,7 +565,6 @@ No se realiza ningún pago adicional.
inset: 0;
pointer-events: none;
z-index: 0;
background-image:
repeating-linear-gradient(
to right,
@ -581,7 +580,6 @@ No se realiza ningún pago adicional.
transparent 1px,
transparent 24px
);
opacity: 0.55;
}
@ -593,51 +591,22 @@ No se realiza ningún pago adicional.
padding: 0 24px;
}
.mt12 { margin-top: 12px; }
.mt16 { margin-top: 16px; }
.mb12 { margin-bottom: 12px; }
.muted { color: #666; line-height: 1.6; }
.hero {
position: relative;
border: none;
border-radius: 16px;
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.08);
padding: 28px;
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 22px;
align-items: start;
background: #fff;
overflow: hidden;
}
.hero::after {
position: absolute;
top: -12px;
left: 24px;
background: linear-gradient(45deg, #1890ff, #52c41a);
color: #fff;
padding: 6px 16px;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 700;
}
.heroLeft, .heroRight { min-width: 0; }
/* ====== HERO TITLES ====== */
.heroKicker {
font-size: 0.95rem;
color: #666;
color: #6b7280;
margin-bottom: 6px;
}
.heroTitle {
margin: 0;
font-size: 1.85rem;
font-weight: 700;
font-weight: 900;
color: #0d1b52;
line-height: 1.15;
}
@ -645,9 +614,27 @@ No se realiza ningún pago adicional.
.heroText {
margin-top: 10px;
font-size: 1.05rem;
color: #666;
color: #6b7280;
}
/* ====== HERO CARD (igual dash) ====== */
.hero {
margin-top: 14px;
border-radius: 18px;
border: 1px solid rgba(13, 27, 82, 0.08);
background: #fff;
box-shadow: 0 10px 28px rgba(0,0,0,0.08);
padding: 24px;
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 18px;
align-items: start;
overflow: hidden;
}
.heroLeft, .heroRight { min-width: 0; }
/* ====== FACTS ====== */
.heroFacts {
margin-top: 16px;
display: grid;
@ -664,13 +651,13 @@ No se realiza ningún pago adicional.
.factK {
font-size: 0.9rem;
color: #666;
font-weight: 700;
color: #6b7280;
font-weight: 900;
}
.factV {
margin-top: 6px;
font-weight: 700;
font-weight: 900;
font-size: 0.95rem;
color: #1a237e;
overflow: hidden;
@ -678,7 +665,7 @@ No se realiza ningún pago adicional.
white-space: nowrap;
}
/* ====== BANK ACTIONS ====== */
.bankActions {
margin-top: 18px;
padding: 16px;
@ -694,28 +681,29 @@ No se realiza ningún pago adicional.
margin: 0;
font-size: 1.05rem;
color: #1a237e;
font-weight: 700;
font-weight: 900;
}
.bankSub {
margin-top: 6px;
font-size: 0.95rem;
color: #666;
line-height: 1.45;
color: #6b7280;
line-height: 1.55;
}
.bankBtn {
height: 44px;
border-radius: 10px;
font-weight: 700;
font-weight: 900;
}
/* ====== CTA CARD ====== */
.ctaCard {
border: none;
border-radius: 16px;
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.08);
border-radius: 18px;
border: 1px solid rgba(13, 27, 82, 0.08);
box-shadow: 0 10px 28px rgba(0,0,0,0.08);
background: #fff;
padding: 20px;
padding: 18px;
display: flex;
flex-direction: column;
gap: 12px;
@ -731,25 +719,25 @@ No se realiza ningún pago adicional.
.ctaTitle {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
font-weight: 900;
color: #0d1b52;
}
.ctaHint {
margin-top: 6px;
font-size: 0.95rem;
color: #666;
line-height: 1.45;
color: #6b7280;
line-height: 1.55;
}
.statusTag {
font-weight: 700;
font-weight: 900;
padding: 4px 12px;
border-radius: 999px;
white-space: nowrap;
border: 1px solid rgba(0,0,0,0.06);
background: rgba(0,0,0,0.03);
color: #666;
color: #6b7280;
}
.statusTag.ok {
@ -771,9 +759,9 @@ No se realiza ningún pago adicional.
}
.ctaBtn {
height: 52px;
height: 50px;
border-radius: 12px;
font-weight: 700;
font-weight: 900;
}
.primaryBtn {
@ -786,11 +774,12 @@ No se realiza ningún pago adicional.
border: 1px solid rgba(13, 27, 82, 0.14);
}
.ctaDivider { margin: 14px 0 0; }
.ctaDivider { margin: 12px 0 0; }
/* ====== SECTION ====== */
.sectionTitle {
font-size: 1.35rem;
font-weight: 700;
font-weight: 900;
color: #0d1b52;
margin: 0;
}
@ -798,15 +787,16 @@ No se realiza ningún pago adicional.
.sectionSub {
margin-top: 8px;
font-size: 1rem;
color: #666;
color: #6b7280;
}
.hint {
margin-top: 10px;
font-size: 0.95rem;
color: #666;
color: #6b7280;
}
/* ====== MODALS ====== */
:deep(.voucher-modal .ant-modal-content),
:deep(.select-modal .ant-modal-content) {
border-radius: 16px;
@ -827,7 +817,7 @@ No se realiza ningún pago adicional.
.voucher-caption {
margin-bottom: 10px;
color: #666;
color: #6b7280;
line-height: 1.6;
}
@ -845,51 +835,64 @@ No se realiza ningún pago adicional.
gap: 10px;
}
/* ====== GLOBAL ANT LOOK ====== */
:deep(.ant-card) { border-radius: 12px; }
:deep(.ant-card-bordered) { border: 1px solid rgba(13, 27, 82, 0.10); }
/* ====== RESPONSIVE ====== */
@media (max-width: 992px) {
.hero {
grid-template-columns: 1fr;
}
@media (max-width: 768px) {
.hide-mobile{
display: none !important;
.heroFacts {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.test-modern{
padding-top: 0 !important;
padding-bottom: 24px; /* opcional */
}
@media (max-width: 768px) {
.hide-mobile { display: none !important; }
/* mismo patrón del dash: full width en móvil */
.test-modern {
padding: 0 0 18px;
}
.section-container{
.section-container {
max-width: none;
padding: 0 !important;
margin: 0 !important;
}
.hero{
grid-template-columns: 1fr;
width: 100%;
margin: 0 !important;
border-radius: 0 !important;
.heroKicker,
.heroTitle,
.heroText {
padding: 0 16px;
}
.hero {
border-radius: 0 !important;
border-left: 0 !important;
border-right: 0 !important;
padding: 14px 16px 18px !important;
}
.heroKicker{ margin-top: 0 !important; }
.heroTitle{ margin-top: 6px !important; }
.heroText{ margin-top: 8px !important; }
.section{
.section {
padding: 0 16px;
}
.heroFacts{
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 576px){
.heroFacts{
@media (max-width: 576px) {
.heroFacts {
grid-template-columns: 1fr;
}
}
</style>
.heroTitle {
font-size: 1.55rem;
}
.ctaBtn {
height: 48px;
}
}
</style>
Loading…
Cancel
Save