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.

757 lines
18 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!-- views/PortalView.vue -->
<template>
<a-layout class="portal-layout">
<!-- Header -->
<a-layout-header class="header">
<div class="header-container">
<div class="header-left">
<a-button type="text" class="menu-toggle" @click="toggleSidebar">
<MenuOutlined />
</a-button>
<router-link to="/portal-postulante" class="logo-wrapper">
<img src="/logotiny.png" alt="Logo UNA" class="logo-img" />
<div class="logo-content">
<div class="portal-title">Portal del Postulante</div>
<div class="portal-subtitle">Universidad Nacional del Altiplano Puno</div>
</div>
</router-link>
</div>
<div class="header-right">
<!-- Perfil -->
<a-dropdown :trigger="['click']" placement="bottomRight">
<div class="profile-trigger">
<a-avatar
:size="isMobile ? 32 : 36"
class="profile-avatar"
:style="{ background: getAvatarColor(authStore.userName) }"
>
{{ getUserInitials(authStore.userName) }}
</a-avatar>
<div v-if="!isMobile" class="profile-text">
<div class="profile-name-top">{{ authStore.userName || 'Postulante' }}</div>
<div class="profile-meta-top">DNI: {{ authStore.userDni || '—' }}</div>
</div>
<DownOutlined v-if="!isMobile" class="dropdown-chevron" />
</div>
<template #overlay>
<div class="profile-dropdown">
<div class="profile-summary">
<a-avatar :size="48" :style="{ background: getAvatarColor(authStore.userName) }">
{{ getUserInitials(authStore.userName) }}
</a-avatar>
<div class="profile-summary-info">
<div class="profile-name-top">{{ authStore.userName || 'Postulante' }}</div>
<div class="profile-email">{{ authStore.userEmail || 'email@ejemplo.com' }}</div>
<div class="profile-dni">DNI: {{ authStore.userDni || 'No registrado' }}</div>
<div class="profile-status">
<a-tag color="green">Activo</a-tag>
</div>
</div>
</div>
<div class="dropdown-divider" />
<div class="dropdown-item logout" @click="handleLogout">
<LogoutOutlined class="dropdown-icon" />
<div class="dropdown-item-content">
<div class="dropdown-item-title">Cerrar sesión</div>
<div class="dropdown-item-subtitle">Salir del portal</div>
</div>
</div>
</div>
</template>
</a-dropdown>
</div>
</div>
</a-layout-header>
<!-- Main layout -->
<a-layout
class="main-layout"
:class="{ 'layout-collapsed': sidebarCollapsed && !isMobile }"
>
<!-- Backdrop móvil -->
<div
v-if="isMobile && !sidebarCollapsed"
class="sidebar-backdrop"
@click="sidebarCollapsed = true"
/>
<!-- Sidebar -->
<a-layout-sider
v-model:collapsed="sidebarCollapsed"
:width="sidebarWidth"
:collapsedWidth="collapsedWidth"
collapsible
breakpoint="lg"
:trigger="null"
theme="light"
class="sidebar"
:class="{ 'sidebar-mobile': isMobile }"
@breakpoint="onBreakpoint"
>
<div class="sidebar-inner">
<div class="sidebar-menu-container">
<a-menu
v-model:selectedKeys="selectedKeys"
mode="inline"
class="sidebar-menu"
@select="handleMenuSelect"
>
<a-menu-item key="dashboard-postulante">
<DashboardOutlined />
<span>Panel Principal</span>
</a-menu-item>
<a-menu-divider v-if="!sidebarCollapsed" />
<div class="menu-section" v-if="!sidebarCollapsed">
<div class="section-label">Proceso de Admisión</div>
</div>
<a-menu-item key="test-postulante">
<FormOutlined />
<span>Test</span>
</a-menu-item>
<a-menu-item key="documentos">
<FolderOutlined />
<span>Documentos</span>
</a-menu-item>
<a-menu-item key="mis-procesos">
<FolderOutlined />
<span>Mis Procesos</span>
</a-menu-item>
<a-menu-item key="pagos">
<DollarOutlined />
<span>Pagos</span>
<a-tag v-if="!sidebarCollapsed" color="green" class="menu-tag">Al día</a-tag>
</a-menu-item>
<a-menu-divider v-if="!sidebarCollapsed" />
<div class="menu-section" v-if="!sidebarCollapsed">
<div class="section-label">Seguimiento</div>
</div>
<a-menu-item key="seguimiento">
<LineChartOutlined />
<span>Estado del Proceso</span>
</a-menu-item>
<a-menu-item key="resultados">
<FileDoneOutlined />
<span>Resultados</span>
</a-menu-item>
<a-menu-divider v-if="!sidebarCollapsed" />
<div class="menu-section" v-if="!sidebarCollapsed">
<div class="section-label">Configuración</div>
</div>
<a-menu-item key="configuracion">
<SettingOutlined />
<span>Configuración</span>
</a-menu-item>
</a-menu>
</div>
<div class="sidebar-footer" v-if="!sidebarCollapsed">
<div class="sidebar-help" @click="openHelp">
<QuestionCircleOutlined />
<span>Centro de Ayuda</span>
</div>
<div class="sidebar-version">
<div class="version">Portal del Postulante</div>
<div class="last-update">UNA Admisión {{ currentYear }}</div>
</div>
</div>
</div>
</a-layout-sider>
<!-- Content -->
<a-layout-content class="content">
<div class="content-container">
<a-card class="content-card" :bordered="false">
<router-view />
</a-card>
</div>
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../../store/postulanteStore'
import { message } from 'ant-design-vue'
import {
MenuOutlined,
DownOutlined,
UserOutlined,
SettingOutlined,
LogoutOutlined,
DashboardOutlined,
FormOutlined,
FolderOutlined,
DollarOutlined,
LineChartOutlined,
FileDoneOutlined,
QuestionCircleOutlined,
} from '@ant-design/icons-vue'
const router = useRouter()
const authStore = useAuthStore()
const selectedKeys = ref(['dashboard-postulante'])
const sidebarCollapsed = ref(false)
const isMobile = ref(false)
const sidebarWidth = computed(() => 280)
const collapsedWidth = computed(() => (isMobile.value ? 0 : 80))
const currentYear = computed(() => new Date().getFullYear())
const checkMobile = () => {
isMobile.value = window.innerWidth < 992
if (isMobile.value) sidebarCollapsed.value = true
}
const toggleSidebar = () => {
sidebarCollapsed.value = !sidebarCollapsed.value
}
const onBreakpoint = (broken) => {
isMobile.value = broken
if (broken) sidebarCollapsed.value = true
}
const getUserInitials = (name) => {
if (!name) return 'P'
const parts = name.trim().split(/\s+/)
if (parts.length === 1) return parts[0].charAt(0).toUpperCase()
return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase()
}
const getAvatarColor = (name) => {
if (!name) return '#1677ff'
const colors = ['#1677ff', '#52c41a', '#fa8c16', '#f5222d', '#722ed1', '#13c2c2', '#eb2f96', '#faad14']
const index = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
return colors[index % colors.length]
}
const handleMenuSelect = ({ key }) => {
selectedKeys.value = [key]
// Ajusta aquí tus rutas reales
const routes = {
'dashboard-postulante': { name: 'DashboardPostulante' },
'test-postulante': { name: 'TestPostulante' },
'inscripcion': { name: 'InscripcionPostulante' },
'documentos': { name: 'DocumentosPostulante' },
'pagos': { name: 'PanelPagos' },
'mis-procesos': { name: 'PanelProcesos' },
'seguimiento': { name: 'SeguimientoPostulante' },
'resultados': { name: 'ResultadosPostulante' },
'configuracion': { name: 'ConfiguracionPostulante' }
}
if (routes[key]) router.push(routes[key])
}
const handleLogout = async () => {
try {
await authStore.logout()
message.success('Sesión cerrada correctamente')
router.push('/login-postulante')
} catch (error) {
message.error('Error al cerrar sesión')
}
}
const goToProfile = () => {
message.info('Perfil del postulante')
// router.push('/portal/perfil')
}
const openHelp = () => {
message.info('Centro de ayuda disponible')
}
onMounted(() => {
checkMobile()
window.addEventListener('resize', checkMobile)
})
onUnmounted(() => {
window.removeEventListener('resize', checkMobile)
})
</script>
<style scoped>
/* Tipografía institucional */
.portal-layout,
.portal-layout * {
font-family: "Times New Roman", Times, serif;
}
/* Layout base */
.portal-layout {
min-height: 100vh;
background: var(--ant-colorBgLayout, #f5f5f5);
}
/* ===== Header ===== */
.header {
height: 64px;
padding: 0;
background: var(--ant-colorBgContainer, #fff);
border-bottom: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06));
box-shadow: 0 2px 8px rgba(0,0,0,.05);
position: sticky;
top: 0;
z-index: 1000;
}
.header-container {
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32px; /* más aire */
width: 100%; /* ocupa todo */
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.menu-toggle {
width: 40px;
height: 40px;
border-radius: 10px;
display: grid;
place-items: center;
color: var(--ant-colorTextSecondary, #6b7280);
transition: background .2s ease, color .2s ease;
}
.menu-toggle:hover {
background: var(--ant-colorFillAlter, #fafafa);
color: var(--ant-colorText, #111827);
}
.logo-wrapper {
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
}
.logo-img {
height: 40px;
width: 40px;
object-fit: contain;
}
.logo-content {
display: flex;
flex-direction: column;
line-height: 1.1;
}
.portal-title {
font-size: 15px;
font-weight: 800;
color: var(--ant-colorTextHeading, #111827);
}
.portal-subtitle {
font-size: 12px;
color: var(--ant-colorTextSecondary, #6b7280);
margin-top: 2px;
}
/* Profile trigger */
.header-right {
display: flex;
align-items: center;
gap: 10px;
}
.profile-trigger {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 10px;
border-radius: 12px;
cursor: pointer;
transition: background .2s ease;
}
.profile-trigger:hover {
background: var(--ant-colorFillAlter, #fafafa);
}
.profile-avatar {
color: #fff;
font-weight: 700;
}
.profile-text {
display: flex;
flex-direction: column;
gap: 0; /* ✅ elimina separación */
line-height: 1.05; /* ✅ compacto */
}
.profile-name-top {
font-size: 13px;
font-weight: 800;
color: var(--ant-colorText, #374151);
margin: 0; /* ✅ sin margen */
padding: 0;
}
.profile-meta-top {
font-size: 12px;
font-weight: 700;
color: var(--ant-colorTextSecondary, #6b7280);
margin: 0; /* ✅ sin margen */
padding: 0;
transform: translateY(-1px); /* ✅ sube 1px (opcional) */
}
.dropdown-chevron {
font-size: 12px;
color: var(--ant-colorTextSecondary, #6b7280);
}
/* Dropdown */
.profile-dropdown {
width: 300px;
background: var(--ant-colorBgContainer, #fff);
border-radius: 14px;
border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06));
box-shadow: 0 14px 36px rgba(0,0,0,.12);
overflow: hidden;
}
.profile-summary {
padding: 16px;
background: var(--ant-colorFillAlter, #fafafa);
display: flex;
gap: 12px;
align-items: center;
border-bottom: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06));
}
.profile-summary-info .profile-name {
font-size: 15px;
font-weight: 900;
color: var(--ant-colorTextHeading, #111827);
}
.profile-email,
.profile-dni {
font-size: 12px;
color: var(--ant-colorTextSecondary, #6b7280);
margin-top: 2px;
}
.profile-status {
margin-top: 8px;
}
.dropdown-item {
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
transition: background .2s ease;
}
.dropdown-item:hover {
background: var(--ant-colorFillAlter, #fafafa);
}
.dropdown-icon {
font-size: 16px;
color: var(--ant-colorTextSecondary, #6b7280);
}
.dropdown-item-title {
font-size: 13px;
font-weight: 800;
color: var(--ant-colorTextHeading, #111827);
}
.dropdown-item-subtitle {
font-size: 12px;
color: var(--ant-colorTextSecondary, #6b7280);
}
.dropdown-divider {
height: 1px;
background: var(--ant-colorBorderSecondary, rgba(0,0,0,.06));
}
.dropdown-item.logout .dropdown-icon,
.dropdown-item.logout .dropdown-item-title {
color: #ff4d4f;
}
/* ===== Sidebar ===== */
.sidebar {
background: var(--ant-colorBgContainer, #fff);
border-right: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06));
height: calc(100vh - 64px);
position: fixed;
left: 0;
top: 64px;
z-index: 999;
box-shadow: 2px 0 10px rgba(0,0,0,.05);
}
.sidebar-inner {
height: 100%;
position: relative;
}
.sidebar-menu-container {
height: calc(100% - 110px);
overflow: auto;
padding: 14px 8px;
}
.sidebar-menu-container::-webkit-scrollbar {
width: 6px;
}
.sidebar-menu-container::-webkit-scrollbar-thumb {
background: rgba(0,0,0,.18);
border-radius: 10px;
}
.menu-section {
padding: 10px 10px 6px;
}
.section-label {
font-size: 11px;
font-weight: 900;
color: var(--ant-colorTextSecondary, #6b7280);
text-transform: uppercase;
letter-spacing: .5px;
}
/* ✅ AntDV menu selected/hover correctos */
.sidebar-menu :deep(.ant-menu-item) {
border-radius: 12px;
margin: 4px 6px;
height: 42px;
line-height: 42px;
transition: background .2s ease;
}
.sidebar-menu :deep(.ant-menu-item:hover) {
background: var(--ant-colorFillAlter, #fafafa);
}
.sidebar-menu :deep(.ant-menu-item-selected) {
background: rgba(22, 119, 255, 0.10) !important;
}
.sidebar-menu :deep(.ant-menu-item-selected),
.sidebar-menu :deep(.ant-menu-item-selected a),
.sidebar-menu :deep(.ant-menu-item-selected span),
.sidebar-menu :deep(.ant-menu-item-selected .anticon) {
color: var(--ant-colorPrimary, #1677ff) !important;
}
.menu-tag {
margin-left: auto;
}
/* Sidebar Footer */
.sidebar-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 10px;
border-top: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06));
background: var(--ant-colorBgContainer, #fff);
}
.sidebar-help {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 10px;
border-radius: 12px;
cursor: pointer;
color: var(--ant-colorTextSecondary, #6b7280);
transition: background .2s ease, color .2s ease;
}
.sidebar-help:hover {
background: var(--ant-colorFillAlter, #fafafa);
color: var(--ant-colorPrimary, #1677ff);
}
.sidebar-version {
margin-top: 10px;
padding: 10px;
border-radius: 12px;
background: var(--ant-colorFillAlter, #fafafa);
text-align: center;
}
.version {
font-size: 12px;
font-weight: 900;
color: var(--ant-colorTextSecondary, #6b7280);
}
.last-update {
font-size: 11px;
color: var(--ant-colorTextSecondary, #6b7280);
margin-top: 2px;
}
/* ===== Main + Content ===== */
.main-layout {
margin-left: 280px;
transition: margin-left .25s ease;
min-height: calc(100vh - 64px);
}
.main-layout.layout-collapsed {
margin-left: 80px;
}
.content {
padding: 18px;
background: var(--ant-colorBgLayout, #f5f5f5);
min-height: calc(100vh - 64px);
}
.content-container {
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.content-card {
border-radius: 16px;
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: #fbfcff;
}
.content-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.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;
}
/* ===== Mobile ===== */
.sidebar-backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,.45);
z-index: 998;
}
/* Sidebar en móvil */
.sidebar.sidebar-mobile {
top: 64px;
height: calc(100vh - 64px);
}
/* AntDV collapsed mobile (width 0) ya lo oculta, pero reforzamos por UX */
.sidebar.sidebar-mobile :deep(.ant-layout-sider-children) {
height: 100%;
}
/* Responsive */
@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;
}
.sidebar,
.sidebar.sidebar-mobile {
top: 56px;
height: calc(100vh - 56px);
}
.logo-img {
height: 36px;
width: 36px;
}
.portal-title {
font-size: 14px;
}
.portal-subtitle {
display: none;
}
.content {
padding: 12px;
}
.content-card {
border-radius: 14px;
}
}
</style>