499 lines
12 KiB
Vue
499 lines
12 KiB
Vue
<template>
|
|
<div class="board-dashboard">
|
|
<!-- Dues Payment Banner -->
|
|
<DuesPaymentBanner />
|
|
|
|
<!-- Enhanced Welcome Header -->
|
|
<div class="dashboard-header glass-header mb-6">
|
|
<h1 class="dashboard-title text-gradient">
|
|
Welcome Back, {{ firstName }}!
|
|
</h1>
|
|
|
|
<!-- Profile Picture Section -->
|
|
<div class="profile-picture-section my-4">
|
|
<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="80"
|
|
show-border
|
|
class="profile-avatar-main"
|
|
/>
|
|
</div>
|
|
|
|
<p class="dashboard-subtitle">
|
|
MonacoUSA Board Portal
|
|
</p>
|
|
<div class="text-center">
|
|
<v-chip class="glass-badge mt-2">
|
|
<v-icon start>mdi-shield-account</v-icon>
|
|
Board Member
|
|
</v-chip>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Board Statistics with Bento Grid -->
|
|
<div class="bento-grid mb-6">
|
|
<div class="bento-item bento-item--xlarge">
|
|
<v-card class="glass-card stat-card animated-entrance" style="animation-delay: 0.2s;">
|
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
|
<v-icon class="mr-2" color="primary">mdi-chart-box-outline</v-icon>
|
|
Board Overview
|
|
</v-card-title>
|
|
<v-card-text class="pa-4">
|
|
<v-row>
|
|
<v-col cols="6" md="3" class="text-center">
|
|
<div class="stat-value">{{ stats.totalMembers }}</div>
|
|
<div class="text-body-2">Total Members</div>
|
|
</v-col>
|
|
<v-col cols="6" md="3" class="text-center">
|
|
<div class="stat-value">{{ stats.activeMembers }}</div>
|
|
<div class="text-body-2">Active Members</div>
|
|
</v-col>
|
|
<v-col cols="6" md="6" class="text-center">
|
|
<div class="stat-value">{{ stats.upcomingEvents }}</div>
|
|
<div class="text-body-2">Upcoming Events</div>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card-text>
|
|
</v-card>
|
|
</div>
|
|
|
|
<div class="bento-item bento-item--medium">
|
|
<v-card class="glass-card animated-entrance" style="animation-delay: 0.3s;">
|
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
|
<v-icon class="mr-2" color="primary">mdi-calendar-today</v-icon>
|
|
Next Event
|
|
</v-card-title>
|
|
<v-card-text class="pa-4">
|
|
<div class="text-h6 mb-2">{{ nextEvent.title }}</div>
|
|
<div class="text-body-2 mb-2">
|
|
<v-icon size="small" class="mr-1">mdi-calendar</v-icon>
|
|
{{ nextEvent.date }}
|
|
</div>
|
|
<div class="text-body-2 mb-4">
|
|
<v-icon size="small" class="mr-1">mdi-clock</v-icon>
|
|
{{ nextEvent.time }}
|
|
</div>
|
|
<v-btn
|
|
color="primary"
|
|
variant="outlined"
|
|
size="small"
|
|
style="border-color: #a31515; color: #a31515;"
|
|
@click="viewEventDetails"
|
|
>
|
|
View Details
|
|
</v-btn>
|
|
</v-card-text>
|
|
</v-card>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dues Management Section -->
|
|
<v-row class="mb-6">
|
|
<v-col cols="12">
|
|
<BoardDuesManagement
|
|
:refresh-trigger="duesRefreshTrigger"
|
|
@view-member="handleViewMember"
|
|
@view-all-members="navigateToMembers"
|
|
@member-updated="handleMemberUpdated"
|
|
/>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
|
|
<!-- View Member Dialog -->
|
|
<ViewMemberDialog
|
|
v-model="showViewDialog"
|
|
:member="selectedMember"
|
|
@edit="handleEditMember"
|
|
/>
|
|
|
|
<!-- Edit Member Dialog -->
|
|
<EditMemberDialog
|
|
v-model="showEditDialog"
|
|
:member="selectedMember"
|
|
@member-updated="handleMemberUpdated"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Member } from '~/utils/types';
|
|
import ProfileAvatar from '~/components/ProfileAvatar.vue';
|
|
|
|
definePageMeta({
|
|
layout: 'board',
|
|
middleware: 'auth'
|
|
});
|
|
|
|
const { firstName, isBoard, isAdmin, user } = useAuth();
|
|
|
|
// Fetch member data for profile
|
|
const { data: sessionData } = await useFetch<{ success: boolean; member: Member | null }>('/api/auth/session', {
|
|
server: false
|
|
});
|
|
|
|
const memberData = computed<Member | null>(() => sessionData.value?.member || null);
|
|
|
|
// Check board access on mount
|
|
onMounted(() => {
|
|
if (!isBoard.value && !isAdmin.value) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
statusMessage: 'Access denied. Board membership required.'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Dues management state
|
|
const duesRefreshTrigger = ref(0);
|
|
|
|
// Member dialog state
|
|
const showViewDialog = ref(false);
|
|
const showEditDialog = ref(false);
|
|
const selectedMember = ref<Member | null>(null);
|
|
|
|
// Real data for board dashboard
|
|
const stats = ref({
|
|
totalMembers: 0,
|
|
activeMembers: 0,
|
|
upcomingEvents: 0
|
|
});
|
|
|
|
const nextEvent = ref({
|
|
id: null,
|
|
title: 'Next Event',
|
|
date: 'Loading...',
|
|
time: 'Loading...',
|
|
location: 'TBD',
|
|
description: 'Upcoming association event'
|
|
});
|
|
|
|
const isLoading = ref(true);
|
|
|
|
// Load real data on component mount
|
|
onMounted(async () => {
|
|
await loadBoardData();
|
|
});
|
|
|
|
const loadBoardData = async () => {
|
|
try {
|
|
isLoading.value = true;
|
|
|
|
// Load board statistics
|
|
const [statsResponse, meetingResponse] = await Promise.allSettled([
|
|
$fetch('/api/board/stats'),
|
|
$fetch('/api/board/next-meeting')
|
|
]);
|
|
|
|
// Handle stats response
|
|
if (statsResponse.status === 'fulfilled') {
|
|
const statsData = statsResponse.value as any;
|
|
if (statsData?.success) {
|
|
stats.value = {
|
|
totalMembers: statsData.data.totalMembers || 0,
|
|
activeMembers: statsData.data.activeMembers || 0,
|
|
upcomingEvents: statsData.data.upcomingEvents || 0
|
|
};
|
|
}
|
|
}
|
|
|
|
// Handle next meeting response
|
|
if (meetingResponse.status === 'fulfilled') {
|
|
const meetingData = meetingResponse.value as any;
|
|
if (meetingData?.success) {
|
|
nextEvent.value = {
|
|
id: meetingData.data.id,
|
|
title: meetingData.data.title || 'Next Event',
|
|
date: meetingData.data.date || 'TBD',
|
|
time: meetingData.data.time || 'TBD',
|
|
location: meetingData.data.location || 'TBD',
|
|
description: meetingData.data.description || 'Upcoming association event'
|
|
};
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading board data:', error);
|
|
// Keep fallback values
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const recentActivity = ref([
|
|
{
|
|
id: 1,
|
|
title: 'Monthly Board Meeting',
|
|
description: 'Meeting minutes approved and distributed',
|
|
type: 'success',
|
|
status: 'Completed'
|
|
},
|
|
{
|
|
id: 2,
|
|
title: 'Budget Review',
|
|
description: 'Q4 financial report under review',
|
|
type: 'warning',
|
|
status: 'In Progress'
|
|
},
|
|
{
|
|
id: 3,
|
|
title: 'Member Application',
|
|
description: 'New member application pending approval',
|
|
type: 'info',
|
|
status: 'Pending'
|
|
}
|
|
]);
|
|
|
|
// Dues management handlers
|
|
const handleViewMember = (member: Member) => {
|
|
// Open the view dialog instead of navigating away
|
|
selectedMember.value = member;
|
|
showViewDialog.value = true;
|
|
};
|
|
|
|
const handleEditMember = (member: Member) => {
|
|
// Close the view dialog and open the edit dialog
|
|
showViewDialog.value = false;
|
|
selectedMember.value = member;
|
|
showEditDialog.value = true;
|
|
};
|
|
|
|
const handleMemberUpdated = (member: Member) => {
|
|
console.log('Member updated:', member.FullName || `${member.first_name} ${member.last_name}`);
|
|
|
|
// Close edit dialog
|
|
showEditDialog.value = false;
|
|
|
|
// Trigger dues refresh to update the lists
|
|
duesRefreshTrigger.value += 1;
|
|
|
|
// You could also update stats here if needed
|
|
// stats.value = await fetchUpdatedStats();
|
|
};
|
|
|
|
// Navigation methods
|
|
const navigateToEvents = () => {
|
|
// Navigate to events page
|
|
navigateTo('/dashboard/events');
|
|
};
|
|
|
|
const navigateToMembers = () => {
|
|
// Navigate to member list page
|
|
navigateTo('/dashboard/member-list');
|
|
};
|
|
|
|
const viewEventDetails = () => {
|
|
console.log('View event details');
|
|
};
|
|
|
|
const scheduleNewMeeting = () => {
|
|
console.log('Schedule new meeting');
|
|
};
|
|
|
|
const createAnnouncement = () => {
|
|
console.log('Create announcement');
|
|
};
|
|
|
|
const generateReport = () => {
|
|
console.log('Generate report');
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.board-dashboard {
|
|
padding: 1rem;
|
|
}
|
|
|
|
/* Enhanced Header */
|
|
.dashboard-header {
|
|
margin-bottom: 2rem;
|
|
padding: 2rem;
|
|
border-radius: 20px;
|
|
background: linear-gradient(
|
|
135deg,
|
|
rgba(255, 255, 255, 0.95),
|
|
rgba(255, 255, 255, 0.85)
|
|
);
|
|
backdrop-filter: blur(30px) saturate(180%);
|
|
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
|
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
box-shadow:
|
|
0 8px 32px 0 rgba(31, 38, 135, 0.15),
|
|
inset 0 1px 2px rgba(255, 255, 255, 0.6);
|
|
animation: slide-up 0.6s ease-out;
|
|
text-align: center;
|
|
|
|
.profile-picture-section {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
|
|
.profile-avatar-main {
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
border: 3px solid white;
|
|
border-radius: 50%;
|
|
}
|
|
}
|
|
}
|
|
|
|
.dashboard-title {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
margin-bottom: 0.5rem;
|
|
animation: fade-in 0.8s ease-out;
|
|
}
|
|
|
|
.dashboard-subtitle {
|
|
color: #71717a;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.glass-badge {
|
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important;
|
|
color: white !important;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Bento Grid Layout */
|
|
.bento-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(12, 1fr);
|
|
gap: 1.5rem;
|
|
|
|
.bento-item {
|
|
border-radius: 20px;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
&--small { grid-column: span 3; }
|
|
&--medium { grid-column: span 4; }
|
|
&--large { grid-column: span 6; }
|
|
&--xlarge { grid-column: span 8; }
|
|
&--full { grid-column: span 12; }
|
|
|
|
&:hover {
|
|
transform: translateY(-4px);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Glass Card Effects */
|
|
.glass-card {
|
|
background: linear-gradient(
|
|
135deg,
|
|
rgba(255, 255, 255, 0.95),
|
|
rgba(255, 255, 255, 0.85),
|
|
rgba(255, 255, 255, 0.75)
|
|
) !important;
|
|
backdrop-filter: blur(30px) saturate(180%);
|
|
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
|
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
box-shadow:
|
|
0 8px 32px 0 rgba(31, 38, 135, 0.15),
|
|
inset 0 1px 2px rgba(255, 255, 255, 0.6),
|
|
inset 0 -1px 2px rgba(0, 0, 0, 0.05) !important;
|
|
border-radius: 20px !important;
|
|
|
|
&:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow:
|
|
0 12px 40px 0 rgba(31, 38, 135, 0.25),
|
|
inset 0 1px 2px rgba(255, 255, 255, 0.8),
|
|
inset 0 -1px 2px rgba(0, 0, 0, 0.05) !important;
|
|
}
|
|
}
|
|
|
|
/* Statistics Cards */
|
|
.stat-card {
|
|
.stat-value {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
}
|
|
|
|
/* Animated Entrance */
|
|
.animated-entrance {
|
|
animation: slide-up 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
|
}
|
|
|
|
@keyframes slide-up {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes fade-in {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
/* Button Enhancements */
|
|
.v-btn {
|
|
text-transform: none !important;
|
|
font-weight: 600;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
&:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.25);
|
|
}
|
|
}
|
|
|
|
h3 {
|
|
color: #333;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.text-body-2 {
|
|
color: #666;
|
|
}
|
|
|
|
.v-chip {
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 1280px) {
|
|
.bento-grid {
|
|
.bento-item--xlarge {
|
|
grid-column: span 12;
|
|
}
|
|
.bento-item--large {
|
|
grid-column: span 6;
|
|
}
|
|
.bento-item--medium {
|
|
grid-column: span 6;
|
|
}
|
|
}
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.bento-grid {
|
|
.bento-item--large {
|
|
grid-column: span 12;
|
|
}
|
|
.bento-item--medium {
|
|
grid-column: span 12;
|
|
}
|
|
}
|
|
}
|
|
</style>
|