You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
570 lines
13 KiB
Vue
570 lines
13 KiB
Vue
<!-- DashboardPostulante.vue (test + procesos activos) -->
|
|
<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>
|
|
|
|
<a-divider class="softDivider" />
|
|
|
|
<!-- 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="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>
|
|
|
|
<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>
|
|
</div>
|
|
</a-card>
|
|
|
|
<a-divider class="softDivider" />
|
|
|
|
<!-- PROCESOS ACTIVOS -->
|
|
|
|
<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="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 === '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>
|
|
</div>
|
|
|
|
<!-- Mensaje cuando no hay procesos -->
|
|
<a-empty
|
|
v-if="!loading && state.processes.length === 0"
|
|
class="mt12"
|
|
description="No hay procesos activos por el momento."
|
|
/>
|
|
</div>
|
|
</a-spin>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onMounted, reactive, ref } from "vue";
|
|
import { Modal, message } from "ant-design-vue";
|
|
import { useRouter } from "vue-router";
|
|
import { useAuthStore } from "../../store/postulanteStore";
|
|
|
|
// ✅ Ajusta a tus rutas reales
|
|
const ROUTE_TEST_PANEL = { name: "PanelTest" }; // tu panel del test
|
|
const ROUTE_PROCESS_DETAIL = (id) => ({ name: "ProcesoDetalle", params: { id } }); // opcional
|
|
|
|
const router = useRouter();
|
|
const authStore = useAuthStore();
|
|
|
|
const loading = ref(false);
|
|
|
|
const state = reactive({
|
|
test: { hasAssigned: true }, // viene de tu backend/store
|
|
processes: [], // procesos activos
|
|
});
|
|
|
|
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: "Acciones", key: "actions" },
|
|
]);
|
|
|
|
// ✅ Mock: reemplaza por tu store / api real
|
|
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 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.");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function goToTest() {
|
|
if (!canGoTest.value) return;
|
|
|
|
Modal.confirm({
|
|
title: "Test diagnóstico",
|
|
content: "Este test es referencial y no afecta tu postulación. ¿Deseas continuar?",
|
|
okText: "Continuar",
|
|
cancelText: "Cancelar",
|
|
onOk() {
|
|
router.push("/portal-postulante/test");
|
|
},
|
|
});
|
|
}
|
|
|
|
function onViewProcess(process) {
|
|
// router.push(ROUTE_PROCESS_DETAIL(process.id));
|
|
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.");
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
onMounted(fetchDashboard);
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* =========================
|
|
BASE estilo Convocatorias
|
|
========================= */
|
|
.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;
|
|
}
|
|
|
|
.section-container {
|
|
position: relative;
|
|
z-index: 1;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 0 24px;
|
|
}
|
|
|
|
/* Layout container */
|
|
.page {
|
|
width: 100%;
|
|
max-width: 1120px;
|
|
margin: 0 auto;
|
|
padding: 0;
|
|
}
|
|
|
|
/* Helpers */
|
|
.mt12 { margin-top: 12px; }
|
|
.microHelp { font-size: 0.95rem; color: #666; margin-top: 6px; }
|
|
.softDivider { margin: 18px 0; }
|
|
|
|
/* =========================
|
|
Topbar
|
|
========================= */
|
|
.topbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 14px;
|
|
flex-wrap: wrap;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.hello {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: #0d1b52;
|
|
margin: 0;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* =========================
|
|
Test destacado (card principal)
|
|
========================= */
|
|
.testBox {
|
|
position: relative;
|
|
border: none;
|
|
border-radius: 16px;
|
|
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.08);
|
|
background: #fff;
|
|
}
|
|
|
|
.testBox :deep(.ant-card-body) {
|
|
padding: 28px;
|
|
}
|
|
|
|
/* Badge tipo convocatorias */
|
|
.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;
|
|
}
|
|
|
|
.testHead {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.testTitle {
|
|
font-size: 1.55rem;
|
|
font-weight: 700;
|
|
color: #1a237e;
|
|
}
|
|
|
|
.testSub {
|
|
margin-top: 8px;
|
|
font-size: 1rem;
|
|
color: #666;
|
|
line-height: 1.55;
|
|
max-width: 760px;
|
|
}
|
|
|
|
.testHeadRight {
|
|
min-width: 260px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
align-items: flex-end;
|
|
}
|
|
|
|
.btnTest {
|
|
height: 52px;
|
|
border-radius: 12px;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, #1a237e 0%, #283593 100%);
|
|
border: none;
|
|
}
|
|
|
|
/* bullets */
|
|
.bulletRow {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: flex-start;
|
|
font-size: 1rem;
|
|
color: #666;
|
|
line-height: 1.55;
|
|
}
|
|
|
|
.dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 999px;
|
|
background: #1a237e;
|
|
margin-top: 9px;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
/* =========================
|
|
Section header
|
|
========================= */
|
|
.sectionHead {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.sectionRight { margin-top: 4px; }
|
|
|
|
.sectionTitle {
|
|
font-size: 1.55rem;
|
|
font-weight: 700;
|
|
color: #0d1b52;
|
|
margin: 0;
|
|
}
|
|
|
|
.sectionSub {
|
|
margin-top: 8px;
|
|
font-size: 1rem;
|
|
color: #666;
|
|
line-height: 1.55;
|
|
}
|
|
|
|
/* 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);
|
|
border-radius: 999px;
|
|
}
|
|
|
|
/* =========================
|
|
Table "card look"
|
|
========================= */
|
|
.tableWrap {
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
border-radius: 16px;
|
|
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.08);
|
|
background: #fff;
|
|
padding: 8px;
|
|
}
|
|
|
|
:deep(.modernTable .ant-table) {
|
|
background: transparent;
|
|
}
|
|
|
|
:deep(.modernTable .ant-table-container) {
|
|
overflow-x: auto;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
:deep(.modernTable .ant-table-thead > tr > th) {
|
|
background: rgba(13, 27, 82, 0.03);
|
|
color: #0d1b52;
|
|
font-weight: 700;
|
|
}
|
|
|
|
:deep(.modernTable .ant-table-tbody > tr > td) {
|
|
color: #666;
|
|
}
|
|
|
|
/* Status pill estilo convocatorias */
|
|
.status-tag {
|
|
font-weight: 700;
|
|
padding: 4px 12px;
|
|
border-radius: 999px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Actions */
|
|
.actions {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
}
|
|
|
|
.actionBtn {
|
|
border-radius: 10px;
|
|
height: 38px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.actionBtnPrimary {
|
|
border-radius: 10px;
|
|
height: 38px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.hide-mobile{
|
|
display: none !important;
|
|
}
|
|
/* ✅ Quita el padding superior del wrapper (era 40px) */
|
|
.test-modern{
|
|
padding-top: 0 !important;
|
|
padding-bottom: 24px; /* opcional */
|
|
}
|
|
|
|
/* ✅ Quita padding lateral del container para que sea edge-to-edge */
|
|
.section-container{
|
|
max-width: none;
|
|
padding: 0 !important;
|
|
margin: 0 !important;
|
|
}
|
|
|
|
/* ✅ Card principal a todo el ancho y pegado arriba */
|
|
.hero{
|
|
grid-template-columns: 1fr;
|
|
width: 100%;
|
|
margin: 0 !important;
|
|
border-radius: 0 !important;
|
|
|
|
/* importante: sin “espacio arriba” */
|
|
padding: 14px 16px 18px !important;
|
|
}
|
|
|
|
/* ✅ Asegura que el primer texto no empuje hacia abajo */
|
|
.heroKicker{ margin-top: 0 !important; }
|
|
.heroTitle{ margin-top: 6px !important; }
|
|
.heroText{ margin-top: 8px !important; }
|
|
|
|
/* Para que la sección “Tu camino” no se pegue a los bordes */
|
|
.section{
|
|
padding: 0 16px;
|
|
}
|
|
|
|
/* Facts */
|
|
.heroFacts{
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
}
|
|
|
|
@media (max-width: 576px){
|
|
.heroFacts{
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|