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.

1210 lines
24 KiB
Vue

2 months ago
<!-- views/PortalView.vue -->
<template>
<a-layout style="min-height: 100vh;" class="portal-layout">
<!-- Header Responsive -->
<a-layout-header class="header">
<div class="header-container">
<div class="header-left">
<div class="brand-section">
<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" style="height: 40px;" />
<div class="logo-content">
<h1 class="admin-name">Portal del Postulante</h1>
</div>
</router-link>
</div>
</div>
<!-- Desktop Actions -->
<div class="header-right">
<!-- Perfil -->
<a-dropdown :trigger="['click']" placement="bottomRight" class="profile-dropdown-wrapper">
<div class="profile-trigger">
<div class="profile-info">
<a-avatar
:size="isMobile ? 32 : 36"
class="profile-avatar"
:style="{ background: getAvatarColor(authStore.userName) }"
>
{{ getUserInitials(authStore.userName) }}
</a-avatar>
</div>
<DownOutlined 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">{{ 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" size="small">Activo</a-tag>
</div>
</div>
</div>
<div class="dropdown-item" @click="goToProfile">
<UserOutlined class="dropdown-icon" />
<div class="dropdown-item-content">
<div class="dropdown-item-title">Mi Perfil</div>
<div class="dropdown-item-subtitle">Información personal</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>
<!-- Layout Principal Responsive -->
<a-layout
class="main-layout"
:class="{
'layout-collapsed': sidebarCollapsed && !isMobile
}"
>
<!-- Sidebar - Visible en desktop, oculto en móvil -->
<div
v-if="isMobile && !sidebarCollapsed"
class="sidebar-backdrop"
@click="sidebarCollapsed = true"
/>
<a-layout-sider
v-model:collapsed="sidebarCollapsed"
:width="sidebarWidth"
:collapsedWidth="collapsedWidth"
collapsible
breakpoint="lg"
:trigger="null"
theme="light"
class="sidebar"
:class="{ 'sidebar-collapsed': sidebarCollapsed, 'sidebar-mobile': isMobile }"
@breakpoint="onBreakpoint"
>
<div class="sidebar-menu-container">
<a-menu
v-model:selectedKeys="selectedKeys"
mode="inline"
class="sidebar-menu"
@select="handleMenuSelect"
>
<!-- Dashboard -->
<a-menu-item key="dashboard-postulante" class="menu-item">
<div class="menu-item-content">
<DashboardOutlined class="menu-icon" />
<span class="menu-label">Panel Principal</span>
</div>
</a-menu-item>
<!-- Proceso de Admisión -->
<div class="menu-section" v-if="!sidebarCollapsed">
<div class="section-label">Proceso de Admisión</div>
</div>
<a-menu-item key="inscripcion" class="menu-item">
<div class="menu-item-content">
<FormOutlined class="menu-icon" />
<span class="menu-label">Preinscripción</span>
</div>
</a-menu-item>
<a-menu-item key="documentos" class="menu-item">
<div class="menu-item-content">
<FolderOutlined class="menu-icon" />
<span class="menu-label">Documentos</span>
</div>
</a-menu-item>
<a-menu-item key="pagos" class="menu-item">
<div class="menu-item-content">
<DollarOutlined class="menu-icon" />
<span class="menu-label">Pagos</span>
<a-tag v-if="!sidebarCollapsed" color="green" size="small">Al día</a-tag>
</div>
</a-menu-item>
<!-- Seguimiento -->
<div class="menu-section" v-if="!sidebarCollapsed">
<div class="section-label">Seguimiento</div>
</div>
<a-menu-item key="seguimiento" class="menu-item">
<div class="menu-item-content">
<LineChartOutlined class="menu-icon" />
<span class="menu-label">Estado del Proceso</span>
</div>
</a-menu-item>
<a-menu-item key="resultados" class="menu-item">
<div class="menu-item-content">
<FileDoneOutlined class="menu-icon" />
<span class="menu-label">Resultados</span>
<a-badge v-if="!sidebarCollapsed" status="default" />
</div>
</a-menu-item>
<!-- Configuración -->
<div class="menu-section" v-if="!sidebarCollapsed">
<div class="section-label">Configuración</div>
</div>
<a-menu-item key="configuracion" class="menu-item">
<div class="menu-item-content">
<SettingOutlined class="menu-icon" />
<span class="menu-label">Configuración</span>
</div>
</a-menu-item>
</a-menu>
</div>
<!-- Footer del Sidebar -->
<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>
</a-layout-sider>
<!-- Contenido Principal -->
<a-layout-content class="content" :class="{ 'content-collapsed': sidebarCollapsed && !isMobile }">
<div class="content-container">
<div class="content-wrapper">
<a-card class="content-card">
<router-view />
</a-card>
</div>
</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,
BellOutlined,
QuestionCircleOutlined,
HomeOutlined,
FileTextOutlined,
ClockCircleOutlined,
CalendarOutlined,
} from '@ant-design/icons-vue'
const router = useRouter()
const authStore = useAuthStore()
// Estado
const selectedKeys = ref(['dashboard-postulante'])
const sidebarCollapsed = ref(false)
const isMobile = ref(false)
const currentSection = ref('Panel Principal')
const currentSubSection = ref('')
const pageTitle = ref('Panel Principal')
const pageSubtitle = ref('Bienvenido al Portal del Postulante')
const sidebarWidth = computed(() => 280)
const collapsedWidth = computed(() => (isMobile.value ? 0 : 80))
const currentYear = computed(() => new Date().getFullYear())
// Métodos
const checkMobile = () => {
isMobile.value = window.innerWidth < 992
if (isMobile.value) {
sidebarCollapsed.value = true
}
}
const toggleSidebar = () => {
if (isMobile.value) {
sidebarCollapsed.value = !sidebarCollapsed.value
} else {
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.split(' ')
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 '#1890ff'
const colors = [
'#1890ff', '#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]
updatePageInfo(key)
const routes = {
'dashboard-postulante': { name: 'DashboardPostulante' },
'estudiantes-lista': { name: 'AcademiaEstudiantes' },
'estudiantes-nuevo': { name: 'AcademiaEstudianteNuevo' },
'examenes-proceso-lista': { name: 'Procesos' },
'examenes-area-lista': { name: 'Areas' },
'examenes-curso-lista': { name: 'Cursos' },
'lista-areas': { name: 'AcademiaAreas' },
'lista-cursos': { name: 'AcademiaCursos' },
'pagos': { name: 'PanelPagos' },
'reportes': { name: 'AcademiaReportes' },
'config-academia': { name: 'AcademiaConfig' },
'usuarios': { name: 'AcademiaUsuarios' }
}
if (routes[key]) {
router.push(routes[key])
}
}
const updatePageInfo = (key) => {
const pageInfo = {
'dashboard-postulante': {
section: 'Panel Principal',
subSection: '',
title: 'Panel Principal',
subtitle: 'Bienvenido al Portal del Postulante'
},
'inscripcion': {
section: 'Proceso de Admisión',
subSection: 'Preinscripción',
title: 'Preinscripción',
subtitle: 'Complete su formulario de preinscripción'
},
'documentos': {
section: 'Proceso de Admisión',
subSection: 'Documentos',
title: 'Documentos',
subtitle: 'Gestión de documentos requeridos'
},
'pagos': {
section: 'Proceso de Admisión',
subSection: 'Pagos',
title: 'Pagos',
subtitle: 'Gestión de pagos y derechos'
},
'seguimiento': {
section: 'Seguimiento',
subSection: 'Estado',
title: 'Estado del Proceso',
subtitle: 'Siga el progreso de su admisión'
},
'resultados': {
section: 'Seguimiento',
subSection: 'Resultados',
title: 'Resultados',
subtitle: 'Consulte los resultados de su evaluación'
}
}
const info = pageInfo[key] || {
section: key.charAt(0).toUpperCase() + key.slice(1),
subSection: '',
title: key.charAt(0).toUpperCase() + key.slice(1),
subtitle: 'Portal del Postulante'
}
currentSection.value = info.section
currentSubSection.value = info.subSection
pageTitle.value = info.title
pageSubtitle.value = info.subtitle
}
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')
// window.open('/ayuda-postulante', '_blank')
}
// Lifecycle
onMounted(() => {
checkMobile()
window.addEventListener('resize', checkMobile)
updatePageInfo(selectedKeys.value[0])
})
onUnmounted(() => {
window.removeEventListener('resize', checkMobile)
})
</script>
<style scoped>
/* Layout General */
.portal-layout {
background: #f8fafc;
}
/* Header Moderno */
.header {
background: white;
height: 64px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-bottom: 1px solid #f0f0f0;
position: sticky;
top: 0;
z-index: 100;
}
.header-container {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.brand-section {
display: flex;
align-items: center;
gap: 16px;
}
.menu-toggle {
width: 40px;
height: 40px;
border-radius: 8px;
border: none;
color: #666;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.menu-toggle:hover {
background: #f5f5f5;
}
.logo-wrapper {
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
}
.logo-content {
display: flex;
flex-direction: column;
}
.admin-name {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f1f1f;
line-height: 1.4;
}
.university-name {
font-size: 12px;
color: #666;
margin-top: 2px;
}
/* Header Actions */
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.notification-btn {
width: 40px;
height: 40px;
border-radius: 8px;
color: #666;
font-size: 18px;
}
.notification-btn:hover {
background: #f5f5f5;
}
.notifications-panel {
width: 320px;
background: white;
border-radius: 12px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
overflow: hidden;
border: 1px solid #f0f0f0;
}
.notifications-header {
padding: 16px 20px;
background: #f8fafc;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f0f0f0;
}
.notifications-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.notifications-list {
max-height: 400px;
overflow-y: auto;
}
.notification-item {
padding: 16px 20px;
display: flex;
gap: 12px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.3s;
}
.notification-item:hover {
background: #f5f5f5;
}
.notification-item.unread {
background: #f0f9ff;
}
.notification-icon {
font-size: 20px;
margin-top: 2px;
}
.notification-content {
flex: 1;
}
.notification-title {
font-weight: 500;
color: #1f1f1f;
margin-bottom: 4px;
}
.notification-desc {
font-size: 13px;
color: #666;
margin-bottom: 4px;
}
.notification-time {
font-size: 11px;
color: #999;
}
.notifications-footer {
padding: 12px 20px;
border-top: 1px solid #f0f0f0;
text-align: center;
}
.profile-trigger {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: 8px;
cursor: pointer;
transition: background 0.3s;
}
.profile-trigger:hover {
background: #f5f5f5;
}
.profile-info {
display: flex;
align-items: center;
gap: 8px;
}
.profile-avatar {
font-weight: 500;
color: white;
}
.profile-details {
display: flex;
flex-direction: column;
}
.profile-name {
font-size: 14px;
font-weight: 500;
color: #1f1f1f;
line-height: 1.4;
}
.role-badge-wrapper {
margin-top: 2px;
}
.role-badge {
font-size: 11px;
padding: 2px 6px;
border-radius: 10px;
background: #e6f7ff;
color: #1890ff;
font-weight: 500;
}
.role-badge.role-postulante {
background: #f6ffed;
color: #52c41a;
}
.dropdown-chevron {
font-size: 12px;
color: #999;
transition: transform 0.3s;
}
.profile-trigger:hover .dropdown-chevron {
color: #666;
}
/* Profile Dropdown */
.profile-dropdown {
width: 280px;
background: white;
border-radius: 12px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
overflow: hidden;
border: 1px solid #f0f0f0;
}
.profile-summary {
padding: 20px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
display: flex;
gap: 12px;
align-items: center;
}
.profile-summary-info {
flex: 1;
}
.profile-summary-info .profile-name {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.profile-email {
font-size: 12px;
color: #666;
margin-bottom: 2px;
}
.profile-dni {
font-size: 12px;
color: #666;
margin-bottom: 8px;
}
.dropdown-item {
padding: 12px 20px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
transition: background 0.3s;
}
.dropdown-item:hover {
background: #f5f5f5;
}
.dropdown-icon {
font-size: 16px;
color: #666;
}
.dropdown-item-content {
flex: 1;
}
.dropdown-item-title {
font-size: 14px;
font-weight: 500;
color: #1f1f1f;
margin-bottom: 2px;
}
.dropdown-item-subtitle {
font-size: 12px;
color: #999;
}
.dropdown-divider {
height: 1px;
background: #f0f0f0;
margin: 4px 0;
}
.dropdown-item.logout {
border-top: 1px solid #f0f0f0;
margin-top: 4px;
}
.dropdown-item.logout .dropdown-icon {
color: #ff4d4f;
}
.dropdown-item.logout .dropdown-item-title {
color: #ff4d4f;
}
/* Sidebar */
.sidebar {
background: white;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
border-right: 1px solid #f0f0f0;
height: calc(100vh - 64px);
position: fixed;
left: 0;
top: 64px;
z-index: 99;
transition: all 0.3s;
}
.sidebar-profile {
border-bottom: 1px solid #f0f0f0;
transition: all 0.3s;
}
.sidebar-menu-container {
height: calc(100% - 100px);
overflow-y: auto;
padding: 24px 0;
}
.sidebar-menu-container::-webkit-scrollbar {
width: 4px;
}
.sidebar-menu-container::-webkit-scrollbar-track {
background: #f1f1f1;
}
.sidebar-menu-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.sidebar-menu {
border-right: none !important;
}
.menu-section {
padding: 0 16px;
margin: 24px 0 12px 0;
}
.menu-section:first-child {
margin-top: 0;
}
.section-label {
font-size: 11px;
color: #999;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.menu-item {
border-radius: 0 !important;
margin: 0 !important;
height: 40px !important;
padding: 0 16px !important;
border-left: 3px solid transparent !important;
}
.menu-item-content {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.menu-icon {
font-size: 16px;
color: #666;
width: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.menu-label {
font-size: 14px;
color: #333;
flex: 1;
}
.menu-item:deep(.ant-menu-item-selected) {
background: #e6f7ff !important;
border-left-color: #1890ff !important;
}
.menu-item:deep(.ant-menu-item-selected) .menu-icon,
.menu-item:deep(.ant-menu-item-selected) .menu-label {
color: #1890ff !important;
}
/* Sidebar Footer */
.sidebar-footer {
padding: 8px;
border-top: 1px solid #f0f0f0;
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: white;
}
.progress-section {
margin-bottom: 16px;
padding: 12px;
background: #f8fafc;
border-radius: 8px;
}
.sidebar-help {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 6px;
color: #666;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.sidebar-help:hover {
background: #f5f5f5;
color: #1890ff;
}
.sidebar-version {
margin-top: 12px;
padding: 8px;
background: #f8fafc;
border-radius: 6px;
text-align: center;
}
.version {
font-size: 12px;
font-weight: 500;
color: #666;
}
.last-update {
font-size: 11px;
color: #999;
margin-top: 2px;
}
/* Main Layout */
.main-layout {
margin-left: 280px;
transition: margin-left 0.3s ease;
min-height: calc(100vh - 64px);
}
.main-layout.layout-collapsed {
margin-left: 80px;
}
/* Content */
.content {
background: #f8fafc;
min-height: calc(100vh - 64px);
padding: 24px;
width: 100%;
box-sizing: border-box;
}
.content-container {
max-width: 100%;
margin: 0 auto;
}
/* Content Header */
.content-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
}
.breadcrumb {
flex: 1;
min-width: 0;
}
.breadcrumb :deep(.ant-breadcrumb) {
font-size: 14px;
}
.breadcrumb :deep(.ant-breadcrumb a) {
color: #666;
}
.breadcrumb :deep(.ant-breadcrumb li:last-child a) {
color: #1f1f1f;
font-weight: 500;
}
.page-title {
margin-top: 8px;
}
.page-title h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #1f1f1f;
line-height: 1.3;
}
.page-subtitle {
font-size: 14px;
color: #666;
margin-top: 4px;
}
.content-actions {
display: flex;
align-items: center;
gap: 8px;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #f0f0f0;
transition: all 0.3s;
cursor: pointer;
}
.stat-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.stat-content {
display: flex;
align-items: center;
gap: 16px;
}
.stat-icon-wrapper {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #1f1f1f;
line-height: 1.3;
}
.stat-label {
font-size: 14px;
color: #666;
margin-top: 2px;
}
/* Action Cards */
.action-card {
border-radius: 12px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.action-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.action-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
/* Page Icon */
.page-icon {
font-size: 64px;
color: #1890ff;
margin-bottom: 24px;
}
/* Responsive */
@media (max-width: 992px) {
.main-layout {
margin-left: 0 !important;
}
.main-layout.layout-collapsed {
margin-left: 0 !important;
}
.sidebar {
z-index: 1000;
}
.content {
padding: 20px;
}
.header-container {
padding: 0 20px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.header {
height: 56px;
}
.sidebar {
top: 56px;
height: calc(100vh - 56px);
}
.content {
padding: 16px;
min-height: calc(100vh - 56px);
}
.header-container {
padding: 0 16px;
}
.admin-name {
font-size: 14px;
}
.menu-toggle {
width: 36px;
height: 36px;
font-size: 16px;
}
.page-title h2 {
font-size: 20px;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.content-header {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.content-actions {
justify-content: flex-end;
}
}
@media (max-width: 576px) {
.header-container {
padding: 0 12px;
}
.content {
padding: 12px;
}
.profile-details {
display: none;
}
.dropdown-chevron {
display: none;
}
.university-name {
display: none;
}
}
/* Backdrop para móvil */
.sidebar-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 998;
}
/* Sidebar en móvil */
.sidebar.sidebar-mobile {
position: fixed;
left: 0;
top: 64px;
height: calc(100vh - 64px);
z-index: 999;
transition: transform 0.3s ease;
}
.sidebar.sidebar-mobile.ant-layout-sider-collapsed {
transform: translateX(-100%);
}
.content-wrapper {
background: transparent;
}
.content-card {
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #f0f0f0;
min-height: auto;
}
</style>