monacousa-portal/layouts/board.vue

678 lines
19 KiB
Vue

<template>
<v-app>
<v-navigation-drawer
v-model="drawer"
:rail="miniVariant"
:expand-on-hover="false"
permanent
:width="miniVariant ? 56 : 280"
class="enhanced-glass-drawer"
>
<!-- Enhanced Logo Section -->
<v-list-item class="pa-4 text-center enhanced-glass-logo">
<template v-if="!miniVariant">
<v-img
src="/MONACOUSA-Flags_376x376.png"
width="80"
height="80"
class="mx-auto mb-2 shimmer-animation"
/>
<div class="text-h6 font-weight-bold text-gradient">
MonacoUSA Portal
</div>
<v-chip
size="x-small"
class="glass-badge mt-1"
>
BOARD MEMBER
</v-chip>
</template>
<template v-else>
<v-img
src="/MONACOUSA-Flags_376x376.png"
width="40"
height="40"
class="mx-auto shimmer-animation"
/>
</template>
</v-list-item>
<v-divider class="glass-divider mx-3" />
<!-- Enhanced Navigation Menu -->
<v-list nav density="comfortable" class="enhanced-glass-nav">
<!-- Board Overview -->
<v-tooltip
:text="miniVariant ? 'Board Dashboard' : ''"
location="end"
:disabled="!miniVariant"
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
to="/board/dashboard"
prepend-icon="mdi-view-dashboard"
:title="!miniVariant ? 'Board Dashboard' : undefined"
value="dashboard"
class="glass-nav-item animated-nav-item"
/>
</template>
</v-tooltip>
<!-- Member Management -->
<v-list-group value="members" v-if="!miniVariant">
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
prepend-icon="mdi-account-group"
title="Members"
class="glass-nav-item animated-nav-item"
/>
</template>
<v-list-item
to="/board/members"
title="Member Directory"
value="member-list"
class="glass-nav-item-sub"
/>
<v-list-item
to="/board/members/dues"
title="Dues Management"
value="dues"
class="glass-nav-item-sub"
/>
<v-list-item
to="/board/members/applications"
title="Applications"
value="applications"
class="glass-nav-item-sub"
>
<template v-slot:append>
<v-badge
:content="pendingApplications"
:value="pendingApplications > 0"
color="error"
class="glass-badge"
/>
</template>
</v-list-item>
</v-list-group>
<!-- Events & Meetings -->
<v-list-group value="events" v-if="!miniVariant">
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
prepend-icon="mdi-calendar"
title="Events & Meetings"
class="glass-nav-item animated-nav-item"
/>
</template>
<v-list-item
to="/board/events"
title="All Events"
value="events"
class="glass-nav-item-sub"
/>
<v-list-item
to="/board/meetings"
title="Board Meetings"
value="meetings"
class="glass-nav-item-sub"
/>
<v-list-item
to="/board/meetings/minutes"
title="Meeting Minutes"
value="minutes"
class="glass-nav-item-sub"
/>
</v-list-group>
<!-- Reports & Analytics -->
<v-tooltip
:text="miniVariant ? 'Reports & Analytics' : ''"
location="end"
:disabled="!miniVariant"
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
to="/board/reports"
prepend-icon="mdi-chart-box"
:title="!miniVariant ? 'Reports & Analytics' : undefined"
value="reports"
class="glass-nav-item animated-nav-item"
/>
</template>
</v-tooltip>
<!-- Governance -->
<v-tooltip
:text="miniVariant ? 'Governance' : ''"
location="end"
:disabled="!miniVariant"
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
to="/board/governance"
prepend-icon="mdi-gavel"
:title="!miniVariant ? 'Governance' : undefined"
value="governance"
class="glass-nav-item animated-nav-item"
/>
</template>
</v-tooltip>
<!-- Communications -->
<v-tooltip
:text="miniVariant ? 'Communications' : ''"
location="end"
:disabled="!miniVariant"
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
to="/board/communications"
prepend-icon="mdi-email-newsletter"
:title="!miniVariant ? 'Communications' : undefined"
value="communications"
class="glass-nav-item animated-nav-item"
/>
</template>
</v-tooltip>
<v-divider class="my-2 glass-divider" />
<!-- Member Section Access -->
<v-list-subheader v-if="!miniVariant" class="monaco-subheader">Member Portal</v-list-subheader>
<v-tooltip
:text="miniVariant ? 'Member View' : ''"
location="end"
:disabled="!miniVariant"
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
to="/member/dashboard"
prepend-icon="mdi-account"
:title="!miniVariant ? 'Member View' : undefined"
value="member-view"
class="glass-nav-item animated-nav-item"
/>
</template>
</v-tooltip>
</v-list>
<!-- Enhanced Profile Card -->
<template v-slot:append>
<div class="pa-3">
<v-card class="glass-profile-card pa-3">
<div class="d-flex flex-column align-center">
<ProfileAvatar
:member-id="memberData?.member_id || memberData?.Id"
:first-name="memberData?.first_name || user?.firstName"
:last-name="memberData?.last_name || user?.lastName"
:member-name="memberData?.FullName || user?.name"
size="small"
class="mb-2"
/>
<div class="text-center">
<div class="text-subtitle-2 font-weight-bold">{{ user?.name || 'Board Member' }}</div>
<div class="text-caption text-medium-emphasis">{{ user?.email || 'board@monacousa.org' }}</div>
</div>
<v-menu location="top">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
icon="mdi-dots-vertical"
size="x-small"
variant="text"
class="mt-2"
/>
</template>
<v-list density="compact" class="glass-menu">
<v-list-item @click="() => {}">
<template v-slot:prepend>
<v-icon size="small">mdi-account</v-icon>
</template>
<v-list-item-title>Profile</v-list-item-title>
</v-list-item>
<v-list-item @click="() => {}">
<template v-slot:prepend>
<v-icon size="small">mdi-cog</v-icon>
</template>
<v-list-item-title>Settings</v-list-item-title>
</v-list-item>
<v-divider class="my-1" />
<v-list-item @click="logout">
<template v-slot:prepend>
<v-icon size="small">mdi-logout</v-icon>
</template>
<v-list-item-title>Logout</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</v-card>
</div>
</template>
</v-navigation-drawer>
<v-app-bar elevation="0" flat class="glass-app-bar board-bar">
<v-btn
icon
@click="toggleDrawer"
class="glass-icon-btn mr-2"
>
<v-icon>{{ miniVariant ? 'mdi-menu' : 'mdi-menu-open' }}</v-icon>
</v-btn>
<v-toolbar-title class="font-weight-bold text-white">
Board Portal
</v-toolbar-title>
<v-spacer />
<!-- Quick Actions with Glass Effects -->
<v-btn
icon
class="glass-icon-btn"
@click="toggleSearch"
>
<v-icon>mdi-magnify</v-icon>
</v-btn>
<v-btn icon class="glass-icon-btn">
<v-badge
:content="notifications"
:value="notifications > 0"
color="error"
>
<v-icon>mdi-bell</v-icon>
</v-badge>
</v-btn>
<!-- User Menu -->
<v-menu offset-y>
<template v-slot:activator="{ props }">
<v-btn icon v-bind="props" class="glass-icon-btn">
<ProfileAvatar
:member-id="memberData?.member_id"
:member-name="user?.name"
:first-name="user?.firstName || memberData?.first_name"
:last-name="user?.lastName || memberData?.last_name"
size="small"
:lazy="false"
show-border
/>
</v-btn>
</template>
<v-list min-width="250" class="glass-dropdown">
<v-list-item>
<v-list-item-title class="font-weight-bold">
{{ user?.name || 'Board Member' }}
</v-list-item-title>
<v-list-item-subtitle>
{{ user?.email }}
</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<v-chip
size="x-small"
class="monaco-chip-gradient"
>
BOARD MEMBER
</v-chip>
</v-list-item>
<v-divider class="my-2 glass-divider" />
<v-list-item to="/board/profile" class="glass-dropdown-item">
<template v-slot:prepend>
<v-icon>mdi-account</v-icon>
</template>
<v-list-item-title>Board Profile</v-list-item-title>
</v-list-item>
<v-list-item to="/member/dashboard" class="glass-dropdown-item">
<template v-slot:prepend>
<v-icon>mdi-account-switch</v-icon>
</template>
<v-list-item-title>Member Portal</v-list-item-title>
</v-list-item>
<v-list-item to="/board/settings" class="glass-dropdown-item">
<template v-slot:prepend>
<v-icon>mdi-cog</v-icon>
</template>
<v-list-item-title>Settings</v-list-item-title>
</v-list-item>
<v-divider class="my-2 glass-divider" />
<v-list-item @click="handleLogout" class="glass-dropdown-item text-error">
<template v-slot:prepend>
<v-icon color="error">mdi-logout</v-icon>
</template>
<v-list-item-title>Logout</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-app-bar>
<!-- Search Overlay with Glass Effect -->
<v-dialog v-model="searchOpen" max-width="600" persistent>
<v-card class="glass-card">
<v-card-title class="d-flex align-center">
<v-icon class="mr-2 monaco-red-text">mdi-magnify</v-icon>
Search Members
<v-spacer />
<v-btn icon @click="searchOpen = false" class="glass-icon-btn-dark">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-text-field
v-model="searchQuery"
label="Search by name, email, or member ID"
prepend-inner-icon="mdi-magnify"
variant="outlined"
autofocus
@keyup.enter="performSearch"
class="glass-input"
/>
</v-card-text>
</v-card>
</v-dialog>
<v-main class="glass-main">
<v-container fluid class="pa-6">
<slot />
</v-container>
</v-main>
</v-app>
</template>
<script setup lang="ts">
import type { Member } from '~/utils/types';
import ProfileAvatar from '~/components/ProfileAvatar.vue';
const { user, logout } = useAuth();
const drawer = ref(true);
const miniVariant = ref(false);
const notifications = ref(0);
const pendingApplications = ref(0);
const searchOpen = ref(false);
const searchQuery = ref('');
// Fetch member data
const { data: sessionData } = await useFetch<{ success: boolean; member: Member | null }>('/api/auth/session', {
server: false
});
const memberData = computed<Member | null>(() => sessionData.value?.member || null);
// Load board-specific notifications
onMounted(async () => {
try {
const [notificationsRes, applicationsRes] = await Promise.all([
$fetch('/api/board/notifications/count'),
$fetch('/api/board/applications/pending/count')
]);
notifications.value = notificationsRes?.data?.count || 0;
pendingApplications.value = applicationsRes?.data?.count || 0;
} catch (error) {
console.error('Error fetching board data:', error);
}
});
const toggleDrawer = () => {
miniVariant.value = !miniVariant.value;
};
const toggleSearch = () => {
searchOpen.value = true;
};
const performSearch = () => {
if (searchQuery.value) {
navigateTo(`/board/members?search=${encodeURIComponent(searchQuery.value)}`);
searchOpen.value = false;
searchQuery.value = '';
}
};
const handleLogout = async () => {
await logout();
};
// Responsive drawer behavior
const { width } = useDisplay();
watch(width, (newWidth) => {
drawer.value = newWidth >= 1024;
}, { immediate: true });
</script>
<style scoped lang="scss">
@import '~/assets/scss/main.scss';
// Glass Drawer Styles
.glass-drawer {
@include glass-effect(0.95, 30px);
border-right: 1px solid rgba(255, 255, 255, 0.2) !important;
}
.glass-logo-section {
background: linear-gradient(135deg,
rgba(220, 38, 38, 0.05) 0%,
rgba(255, 255, 255, 0.8) 100%);
border-radius: 16px;
margin-bottom: 8px;
}
.float-animation {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
// Monaco Text Colors
.monaco-red-text {
color: #dc2626 !important;
}
// Glass Navigation Items
.glass-nav-list {
background: transparent !important;
}
.glass-nav-item {
border-radius: 12px !important;
margin: 4px 12px !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
&:hover {
background: rgba(220, 38, 38, 0.05) !important;
transform: translateX(2px);
}
&.v-list-item--active {
background: linear-gradient(135deg,
rgba(220, 38, 38, 0.15) 0%,
rgba(220, 38, 38, 0.08) 100%) !important;
color: #dc2626 !important;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 70%;
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
border-radius: 0 2px 2px 0;
}
.v-icon {
color: #dc2626 !important;
}
}
}
.glass-nav-item-sub {
padding-left: 52px !important;
border-radius: 8px !important;
margin: 2px 12px 2px 24px !important;
transition: all 0.2s ease !important;
&:hover {
background: rgba(220, 38, 38, 0.03) !important;
}
&.v-list-item--active {
background: rgba(220, 38, 38, 0.08) !important;
color: #dc2626 !important;
}
}
// Monaco Subheader
.monaco-subheader {
color: #dc2626 !important;
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
// Glass Divider
.glass-divider {
opacity: 0.2;
border-color: rgba(220, 38, 38, 0.2);
}
// Board App Bar with Gradient
.board-bar {
background: linear-gradient(135deg,
rgba(220, 38, 38, 0.9) 0%,
rgba(124, 45, 18, 0.9) 100%) !important;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
// Glass Icon Buttons
.glass-icon-btn {
background: rgba(255, 255, 255, 0.1) !important;
backdrop-filter: blur(10px);
color: white !important;
transition: all 0.3s ease !important;
&:hover {
background: rgba(255, 255, 255, 0.2) !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.glass-icon-btn-dark {
background: rgba(0, 0, 0, 0.05) !important;
backdrop-filter: blur(10px);
color: #71717a !important;
transition: all 0.3s ease !important;
&:hover {
background: rgba(0, 0, 0, 0.1) !important;
transform: translateY(-1px);
}
}
// Glass Badge
.glass-badge {
:deep(.v-badge__badge) {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
}
}
// Monaco Chip
.monaco-chip-gradient {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important;
color: white !important;
border: none;
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.25);
}
// Glass Dropdown
.glass-dropdown {
@include glass-effect(0.95, 20px);
border-radius: 12px !important;
overflow: hidden;
}
.glass-dropdown-item {
transition: all 0.2s ease !important;
&:hover {
background: rgba(220, 38, 38, 0.05) !important;
}
}
// Glass Input
.glass-input {
:deep(.v-field) {
background: rgba(255, 255, 255, 0.5) !important;
backdrop-filter: blur(10px);
border: 1px solid rgba(220, 38, 38, 0.2);
}
}
// Glass Main Background
.glass-main {
background: linear-gradient(180deg,
rgba(250, 250, 250, 0.9) 0%,
rgba(245, 245, 245, 0.9) 100%);
min-height: 100vh;
position: relative;
&::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
radial-gradient(circle at 20% 50%, rgba(220, 38, 38, 0.03) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(220, 38, 38, 0.03) 0%, transparent 50%),
radial-gradient(circle at 40% 20%, rgba(220, 38, 38, 0.03) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
}
// Responsive adjustments
@media (max-width: 1024px) {
.glass-nav-item {
margin: 2px 8px !important;
}
.glass-nav-item-sub {
margin: 2px 8px 2px 16px !important;
}
}
</style>