|
|
|
|
@ -1,6 +1,185 @@
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<a-spin :spinning="loading">
|
|
|
|
|
|
|
|
|
|
<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="topbarRight">
|
|
|
|
|
<div class="statusLine">
|
|
|
|
|
<span class="label">Estado:</span>
|
|
|
|
|
<a-badge :status="eligibilityUi.badge" :text="eligibilityUi.text" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a-button class="btnTop" @click="fetchDashboard" :loading="loading" block>
|
|
|
|
|
Actualizar
|
|
|
|
|
</a-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a-divider />
|
|
|
|
|
|
|
|
|
|
<div class="testBox">
|
|
|
|
|
<div class="testHead">
|
|
|
|
|
<div class="testHeadLeft">
|
|
|
|
|
<div class="testTitle">Tu test de admisión</div>
|
|
|
|
|
<div class="testSub">
|
|
|
|
|
Responde con calma. Busca un lugar tranquilo. (10 preguntas • 10 min aprox.)
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="testHeadRight">
|
|
|
|
|
<a-button
|
|
|
|
|
type="primary"
|
|
|
|
|
size="large"
|
|
|
|
|
class="btnTest"
|
|
|
|
|
:disabled="!canStartTest"
|
|
|
|
|
block
|
|
|
|
|
@click="onStartTest"
|
|
|
|
|
>
|
|
|
|
|
{{ testCtaText }}
|
|
|
|
|
</a-button>
|
|
|
|
|
<div class="microHelp" v-if="!canStartTest">No disponible por el momento.</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="testInfoGrid">
|
|
|
|
|
<div class="infoItem">
|
|
|
|
|
<div class="infoK">Estado</div>
|
|
|
|
|
<div class="infoV">
|
|
|
|
|
<a-badge :status="testStatusUi.badge" :text="testStatusUi.text" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="infoItem">
|
|
|
|
|
<div class="infoK">Tiempo restante</div>
|
|
|
|
|
<div class="infoV">
|
|
|
|
|
<span class="strong" :class="{ warn: expiresSoon && timeRemainingText !== 'Vencido' }">
|
|
|
|
|
{{ timeRemainingText }}
|
|
|
|
|
</span>
|
|
|
|
|
<span v-if="expiresSoon && timeRemainingText !== 'Vencido'" class="hintInline">
|
|
|
|
|
(vence pronto)
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="infoItem">
|
|
|
|
|
<div class="infoK">Disponible desde</div>
|
|
|
|
|
<div class="infoV">
|
|
|
|
|
{{ state.eligibility.testAvailableAt ? fmtDate(state.eligibility.testAvailableAt) : "—" }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="infoItem">
|
|
|
|
|
<div class="infoK">Fecha límite</div>
|
|
|
|
|
<div class="infoV">
|
|
|
|
|
{{ state.eligibility.testExpiresAt ? fmtDate(state.eligibility.testExpiresAt) : "—" }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mt12">
|
|
|
|
|
<a-alert
|
|
|
|
|
v-if="state.eligibility.testStatus === 'COMPLETADO'"
|
|
|
|
|
type="success"
|
|
|
|
|
show-icon
|
|
|
|
|
message="¡Listo! Ya completaste el test."
|
|
|
|
|
description="Ahora puedes revisar los procesos disponibles."
|
|
|
|
|
/>
|
|
|
|
|
<a-alert
|
|
|
|
|
v-else-if="!state.eligibility.hasTestAssigned"
|
|
|
|
|
type="info"
|
|
|
|
|
show-icon
|
|
|
|
|
message="Aún no tienes un test asignado."
|
|
|
|
|
description="Vuelve a revisar en unos minutos."
|
|
|
|
|
/>
|
|
|
|
|
<a-alert
|
|
|
|
|
v-else
|
|
|
|
|
type="info"
|
|
|
|
|
show-icon
|
|
|
|
|
message="Importante"
|
|
|
|
|
description="Al iniciar o continuar, el tiempo puede comenzar a correr según la configuración del proceso."
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a-alert
|
|
|
|
|
v-if="!state.eligibility.isEligibleToApply && state.eligibility.reasons?.length"
|
|
|
|
|
type="warning"
|
|
|
|
|
show-icon
|
|
|
|
|
message="Requisitos pendientes"
|
|
|
|
|
class="mt12"
|
|
|
|
|
>
|
|
|
|
|
<template #description>
|
|
|
|
|
<ul class="reasons">
|
|
|
|
|
<li v-for="(r, idx) in state.eligibility.reasons" :key="idx">{{ r }}</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</template>
|
|
|
|
|
</a-alert>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a-divider />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<a-divider />
|
|
|
|
|
|
|
|
|
|
<!-- Procesos -->
|
|
|
|
|
<div class="section">
|
|
|
|
|
<div class="sectionHead">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="sectionTitle">Procesos disponibles</div>
|
|
|
|
|
<div class="sectionSub">Revisa fechas y postula cuando esté habilitado.</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="tableWrap mt12">
|
|
|
|
|
<a-table
|
|
|
|
|
:columns="processColumns"
|
|
|
|
|
:data-source="state.availableProcesses"
|
|
|
|
|
row-key="id"
|
|
|
|
|
:pagination="{ pageSize: 6 }"
|
|
|
|
|
:scroll="{ x: 720 }"
|
|
|
|
|
>
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
<template v-if="column.key === 'startDate'">
|
|
|
|
|
{{ fmtDate(record.startDate) }}
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template v-else-if="column.key === 'endDate'">
|
|
|
|
|
{{ fmtDate(record.endDate) }}
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template v-else-if="column.key === 'status'">
|
|
|
|
|
<a-tag class="chip">{{ record.status }}</a-tag>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template v-else-if="column.key === 'actions'">
|
|
|
|
|
<div class="actions">
|
|
|
|
|
<a-button size="small" block @click="onViewProcess(record)">Ver</a-button>
|
|
|
|
|
<a-button size="small" type="primary" block :disabled="!record.canApply" @click="onApply(record)">
|
|
|
|
|
Postular
|
|
|
|
|
</a-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="!record.canApply" class="microHelp">Este proceso aún no permite postular.</div>
|
|
|
|
|
</template>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a-divider />
|
|
|
|
|
</a-spin>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { computed, onMounted, reactive, ref, onBeforeUnmount } from "vue";
|
|
|
|
|
import { message, Modal } from "ant-design-vue";
|
|
|
|
|
import { useAuthStore } from '../../store/postulanteStore'
|
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const nowTick = ref(Date.now());
|
|
|
|
|
@ -13,7 +192,7 @@ const state = reactive({
|
|
|
|
|
isEligibleToApply: false,
|
|
|
|
|
reasons: [],
|
|
|
|
|
hasTestAssigned: true,
|
|
|
|
|
testStatus: "PENDIENTE", // PENDIENTE | EN_PROGRESO | COMPLETADO | NO_DISPONIBLE
|
|
|
|
|
testStatus: "PENDIENTE",
|
|
|
|
|
testAvailableAt: null,
|
|
|
|
|
testExpiresAt: null,
|
|
|
|
|
testUrl: null,
|
|
|
|
|
@ -21,13 +200,10 @@ const state = reactive({
|
|
|
|
|
availableProcesses: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/** ---------------------------
|
|
|
|
|
* Helpers tiempo (sin dayjs)
|
|
|
|
|
* --------------------------- */
|
|
|
|
|
|
|
|
|
|
function parseDate(val) {
|
|
|
|
|
if (!val) return null;
|
|
|
|
|
// soporta "YYYY-MM-DD HH:mm" o ISO
|
|
|
|
|
const iso = val.includes("T") ? val : val.replace(" ", "T");
|
|
|
|
|
const iso = String(val).includes("T") ? String(val) : String(val).replace(" ", "T");
|
|
|
|
|
const d = new Date(iso);
|
|
|
|
|
return isNaN(d.getTime()) ? null : d;
|
|
|
|
|
}
|
|
|
|
|
@ -41,36 +217,36 @@ function msToHuman(ms) {
|
|
|
|
|
if (h > 0) return `${h}h ${m}m`;
|
|
|
|
|
return `${m}m`;
|
|
|
|
|
}
|
|
|
|
|
function fmtDate(val) {
|
|
|
|
|
const d = parseDate(val);
|
|
|
|
|
if (!d) return val || "—";
|
|
|
|
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
|
|
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
const yyyy = d.getFullYear();
|
|
|
|
|
const hh = String(d.getHours()).padStart(2, "0");
|
|
|
|
|
const mi = String(d.getMinutes()).padStart(2, "0");
|
|
|
|
|
return `${dd}/${mm}/${yyyy} ${hh}:${mi}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** ---------------------------
|
|
|
|
|
* Computed
|
|
|
|
|
* --------------------------- */
|
|
|
|
|
const totalApplications = computed(() => state.applications.length);
|
|
|
|
|
|
|
|
|
|
const applicationsByStatus = computed(() => {
|
|
|
|
|
const map = {};
|
|
|
|
|
for (const a of state.applications) map[a.status] = (map[a.status] || 0) + 1;
|
|
|
|
|
return map;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const eligibilityTag = computed(() => {
|
|
|
|
|
const eligibilityUi = computed(() => {
|
|
|
|
|
return state.eligibility.isEligibleToApply
|
|
|
|
|
? { color: "green", text: "Apto para postular" }
|
|
|
|
|
: { color: "red", text: "No apto para postular" };
|
|
|
|
|
? { badge: "success", text: "Apto para postular" }
|
|
|
|
|
: { badge: "error", text: "No apto para postular" };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const testStatusUi = computed(() => {
|
|
|
|
|
const s = state.eligibility.testStatus;
|
|
|
|
|
if (s === "EN_PROGRESO") return { color: "blue", text: "En progreso" };
|
|
|
|
|
if (s === "COMPLETADO") return { color: "green", text: "Completado" };
|
|
|
|
|
if (s === "NO_DISPONIBLE") return { color: "default", text: "No disponible" };
|
|
|
|
|
return { color: "orange", text: "Pendiente" };
|
|
|
|
|
if (s === "EN_PROGRESO") return { badge: "processing", text: "En progreso" };
|
|
|
|
|
if (s === "COMPLETADO") return { badge: "success", text: "Completado" };
|
|
|
|
|
if (s === "NO_DISPONIBLE") return { badge: "default", text: "No disponible" };
|
|
|
|
|
return { badge: "warning", text: "Pendiente" };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const testCtaText = computed(() => {
|
|
|
|
|
const s = state.eligibility.testStatus;
|
|
|
|
|
if (s === "EN_PROGRESO") return "Continuar test";
|
|
|
|
|
if (s === "COMPLETADO") return "Test completado";
|
|
|
|
|
if (s === "COMPLETADO") return "Ver estado";
|
|
|
|
|
if (s === "NO_DISPONIBLE") return "No disponible";
|
|
|
|
|
return "Iniciar test";
|
|
|
|
|
});
|
|
|
|
|
@ -90,29 +266,34 @@ const testExpireMs = computed(() => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const expiresSoon = computed(() => {
|
|
|
|
|
// “pronto” si queda <= 24h
|
|
|
|
|
const ms = testExpireMs.value;
|
|
|
|
|
return ms !== null && ms > 0 && ms <= 24 * 60 * 60 * 1000;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const processColumns = [
|
|
|
|
|
{ title: "Proceso", dataIndex: "name", key: "name" },
|
|
|
|
|
{ title: "Inicio", dataIndex: "startDate", key: "startDate", width: 140 },
|
|
|
|
|
{ title: "Fin", dataIndex: "endDate", key: "endDate", width: 140 },
|
|
|
|
|
{ title: "Estado", dataIndex: "status", key: "status", width: 120 },
|
|
|
|
|
{ title: "Vacantes", dataIndex: "vacancies", key: "vacancies", width: 110 },
|
|
|
|
|
{ title: "Acciones", key: "actions", width: 220 },
|
|
|
|
|
];
|
|
|
|
|
const timeRemainingText = computed(() => {
|
|
|
|
|
const ms = testExpireMs.value;
|
|
|
|
|
if (ms === null) return "—";
|
|
|
|
|
if (ms <= 0) return "Vencido";
|
|
|
|
|
return msToHuman(ms);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const processColumns = computed(() => [
|
|
|
|
|
{ title: "Proceso", dataIndex: "name", key: "name", ellipsis: true },
|
|
|
|
|
{ title: "Inicio", dataIndex: "startDate", key: "startDate", responsive: ["md"] },
|
|
|
|
|
{ title: "Fin", dataIndex: "endDate", key: "endDate", responsive: ["md"] },
|
|
|
|
|
{ title: "Estado", dataIndex: "status", key: "status", responsive: ["sm"] },
|
|
|
|
|
{ title: "Vacantes", dataIndex: "vacancies", key: "vacancies", responsive: ["sm"] },
|
|
|
|
|
{ title: "Acciones", key: "actions" },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** ---------------------------
|
|
|
|
|
* API (reemplaza por tus endpoints)
|
|
|
|
|
* --------------------------- */
|
|
|
|
|
const api = {
|
|
|
|
|
async getDashboard() {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
resolve({
|
|
|
|
|
applicant: { id: 7, nombres: "Juan Pérez", documento: "DNI 12345678" },
|
|
|
|
|
|
|
|
|
|
applications: [
|
|
|
|
|
{ id: 1, processName: "Admisión 2025-II", status: "NO_APTO", createdAt: "2025-09-05" },
|
|
|
|
|
{ id: 2, processName: "Admisión 2026-I", status: "EN_REVISION", createdAt: "2026-02-03" },
|
|
|
|
|
@ -125,7 +306,7 @@ const api = {
|
|
|
|
|
testStatus: "PENDIENTE",
|
|
|
|
|
testAvailableAt: "2026-02-10 09:00",
|
|
|
|
|
testExpiresAt: "2026-02-20 23:59",
|
|
|
|
|
testUrl: "/postulante/test",
|
|
|
|
|
testUrl: "portal-postulante/test",
|
|
|
|
|
},
|
|
|
|
|
availableProcesses: [
|
|
|
|
|
{ id: 10, name: "Admisión 2026-I", startDate: "2026-02-01", endDate: "2026-02-20", status: "ABIERTO", vacancies: 120, canApply: true },
|
|
|
|
|
@ -167,12 +348,11 @@ async function onStartTest() {
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
title: "Test de Admisión",
|
|
|
|
|
content: "Al iniciar o continuar, el tiempo puede comenzar a correr según la configuración. ¿Deseas continuar?",
|
|
|
|
|
okText: "Sí",
|
|
|
|
|
okText: "Continuar",
|
|
|
|
|
cancelText: "Cancelar",
|
|
|
|
|
async onOk() {
|
|
|
|
|
try {
|
|
|
|
|
await api.startTest();
|
|
|
|
|
message.success("Listo.");
|
|
|
|
|
if (state.eligibility.testUrl) window.location.href = state.eligibility.testUrl;
|
|
|
|
|
} catch {
|
|
|
|
|
message.error("No se pudo iniciar el test.");
|
|
|
|
|
@ -183,6 +363,7 @@ async function onStartTest() {
|
|
|
|
|
|
|
|
|
|
async function onApply(process) {
|
|
|
|
|
if (!process.canApply) return;
|
|
|
|
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
title: "Confirmar postulación",
|
|
|
|
|
content: `¿Deseas postular al proceso "${process.name}"?`,
|
|
|
|
|
@ -214,396 +395,270 @@ onBeforeUnmount(() => {
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<a-spin :spinning="loading">
|
|
|
|
|
<a-space direction="vertical" size="large" style="width: 100%">
|
|
|
|
|
<!-- Encabezado del postulante -->
|
|
|
|
|
<a-card class="soft-card">
|
|
|
|
|
<div class="header-row">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="h-title">Bienvenido, {{ state.applicant.nombres }}</div>
|
|
|
|
|
<div class="h-sub">{{ state.applicant.documento }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="header-actions">
|
|
|
|
|
<a-tag :color="eligibilityTag.color" class="pill-tag">{{ eligibilityTag.text }}</a-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a-alert
|
|
|
|
|
v-if="!state.eligibility.isEligibleToApply && state.eligibility.reasons?.length"
|
|
|
|
|
type="warning"
|
|
|
|
|
show-icon
|
|
|
|
|
message="Requisitos pendientes"
|
|
|
|
|
style="margin-top: 14px"
|
|
|
|
|
>
|
|
|
|
|
<template #description>
|
|
|
|
|
<ul style="margin: 8px 0 0 18px">
|
|
|
|
|
<li v-for="(r, idx) in state.eligibility.reasons" :key="idx">{{ r }}</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</template>
|
|
|
|
|
</a-alert>
|
|
|
|
|
</a-card>
|
|
|
|
|
|
|
|
|
|
<!-- ✅ HERO: TEST DE ADMISIÓN (MUY RESALTADO) -->
|
|
|
|
|
<a-badge-ribbon
|
|
|
|
|
:text="state.eligibility.testStatus === 'COMPLETADO' ? 'LISTO' : 'IMPORTANTE'"
|
|
|
|
|
:color="state.eligibility.testStatus === 'COMPLETADO' ? 'green' : 'red'"
|
|
|
|
|
>
|
|
|
|
|
<a-card class="test-hero" :bordered="false">
|
|
|
|
|
<div class="test-grid">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="test-kicker">Test de admisión</div>
|
|
|
|
|
<div class="test-title">Tu evaluación está aquí</div>
|
|
|
|
|
|
|
|
|
|
<div class="test-meta">
|
|
|
|
|
<a-tag :color="testStatusUi.color" class="pill-tag">
|
|
|
|
|
Estado: {{ testStatusUi.text }}
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
|
|
|
|
<a-tag v-if="expiresSoon" color="volcano" class="pill-tag">
|
|
|
|
|
Vence pronto: {{ msToHuman(testExpireMs || 0) }}
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
|
|
|
|
<a-tag v-else-if="testExpireMs !== null && testExpireMs > 0" color="blue" class="pill-tag">
|
|
|
|
|
Tiempo restante: {{ msToHuman(testExpireMs) }}
|
|
|
|
|
</a-tag>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="test-dates" v-if="state.eligibility.testAvailableAt || state.eligibility.testExpiresAt">
|
|
|
|
|
<div v-if="state.eligibility.testAvailableAt">
|
|
|
|
|
<span class="muted">Disponible desde:</span> <b>{{ state.eligibility.testAvailableAt }}</b>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="state.eligibility.testExpiresAt">
|
|
|
|
|
<span class="muted">Fecha límite:</span> <b>{{ state.eligibility.testExpiresAt }}</b>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a-alert
|
|
|
|
|
v-if="state.eligibility.testStatus === 'COMPLETADO'"
|
|
|
|
|
type="success"
|
|
|
|
|
show-icon
|
|
|
|
|
message="Tu test ya fue completado. ¡Bien!"
|
|
|
|
|
style="margin-top: 12px"
|
|
|
|
|
/>
|
|
|
|
|
<a-alert
|
|
|
|
|
v-else-if="!state.eligibility.hasTestAssigned"
|
|
|
|
|
type="info"
|
|
|
|
|
show-icon
|
|
|
|
|
message="Aún no tienes un test asignado."
|
|
|
|
|
style="margin-top: 12px"
|
|
|
|
|
/>
|
|
|
|
|
<a-alert
|
|
|
|
|
v-else-if="!state.eligibility.isEligibleToApply"
|
|
|
|
|
type="warning"
|
|
|
|
|
show-icon
|
|
|
|
|
message="No estás apto para postular por ahora. Revisa los requisitos pendientes."
|
|
|
|
|
style="margin-top: 12px"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="test-cta">
|
|
|
|
|
<div class="cta-box">
|
|
|
|
|
<div class="cta-label">Acción</div>
|
|
|
|
|
|
|
|
|
|
<a-button
|
|
|
|
|
type="primary"
|
|
|
|
|
size="large"
|
|
|
|
|
block
|
|
|
|
|
class="cta-btn"
|
|
|
|
|
:disabled="!canStartTest"
|
|
|
|
|
@click="onStartTest"
|
|
|
|
|
>
|
|
|
|
|
{{ testCtaText }}
|
|
|
|
|
</a-button>
|
|
|
|
|
|
|
|
|
|
<div class="cta-hint" v-if="canStartTest">
|
|
|
|
|
Entra cuando estés listo. Si estás en progreso, puedes continuar.
|
|
|
|
|
</div>
|
|
|
|
|
<div class="cta-hint" v-else>
|
|
|
|
|
No disponible por el momento.
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a-divider style="margin: 14px 0" />
|
|
|
|
|
|
|
|
|
|
<div class="cta-stats">
|
|
|
|
|
<div class="stat">
|
|
|
|
|
<div class="stat-num">{{ totalApplications }}</div>
|
|
|
|
|
<div class="stat-txt">Postulaciones</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat">
|
|
|
|
|
<div class="stat-num">
|
|
|
|
|
{{ state.eligibility.isEligibleToApply ? "Sí" : "No" }}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-txt">Apto</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a-card>
|
|
|
|
|
</a-badge-ribbon>
|
|
|
|
|
|
|
|
|
|
<!-- KPIs secundarios (más limpios) -->
|
|
|
|
|
<a-row :gutter="[16, 16]">
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
<a-card class="soft-card" title="Resumen de postulaciones">
|
|
|
|
|
<div class="kpi-row">
|
|
|
|
|
<div class="kpi">
|
|
|
|
|
<div class="kpi-label">Veces que postuló</div>
|
|
|
|
|
<div class="kpi-value">{{ totalApplications }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="kpi-tags">
|
|
|
|
|
<a-tag v-for="(v, k) in applicationsByStatus" :key="k">
|
|
|
|
|
{{ k }}: <b>{{ v }}</b>
|
|
|
|
|
</a-tag>
|
|
|
|
|
<a-tag v-if="!Object.keys(applicationsByStatus).length">Sin registros</a-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a-card>
|
|
|
|
|
</a-col>
|
|
|
|
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
<a-card class="soft-card" title="Estado del postulante">
|
|
|
|
|
<a-descriptions :column="1" size="small" bordered>
|
|
|
|
|
<a-descriptions-item label="Aptitud">
|
|
|
|
|
<a-tag :color="eligibilityTag.color" class="pill-tag">{{ eligibilityTag.text }}</a-tag>
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
<a-descriptions-item label="Test">
|
|
|
|
|
<a-tag :color="testStatusUi.color" class="pill-tag">{{ testStatusUi.text }}</a-tag>
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
</a-descriptions>
|
|
|
|
|
</a-card>
|
|
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
<!-- Procesos disponibles -->
|
|
|
|
|
<a-card class="soft-card" title="Procesos disponibles">
|
|
|
|
|
<a-table
|
|
|
|
|
:columns="processColumns"
|
|
|
|
|
:data-source="state.availableProcesses"
|
|
|
|
|
row-key="id"
|
|
|
|
|
:pagination="{ pageSize: 6 }"
|
|
|
|
|
>
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
<template v-if="column.key === 'status'">
|
|
|
|
|
<a-tag :color="record.status === 'ABIERTO' ? 'green' : record.status === 'PRONTO' ? 'blue' : 'default'">
|
|
|
|
|
{{ record.status }}
|
|
|
|
|
</a-tag>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template v-else-if="column.key === 'actions'">
|
|
|
|
|
<a-space>
|
|
|
|
|
<a-button @click="onViewProcess(record)">Ver</a-button>
|
|
|
|
|
<a-button
|
|
|
|
|
type="primary"
|
|
|
|
|
:disabled="!record.canApply || !state.eligibility.isEligibleToApply"
|
|
|
|
|
@click="onApply(record)"
|
|
|
|
|
>
|
|
|
|
|
Postular
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
|
|
|
|
<div v-if="!state.eligibility.isEligibleToApply" class="mini-help">
|
|
|
|
|
Debes estar apto para postular.
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table>
|
|
|
|
|
</a-card>
|
|
|
|
|
|
|
|
|
|
<!-- Historial -->
|
|
|
|
|
<a-card class="soft-card" title="Historial de postulaciones">
|
|
|
|
|
<a-list
|
|
|
|
|
:data-source="state.applications"
|
|
|
|
|
:locale="{ emptyText: 'Aún no tienes postulaciones registradas.' }"
|
|
|
|
|
>
|
|
|
|
|
<template #renderItem="{ item }">
|
|
|
|
|
<a-list-item class="list-item">
|
|
|
|
|
<a-list-item-meta>
|
|
|
|
|
<template #title>
|
|
|
|
|
<div class="list-title">
|
|
|
|
|
<span class="list-title-text">{{ item.processName }}</span>
|
|
|
|
|
<a-tag>{{ item.status }}</a-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template #description>
|
|
|
|
|
<span class="muted">Postuló el:</span> {{ item.createdAt }}
|
|
|
|
|
</template>
|
|
|
|
|
</a-list-item-meta>
|
|
|
|
|
</a-list-item>
|
|
|
|
|
</template>
|
|
|
|
|
</a-list>
|
|
|
|
|
</a-card>
|
|
|
|
|
</a-space>
|
|
|
|
|
</a-spin>
|
|
|
|
|
</template>
|
|
|
|
|
.page {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
/* Cards */
|
|
|
|
|
.soft-card {
|
|
|
|
|
.pageCard {
|
|
|
|
|
max-width: 1120px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
}
|
|
|
|
|
.pill-tag {
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Header */
|
|
|
|
|
.header-row {
|
|
|
|
|
.topbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
gap: 14px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
align-items: center;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
}
|
|
|
|
|
.h-title {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
|
|
|
|
.hello {
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
color: var(--ant-colorTextHeading, #111827);
|
|
|
|
|
}
|
|
|
|
|
.h-sub {
|
|
|
|
|
opacity: 0.75;
|
|
|
|
|
.sub {
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--ant-colorTextSecondary, #6b7280);
|
|
|
|
|
}
|
|
|
|
|
.header-actions {
|
|
|
|
|
|
|
|
|
|
.topbarRight {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
min-width: 260px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* TEST HERO */
|
|
|
|
|
.test-hero {
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
background: linear-gradient(135deg, rgba(24, 144, 255, 0.14), rgba(82, 196, 26, 0.10));
|
|
|
|
|
.statusLine {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
.test-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1.4fr 0.8fr;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
.label {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--ant-colorTextSecondary, #6b7280);
|
|
|
|
|
}
|
|
|
|
|
@media (max-width: 992px) {
|
|
|
|
|
.test-grid {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
.btnTop {
|
|
|
|
|
min-width: 180px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* TEST destacado (sin degradado) */
|
|
|
|
|
.testBox {
|
|
|
|
|
border: 2px solid var(--ant-colorPrimary, #1677ff);
|
|
|
|
|
background: var(--ant-colorBgContainer, #fff);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
}
|
|
|
|
|
.test-kicker {
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
letter-spacing: 0.2px;
|
|
|
|
|
opacity: 0.85;
|
|
|
|
|
|
|
|
|
|
.testHead {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
flex-wrap: wrap; /* ✅ clave para móvil */
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
}
|
|
|
|
|
.test-title {
|
|
|
|
|
font-size: 26px;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
line-height: 1.15;
|
|
|
|
|
|
|
|
|
|
.testTitle {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
color: var(--ant-colorTextHeading, #111827);
|
|
|
|
|
}
|
|
|
|
|
.test-meta {
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
.testSub {
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--ant-colorTextSecondary, #6b7280);
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
}
|
|
|
|
|
.test-dates {
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
|
|
.testHeadRight {
|
|
|
|
|
min-width: 260px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
}
|
|
|
|
|
.muted {
|
|
|
|
|
opacity: 0.75;
|
|
|
|
|
|
|
|
|
|
.btnTest {
|
|
|
|
|
height: 44px;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* CTA box */
|
|
|
|
|
.test-cta {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
.testInfoGrid {
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
.cta-box {
|
|
|
|
|
width: 100%;
|
|
|
|
|
background: rgba(255, 255, 255, 0.75);
|
|
|
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06);
|
|
|
|
|
|
|
|
|
|
.infoItem {
|
|
|
|
|
border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06));
|
|
|
|
|
background: var(--ant-colorFillAlter, #fafafa);
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
.cta-label {
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
opacity: 0.85;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
.infoK {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--ant-colorTextSecondary, #6b7280);
|
|
|
|
|
}
|
|
|
|
|
.cta-btn {
|
|
|
|
|
height: 44px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
.infoV {
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
color: var(--ant-colorText, #111827);
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
}
|
|
|
|
|
.cta-hint {
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
|
|
|
|
.strong { font-weight: 900; }
|
|
|
|
|
.warn { color: var(--ant-colorTextHeading, #111827); }
|
|
|
|
|
.hintInline {
|
|
|
|
|
margin-left: 6px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
opacity: 0.75;
|
|
|
|
|
line-height: 1.35;
|
|
|
|
|
color: var(--ant-colorTextSecondary, #6b7280);
|
|
|
|
|
}
|
|
|
|
|
.cta-stats {
|
|
|
|
|
|
|
|
|
|
.reasons {
|
|
|
|
|
margin: 8px 0 0 18px;
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.summaryBox {
|
|
|
|
|
border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06));
|
|
|
|
|
background: var(--ant-colorFillAlter, #fafafa);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 14px;
|
|
|
|
|
}
|
|
|
|
|
.summaryTitle {
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
color: var(--ant-colorTextHeading, #111827);
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
.summaryGrid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
grid-template-columns: 1fr 2fr;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
.stat {
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
background: rgba(0, 0, 0, 0.02);
|
|
|
|
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
|
|
|
.summaryK {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--ant-colorTextSecondary, #6b7280);
|
|
|
|
|
}
|
|
|
|
|
.stat-num {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
.summaryV {
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
color: var(--ant-colorTextHeading, #111827);
|
|
|
|
|
}
|
|
|
|
|
.stat-txt {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
opacity: 0.75;
|
|
|
|
|
.chips {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* KPI */
|
|
|
|
|
.kpi-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
.chip {
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
}
|
|
|
|
|
.kpi {
|
|
|
|
|
|
|
|
|
|
/* Secciones */
|
|
|
|
|
.sectionHead {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
}
|
|
|
|
|
.kpi-label {
|
|
|
|
|
opacity: 0.75;
|
|
|
|
|
.sectionTitle {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
color: var(--ant-colorTextHeading, #111827);
|
|
|
|
|
}
|
|
|
|
|
.kpi-value {
|
|
|
|
|
font-size: 26px;
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
.sectionSub {
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--ant-colorTextSecondary, #6b7280);
|
|
|
|
|
}
|
|
|
|
|
.kpi-tags {
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
|
|
/* Tabla: evita romper en móvil */
|
|
|
|
|
.tableWrap {
|
|
|
|
|
width: 100%;
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
}
|
|
|
|
|
:deep(.ant-table-container) {
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Acciones: en móvil se apilan */
|
|
|
|
|
.actions {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
.microHelp {
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--ant-colorTextSecondary, #6b7280);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* List */
|
|
|
|
|
.list-item {
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
.listItem {
|
|
|
|
|
padding-left: 0;
|
|
|
|
|
padding-right: 0;
|
|
|
|
|
}
|
|
|
|
|
.list-title {
|
|
|
|
|
.listTitle {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
.list-title-text {
|
|
|
|
|
.listName {
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
}
|
|
|
|
|
.mini-help {
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
.muted {
|
|
|
|
|
color: var(--ant-colorTextSecondary, #6b7280);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mt12 { margin-top: 12px; }
|
|
|
|
|
|
|
|
|
|
/* ✅ Breakpoints reales */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.page { padding: 8px; }
|
|
|
|
|
|
|
|
|
|
.topbarRight {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
}
|
|
|
|
|
.btnTop {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.testHeadRight {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.testInfoGrid {
|
|
|
|
|
grid-template-columns: 1fr; /* ✅ cards info en columna */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.summaryGrid {
|
|
|
|
|
grid-template-columns: 1fr; /* ✅ resumen en columna */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.actions {
|
|
|
|
|
grid-template-columns: 1fr; /* ✅ botones uno debajo del otro */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 480px) {
|
|
|
|
|
.testBox { padding: 12px; }
|
|
|
|
|
.testTitle { font-size: 16px; }
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|