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.

776 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.

<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.webp" 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">
<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>
<a-layout
class="main-layout"
:class="{ 'layout-collapsed': sidebarCollapsed && !isMobile }"
>
<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-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 anteriores</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>Seguimiento de inscripción</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>
<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]
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: 'AvanceProceso' },
'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('/')
} catch (error) {
message.error('Error al cerrar sesión')
}
}
const openHelp = () => {
message.info('Centro de ayuda disponible')
}
onMounted(() => {
checkMobile()
window.addEventListener('resize', checkMobile)
})
onUnmounted(() => {
window.removeEventListener('resize', checkMobile)
})
</script>
<style scoped>
.portal-layout,
.portal-layout * {
font-family: "Times New Roman", Times, serif;
}
.portal-layout {
min-height: 100vh;
background: var(--ant-colorBgLayout, #f5f5f5);
}
.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;
width: 100%;
}
.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;
}
.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;
line-height: 1.05;
}
.profile-name-top {
font-size: 13px;
font-weight: 800;
color: var(--ant-colorText, #374151);
margin: 0;
padding: 0;
}
.profile-meta-top {
font-size: 12px;
font-weight: 700;
color: var(--ant-colorTextSecondary, #6b7280);
margin: 0;
padding: 0;
transform: translateY(-1px);
}
.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 {
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;
}
.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 {
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-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;
}
.sidebar-backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,.45);
z-index: 998;
}
.sidebar.sidebar-mobile {
top: 64px;
height: calc(100vh - 64px);
}
.sidebar.sidebar-mobile :deep(.ant-layout-sider-children) {
height: 100%;
}
@media (max-width: 768px) {
.header,
.header-container {
height: 56px;
}
.header-container {
padding: 0 12px; /* ✅ menos padding lateral en móvil */
}
.sidebar,
.sidebar.sidebar-mobile {
top: 56px;
height: calc(100vh - 56px);
}
.logo-img {
height: 34px;
width: 34px;
}
.portal-title {
font-size: 14px;
}
.portal-subtitle {
display: none;
}
/* ✅ CONTENT: padding externo pequeño (se ve pro y no encoge tanto) */
.content {
padding: 8px !important;
background: var(--ant-colorBgLayout, #f5f5f5);
}
.content-container {
max-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
}
/* ✅ CARD: sin borde/sombra pesada en móvil y radio moderado */
.content-card {
border-radius: 12px !important;
margin: 0 !important;
border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06)) !important;
box-shadow: 0 6px 16px rgba(0,0,0,.06) !important;
background: #fff;
overflow: hidden;
}
/* ✅ CLAVE: padding interno del body (no exagerado) */
.content-card :deep(.ant-card-body) {
padding: 12px !important;
}
/* ✅ opcional: quita el “papel cuadriculado” en móvil (se ve más limpio) */
.content-card::before {
display: none !important;
}
}
/* Extra: teléfonos chicos (más pro aún) */
@media (max-width: 480px) {
.content {
padding: 6px !important;
}
.content-card :deep(.ant-card-body) {
padding: 10px !important;
}
}
/* ===== FIX MÓVIL REAL ===== */
@media (max-width: 992px) {
.main-layout,
.main-layout.layout-collapsed {
margin-left: 0 !important;
}
.sidebar {
position: fixed !important;
left: 0;
}
}
</style>