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

<!-- 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>