feat: Reorganize platform into member, board, and admin sections
Build And Push Image / docker (push) Failing after 55s
Details
Build And Push Image / docker (push) Failing after 55s
Details
Major platform reorganization implementing role-based portal sections: ## Infrastructure Changes - Created role-based middleware for member, board, and admin access - Updated main dashboard router to redirect based on highest privilege - Implemented access hierarchy: Admin > Board > Member ## New Layouts - Member layout: Simplified navigation for regular members - Board layout: Enhanced tools for board member management - Admin layout: Full system administration capabilities ## Member Portal (/member/*) - Dashboard: Profile overview, events, payments, activity tracking - Events: Browse, register, and manage event participation - Profile: Complete personal and professional information management - Resources: Access to documents, guides, FAQs, and quick links ## Board Portal (/board/*) - Dashboard: Statistics, dues management, board-specific tools - Members: Comprehensive member management with filtering ## Admin Portal (/admin/*) - Dashboard: System overview and administrative controls (existing) ## Design Implementation - Monaco red (#dc2626) as primary accent color - Modern card-based layouts with consistent spacing - Responsive design for all screen sizes - Glass morphism effects for enhanced visual appeal This reorganization provides clear separation of concerns based on user privileges while maintaining a cohesive user experience across all sections. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
27e38d98e5
commit
d9d8627e97
|
|
@ -0,0 +1,511 @@
|
||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-navigation-drawer v-model="drawer" app width="300">
|
||||||
|
<!-- Logo Section -->
|
||||||
|
<v-list-item class="pa-4 text-center">
|
||||||
|
<v-img
|
||||||
|
src="/MONACOUSA-Flags_376x376.png"
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
class="mx-auto mb-2"
|
||||||
|
/>
|
||||||
|
<div class="text-h6 font-weight-bold" style="color: #dc2626;">
|
||||||
|
MonacoUSA Portal
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-error font-weight-bold">ADMINISTRATOR</div>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<!-- Navigation Menu -->
|
||||||
|
<v-list nav density="comfortable">
|
||||||
|
<!-- Admin Overview -->
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/dashboard"
|
||||||
|
prepend-icon="mdi-view-dashboard"
|
||||||
|
title="Admin Dashboard"
|
||||||
|
value="dashboard"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- User Management -->
|
||||||
|
<v-list-group value="users">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
prepend-icon="mdi-account-cog"
|
||||||
|
title="User Management"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/users"
|
||||||
|
title="All Users"
|
||||||
|
value="users-list"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/users/roles"
|
||||||
|
title="Roles & Permissions"
|
||||||
|
value="roles"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
@click="openKeycloak"
|
||||||
|
title="Keycloak Admin"
|
||||||
|
value="keycloak"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-icon size="small">mdi-open-in-new</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list-group>
|
||||||
|
|
||||||
|
<!-- Member Management -->
|
||||||
|
<v-list-group value="members">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
prepend-icon="mdi-account-group"
|
||||||
|
title="Member Management"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/members"
|
||||||
|
title="All Members"
|
||||||
|
value="members-list"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/members/import"
|
||||||
|
title="Import Members"
|
||||||
|
value="import"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/members/export"
|
||||||
|
title="Export Data"
|
||||||
|
value="export"
|
||||||
|
/>
|
||||||
|
</v-list-group>
|
||||||
|
|
||||||
|
<!-- Financial Management -->
|
||||||
|
<v-list-group value="financial">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
prepend-icon="mdi-currency-usd"
|
||||||
|
title="Financial"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/payments"
|
||||||
|
title="Payment Management"
|
||||||
|
value="payments"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/payments/stripe"
|
||||||
|
title="Stripe Dashboard"
|
||||||
|
value="stripe"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/payments/reports"
|
||||||
|
title="Financial Reports"
|
||||||
|
value="financial-reports"
|
||||||
|
/>
|
||||||
|
</v-list-group>
|
||||||
|
|
||||||
|
<!-- System Configuration -->
|
||||||
|
<v-list-group value="system">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
prepend-icon="mdi-cog"
|
||||||
|
title="System"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/settings"
|
||||||
|
title="General Settings"
|
||||||
|
value="settings"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/settings/email"
|
||||||
|
title="Email Configuration"
|
||||||
|
value="email"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/settings/security"
|
||||||
|
title="Security Settings"
|
||||||
|
value="security"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/logs"
|
||||||
|
title="System Logs"
|
||||||
|
value="logs"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/backup"
|
||||||
|
title="Backup & Restore"
|
||||||
|
value="backup"
|
||||||
|
/>
|
||||||
|
</v-list-group>
|
||||||
|
|
||||||
|
<!-- Events Management -->
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/events"
|
||||||
|
prepend-icon="mdi-calendar"
|
||||||
|
title="Events Management"
|
||||||
|
value="events"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Analytics -->
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/analytics"
|
||||||
|
prepend-icon="mdi-chart-line"
|
||||||
|
title="Analytics & Insights"
|
||||||
|
value="analytics"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
|
||||||
|
<!-- Portal Access -->
|
||||||
|
<v-list-subheader>Portal Access</v-list-subheader>
|
||||||
|
<v-list-item
|
||||||
|
to="/board/dashboard"
|
||||||
|
prepend-icon="mdi-shield-account"
|
||||||
|
title="Board Portal"
|
||||||
|
value="board-view"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/member/dashboard"
|
||||||
|
prepend-icon="mdi-account"
|
||||||
|
title="Member Portal"
|
||||||
|
value="member-view"
|
||||||
|
/>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<template v-slot:append>
|
||||||
|
<div class="pa-4">
|
||||||
|
<v-card
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
class="text-center pa-3"
|
||||||
|
>
|
||||||
|
<v-icon size="small" class="mb-1" color="white">mdi-shield-crown</v-icon>
|
||||||
|
<div class="text-caption font-weight-bold text-white">ADMINISTRATOR</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<v-app-bar app elevation="0" flat>
|
||||||
|
<!-- Admin header with special styling -->
|
||||||
|
<template v-slot:extension>
|
||||||
|
<div class="admin-header-gradient"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Navigation Toggle -->
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="drawer = !drawer"
|
||||||
|
class="mr-2"
|
||||||
|
color="white"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-menu</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-toolbar-title class="text-white font-weight-bold d-flex align-center">
|
||||||
|
Admin Portal
|
||||||
|
<v-chip
|
||||||
|
size="x-small"
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
FULL ACCESS
|
||||||
|
</v-chip>
|
||||||
|
</v-toolbar-title>
|
||||||
|
|
||||||
|
<v-spacer />
|
||||||
|
|
||||||
|
<!-- System Status Indicator -->
|
||||||
|
<v-chip
|
||||||
|
:color="systemStatus === 'healthy' ? 'success' : 'warning'"
|
||||||
|
variant="flat"
|
||||||
|
size="small"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
<v-icon start size="small">
|
||||||
|
{{ systemStatus === 'healthy' ? 'mdi-check-circle' : 'mdi-alert' }}
|
||||||
|
</v-icon>
|
||||||
|
System {{ systemStatus }}
|
||||||
|
</v-chip>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
color="white"
|
||||||
|
@click="toggleCommandPalette"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-console</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn icon color="white">
|
||||||
|
<v-badge
|
||||||
|
:content="alerts"
|
||||||
|
:value="alerts > 0"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-bell-alert</v-icon>
|
||||||
|
</v-badge>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<!-- User Menu -->
|
||||||
|
<v-menu offset-y>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn icon v-bind="props" color="white">
|
||||||
|
<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">
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title class="font-weight-bold">
|
||||||
|
{{ user?.name || 'Administrator' }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ user?.email }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-chip
|
||||||
|
color="error"
|
||||||
|
size="x-small"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
ADMINISTRATOR
|
||||||
|
</v-chip>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
|
||||||
|
<v-list-item to="/admin/profile">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-account</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Admin Profile</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item to="/board/dashboard">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-shield-account</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Board Portal</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item to="/member/dashboard">
|
||||||
|
<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="/admin/settings">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-cog</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>System Settings</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
|
||||||
|
<v-list-item @click="handleLogout" class="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>
|
||||||
|
|
||||||
|
<!-- Command Palette Dialog -->
|
||||||
|
<v-dialog v-model="commandPaletteOpen" max-width="600">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon class="mr-2">mdi-console</v-icon>
|
||||||
|
Admin Command Palette
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn icon @click="commandPaletteOpen = false">
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-model="commandQuery"
|
||||||
|
label="Type a command..."
|
||||||
|
prepend-inner-icon="mdi-chevron-right"
|
||||||
|
variant="outlined"
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="executeCommand"
|
||||||
|
/>
|
||||||
|
<div class="text-caption">
|
||||||
|
Available commands: clear-cache, rebuild-index, sync-users, export-data
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-main>
|
||||||
|
<v-container fluid class="pa-6">
|
||||||
|
<!-- System Alerts Banner -->
|
||||||
|
<v-alert
|
||||||
|
v-if="systemAlerts.length > 0"
|
||||||
|
type="warning"
|
||||||
|
variant="tonal"
|
||||||
|
closable
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<v-alert-title>System Alerts</v-alert-title>
|
||||||
|
<ul class="mt-2">
|
||||||
|
<li v-for="alert in systemAlerts" :key="alert.id">
|
||||||
|
{{ alert.message }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Member } from '~/utils/types';
|
||||||
|
|
||||||
|
const { user, logout } = useAuth();
|
||||||
|
const drawer = ref(true);
|
||||||
|
const alerts = ref(0);
|
||||||
|
const systemStatus = ref<'healthy' | 'warning' | 'error'>('healthy');
|
||||||
|
const systemAlerts = ref<Array<{ id: number; message: string }>>([]);
|
||||||
|
const commandPaletteOpen = ref(false);
|
||||||
|
const commandQuery = 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 admin-specific data
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// Check system health
|
||||||
|
const healthCheck = await $fetch('/api/admin/system/health');
|
||||||
|
systemStatus.value = healthCheck?.data?.status || 'healthy';
|
||||||
|
|
||||||
|
// Get critical alerts
|
||||||
|
const alertsRes = await $fetch('/api/admin/alerts');
|
||||||
|
alerts.value = alertsRes?.data?.count || 0;
|
||||||
|
systemAlerts.value = alertsRes?.data?.alerts || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching admin data:', error);
|
||||||
|
systemStatus.value = 'warning';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const openKeycloak = () => {
|
||||||
|
window.open('https://auth.monacousa.org/admin', '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCommandPalette = () => {
|
||||||
|
commandPaletteOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeCommand = async () => {
|
||||||
|
if (commandQuery.value) {
|
||||||
|
console.log('Executing command:', commandQuery.value);
|
||||||
|
// Implement command execution logic
|
||||||
|
commandPaletteOpen.value = false;
|
||||||
|
commandQuery.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logout();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Responsive drawer behavior
|
||||||
|
const { width } = useDisplay();
|
||||||
|
watch(width, (newWidth) => {
|
||||||
|
drawer.value = newWidth >= 1024;
|
||||||
|
}, { immediate: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-header-gradient {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, #991b1b 0%, #450a0a 100%);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-navigation-drawer {
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #fef2f2 100%);
|
||||||
|
border-right: 2px solid rgba(153, 27, 27, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item {
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 4px 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item:hover {
|
||||||
|
background-color: rgba(153, 27, 27, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item--active {
|
||||||
|
background: linear-gradient(135deg, rgba(153, 27, 27, 0.2) 0%, rgba(153, 27, 27, 0.1) 100%) !important;
|
||||||
|
color: #991b1b !important;
|
||||||
|
border-left: 3px solid #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item--active .v-icon {
|
||||||
|
color: #991b1b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-group__items .v-list-item {
|
||||||
|
padding-left: 52px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-subheader {
|
||||||
|
color: #991b1b !important;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-app-bar {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-main {
|
||||||
|
background: linear-gradient(180deg, #fafafa 0%, #f5f5f5 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,398 @@
|
||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-navigation-drawer v-model="drawer" app width="280">
|
||||||
|
<!-- Logo Section -->
|
||||||
|
<v-list-item class="pa-4 text-center">
|
||||||
|
<v-img
|
||||||
|
src="/MONACOUSA-Flags_376x376.png"
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
class="mx-auto mb-2"
|
||||||
|
/>
|
||||||
|
<div class="text-h6 font-weight-bold" style="color: #dc2626;">
|
||||||
|
MonacoUSA Portal
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">Board Portal</div>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<!-- Navigation Menu -->
|
||||||
|
<v-list nav density="comfortable">
|
||||||
|
<!-- Board Overview -->
|
||||||
|
<v-list-item
|
||||||
|
to="/board/dashboard"
|
||||||
|
prepend-icon="mdi-view-dashboard"
|
||||||
|
title="Board Dashboard"
|
||||||
|
value="dashboard"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Member Management -->
|
||||||
|
<v-list-group value="members">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
prepend-icon="mdi-account-group"
|
||||||
|
title="Members"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/board/members"
|
||||||
|
title="Member Directory"
|
||||||
|
value="member-list"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/board/members/dues"
|
||||||
|
title="Dues Management"
|
||||||
|
value="dues"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/board/members/applications"
|
||||||
|
title="Applications"
|
||||||
|
value="applications"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-badge
|
||||||
|
:content="pendingApplications"
|
||||||
|
:value="pendingApplications > 0"
|
||||||
|
color="error"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list-group>
|
||||||
|
|
||||||
|
<!-- Events & Meetings -->
|
||||||
|
<v-list-group value="events">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
prepend-icon="mdi-calendar"
|
||||||
|
title="Events & Meetings"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/board/events"
|
||||||
|
title="All Events"
|
||||||
|
value="events"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/board/meetings"
|
||||||
|
title="Board Meetings"
|
||||||
|
value="meetings"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
to="/board/meetings/minutes"
|
||||||
|
title="Meeting Minutes"
|
||||||
|
value="minutes"
|
||||||
|
/>
|
||||||
|
</v-list-group>
|
||||||
|
|
||||||
|
<!-- Reports & Analytics -->
|
||||||
|
<v-list-item
|
||||||
|
to="/board/reports"
|
||||||
|
prepend-icon="mdi-chart-box"
|
||||||
|
title="Reports & Analytics"
|
||||||
|
value="reports"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Governance -->
|
||||||
|
<v-list-item
|
||||||
|
to="/board/governance"
|
||||||
|
prepend-icon="mdi-gavel"
|
||||||
|
title="Governance"
|
||||||
|
value="governance"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Communications -->
|
||||||
|
<v-list-item
|
||||||
|
to="/board/communications"
|
||||||
|
prepend-icon="mdi-email-newsletter"
|
||||||
|
title="Communications"
|
||||||
|
value="communications"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
|
||||||
|
<!-- Member Section Access -->
|
||||||
|
<v-list-subheader>Member Portal</v-list-subheader>
|
||||||
|
<v-list-item
|
||||||
|
to="/member/dashboard"
|
||||||
|
prepend-icon="mdi-account"
|
||||||
|
title="Member View"
|
||||||
|
value="member-view"
|
||||||
|
/>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<template v-slot:append>
|
||||||
|
<div class="pa-4">
|
||||||
|
<v-card
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
class="text-center pa-3"
|
||||||
|
>
|
||||||
|
<v-icon size="small" class="mb-1">mdi-shield-account</v-icon>
|
||||||
|
<div class="text-caption font-weight-bold">BOARD ACCESS</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<v-app-bar app elevation="0" flat>
|
||||||
|
<!-- Custom gradient background -->
|
||||||
|
<template v-slot:extension>
|
||||||
|
<div class="board-header-gradient"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Navigation Toggle -->
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="drawer = !drawer"
|
||||||
|
class="mr-2"
|
||||||
|
color="white"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-menu</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-toolbar-title class="text-white font-weight-bold">
|
||||||
|
Board Portal
|
||||||
|
</v-toolbar-title>
|
||||||
|
|
||||||
|
<v-spacer />
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
color="white"
|
||||||
|
@click="toggleSearch"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-magnify</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn icon color="white">
|
||||||
|
<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" color="white">
|
||||||
|
<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">
|
||||||
|
<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
|
||||||
|
color="primary"
|
||||||
|
size="x-small"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
BOARD MEMBER
|
||||||
|
</v-chip>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
|
||||||
|
<v-list-item to="/board/profile">
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
<v-list-item @click="handleLogout" class="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 -->
|
||||||
|
<v-dialog v-model="searchOpen" max-width="600" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon class="mr-2">mdi-magnify</v-icon>
|
||||||
|
Search Members
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn icon @click="searchOpen = false">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-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';
|
||||||
|
|
||||||
|
const { user, logout } = useAuth();
|
||||||
|
const drawer = ref(true);
|
||||||
|
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 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>
|
||||||
|
.board-header-gradient {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #7c2d12 100%);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-navigation-drawer {
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #fef2f2 100%);
|
||||||
|
border-right: 1px solid rgba(220, 38, 38, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item {
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 4px 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item:hover {
|
||||||
|
background-color: rgba(220, 38, 38, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
border-left: 3px solid #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item--active .v-icon {
|
||||||
|
color: #dc2626 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-group__items .v-list-item {
|
||||||
|
padding-left: 52px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-subheader {
|
||||||
|
color: #dc2626 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-app-bar {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-main {
|
||||||
|
background: linear-gradient(180deg, #fafafa 0%, #f5f5f5 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-navigation-drawer v-model="drawer" app width="280">
|
||||||
|
<!-- Logo Section -->
|
||||||
|
<v-list-item class="pa-4 text-center">
|
||||||
|
<v-img
|
||||||
|
src="/MONACOUSA-Flags_376x376.png"
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
class="mx-auto mb-2"
|
||||||
|
/>
|
||||||
|
<div class="text-h6 font-weight-bold" style="color: #dc2626;">
|
||||||
|
MonacoUSA Portal
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">Member Portal</div>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<!-- Navigation Menu -->
|
||||||
|
<v-list nav>
|
||||||
|
<v-list-item
|
||||||
|
to="/member/dashboard"
|
||||||
|
prepend-icon="mdi-view-dashboard"
|
||||||
|
title="Dashboard"
|
||||||
|
value="dashboard"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/member/profile"
|
||||||
|
prepend-icon="mdi-account"
|
||||||
|
title="My Profile"
|
||||||
|
value="profile"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/member/events"
|
||||||
|
prepend-icon="mdi-calendar"
|
||||||
|
title="Events"
|
||||||
|
value="events"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/member/payments"
|
||||||
|
prepend-icon="mdi-credit-card"
|
||||||
|
title="Payments & Dues"
|
||||||
|
value="payments"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/member/resources"
|
||||||
|
prepend-icon="mdi-book-open-variant"
|
||||||
|
title="Resources"
|
||||||
|
value="resources"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/member/directory"
|
||||||
|
prepend-icon="mdi-contacts"
|
||||||
|
title="Member Directory"
|
||||||
|
value="directory"
|
||||||
|
/>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<template v-slot:append>
|
||||||
|
<div class="pa-4">
|
||||||
|
<v-card
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
class="text-center pa-3"
|
||||||
|
>
|
||||||
|
<v-icon size="small" class="mb-1">mdi-account</v-icon>
|
||||||
|
<div class="text-caption font-weight-bold">MEMBER ACCESS</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<v-app-bar app color="primary" elevation="0" flat>
|
||||||
|
<!-- Navigation Toggle -->
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="drawer = !drawer"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-menu</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-toolbar-title class="text-white font-weight-bold">
|
||||||
|
Member Portal
|
||||||
|
</v-toolbar-title>
|
||||||
|
|
||||||
|
<v-spacer />
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<v-btn icon color="white">
|
||||||
|
<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" color="white">
|
||||||
|
<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">
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title class="font-weight-bold">
|
||||||
|
{{ user?.name || 'Member' }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ user?.email }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-chip
|
||||||
|
color="info"
|
||||||
|
size="x-small"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
MEMBER
|
||||||
|
</v-chip>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
|
||||||
|
<v-list-item to="/member/profile">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-account</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>My Profile</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item to="/member/settings">
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
<v-list-item @click="handleLogout" class="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>
|
||||||
|
|
||||||
|
<v-main>
|
||||||
|
<!-- Dues Payment Banner for Members -->
|
||||||
|
<DuesPaymentBanner />
|
||||||
|
|
||||||
|
<v-container fluid class="pa-6">
|
||||||
|
<slot />
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Member } from '~/utils/types';
|
||||||
|
|
||||||
|
const { user, logout } = useAuth();
|
||||||
|
const drawer = ref(true);
|
||||||
|
const notifications = ref(0);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Check for notifications
|
||||||
|
onMounted(async () => {
|
||||||
|
// Check for upcoming events, dues reminders, etc.
|
||||||
|
try {
|
||||||
|
const { data } = await $fetch('/api/member/notifications/count');
|
||||||
|
notifications.value = data?.count || 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching notifications:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logout();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Responsive drawer behavior
|
||||||
|
const { width } = useDisplay();
|
||||||
|
watch(width, (newWidth) => {
|
||||||
|
drawer.value = newWidth >= 1024;
|
||||||
|
}, { immediate: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-navigation-drawer {
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #fafafa 100%);
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item {
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 4px 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item:hover {
|
||||||
|
background-color: rgba(220, 38, 38, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item--active {
|
||||||
|
background: linear-gradient(135deg, rgba(220, 38, 38, 0.1) 0%, rgba(220, 38, 38, 0.05) 100%) !important;
|
||||||
|
color: #dc2626 !important;
|
||||||
|
border-left: 3px solid #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item--active .v-icon {
|
||||||
|
color: #dc2626 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-app-bar {
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-main {
|
||||||
|
background: linear-gradient(180deg, #fafafa 0%, #f5f5f5 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const { isAuthenticated, isBoard, isAdmin } = useAuth();
|
||||||
|
|
||||||
|
if (!isAuthenticated.value) {
|
||||||
|
return navigateTo('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only board members and admins can access board pages
|
||||||
|
if (!isBoard.value && !isAdmin.value) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Access denied. Board member privileges required.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const { isAuthenticated, isUser, isBoard, isAdmin } = useAuth();
|
||||||
|
|
||||||
|
if (!isAuthenticated.value) {
|
||||||
|
return navigateTo('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Members, board members, and admins can all access member pages
|
||||||
|
if (!isUser.value && !isBoard.value && !isAdmin.value) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Access denied. Member privileges required.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,889 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<h1 class="text-h4 font-weight-bold mb-4">
|
||||||
|
<v-icon left>mdi-account</v-icon>
|
||||||
|
Welcome Back, {{ firstName }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-body-1 mb-6">
|
||||||
|
Manage users and portal settings for the MonacoUSA Portal.
|
||||||
|
</p>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Portal Status -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-caption text-medium-emphasis mb-1">Portal Status</p>
|
||||||
|
<p class="text-h5 font-weight-bold text-success">Online</p>
|
||||||
|
</div>
|
||||||
|
<v-icon color="success" size="40">mdi-check-circle</v-icon>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-caption text-medium-emphasis mb-1">Total Users</p>
|
||||||
|
<p class="text-h5 font-weight-bold">{{ userCount }}</p>
|
||||||
|
</div>
|
||||||
|
<v-icon color="primary" size="40">mdi-account-multiple</v-icon>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- User Management -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title>
|
||||||
|
<v-icon left>mdi-account-group</v-icon>
|
||||||
|
User Management
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p class="mb-4">Manage user accounts, roles, and permissions for the MonacoUSA Portal.</p>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
@click="navigateTo('/dashboard/member-list')"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-account-cog</v-icon>
|
||||||
|
Manage Users
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-btn
|
||||||
|
color="success"
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
@click="showCreateUserDialog = true"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-account-plus</v-icon>
|
||||||
|
Create User Account
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-btn
|
||||||
|
color="secondary"
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
@click="viewAuditLogs"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-file-document-outline</v-icon>
|
||||||
|
View Audit Logs
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-btn
|
||||||
|
color="secondary"
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
@click="showAdminConfig = true"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-cog</v-icon>
|
||||||
|
Portal Settings
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Dues Management -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Portal Configuration -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title>
|
||||||
|
<v-icon left>mdi-cog</v-icon>
|
||||||
|
Portal Configuration
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p class="mb-4">Configure all portal settings including database, email, reCAPTCHA, and membership fees in one centralized location.</p>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
@click="showAdminConfig = true"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-cog</v-icon>
|
||||||
|
Portal Settings
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-row dense>
|
||||||
|
<v-col cols="6" sm="3">
|
||||||
|
<v-chip color="success" variant="tonal" size="small" block>
|
||||||
|
<v-icon start size="14">mdi-database</v-icon>
|
||||||
|
NocoDB
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="3">
|
||||||
|
<v-chip color="info" variant="tonal" size="small" block>
|
||||||
|
<v-icon start size="14">mdi-email</v-icon>
|
||||||
|
Email
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="3">
|
||||||
|
<v-chip color="warning" variant="tonal" size="small" block>
|
||||||
|
<v-icon start size="14">mdi-shield</v-icon>
|
||||||
|
reCAPTCHA
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="3">
|
||||||
|
<v-chip color="primary" variant="tonal" size="small" block>
|
||||||
|
<v-icon start size="14">mdi-bank</v-icon>
|
||||||
|
Membership
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Data Management -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title>
|
||||||
|
<v-icon left>mdi-database-cog</v-icon>
|
||||||
|
Data Management
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p class="mb-4">Manage data integrity and perform maintenance operations on the portal database.</p>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-btn
|
||||||
|
@click="assignMemberIds"
|
||||||
|
color="warning"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-icon="mdi-account-multiple-plus"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
:loading="assigningMemberIds"
|
||||||
|
>
|
||||||
|
Assign Member IDs
|
||||||
|
</v-btn>
|
||||||
|
<div class="text-caption mt-2 text-medium-emphasis">
|
||||||
|
Assign unique member IDs (MUSA-0001, MUSA-0002, etc.) to members who don't have them
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-btn
|
||||||
|
@click="backfillEventIds"
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-icon="mdi-calendar-sync"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
:loading="backfillLoading"
|
||||||
|
>
|
||||||
|
Backfill Event IDs
|
||||||
|
</v-btn>
|
||||||
|
<div class="text-caption mt-2 text-medium-emphasis">
|
||||||
|
Assign business IDs to events that don't have them
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<!-- NocoDB Settings Dialog -->
|
||||||
|
<NocoDBSettingsDialog
|
||||||
|
v-model="showNocoDBSettings"
|
||||||
|
@settings-saved="handleSettingsSaved"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Admin Configuration Dialog -->
|
||||||
|
<AdminConfigurationDialog
|
||||||
|
v-model="showAdminConfig"
|
||||||
|
@settings-saved="handleAdminConfigSaved"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- reCAPTCHA Configuration Dialog -->
|
||||||
|
<v-dialog v-model="showRecaptchaConfig" max-width="600">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
<v-icon left>mdi-shield-account</v-icon>
|
||||||
|
reCAPTCHA Configuration
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-alert type="info" variant="tonal" class="mb-4">
|
||||||
|
<v-alert-title>Security Configuration</v-alert-title>
|
||||||
|
Configure Google reCAPTCHA settings for form protection on the registration page.
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-form ref="recaptchaForm" v-model="recaptchaValid">
|
||||||
|
<v-text-field
|
||||||
|
v-model="recaptchaConfig.siteKey"
|
||||||
|
label="Site Key"
|
||||||
|
placeholder="6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy"
|
||||||
|
:rules="[v => !!v || 'Site key is required']"
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="recaptchaConfig.secretKey"
|
||||||
|
label="Secret Key"
|
||||||
|
placeholder="6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx"
|
||||||
|
:rules="[v => !!v || 'Secret key is required']"
|
||||||
|
variant="outlined"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-alert type="warning" variant="tonal" class="mt-4">
|
||||||
|
<v-alert-title>Important</v-alert-title>
|
||||||
|
Keep your secret key confidential. You can get these keys from the Google reCAPTCHA admin console.
|
||||||
|
</v-alert>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn text @click="showRecaptchaConfig = false">Cancel</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:loading="savingRecaptcha"
|
||||||
|
:disabled="!recaptchaValid"
|
||||||
|
@click="saveRecaptchaConfig"
|
||||||
|
>
|
||||||
|
Save Configuration
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Membership Configuration Dialog -->
|
||||||
|
<v-dialog v-model="showMembershipConfig" max-width="600">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
<v-icon left>mdi-bank</v-icon>
|
||||||
|
Membership Configuration
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-alert type="info" variant="tonal" class="mb-4">
|
||||||
|
<v-alert-title>Payment Configuration</v-alert-title>
|
||||||
|
Configure membership fees and payment details displayed on the registration page.
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-form ref="membershipForm" v-model="membershipValid">
|
||||||
|
<v-text-field
|
||||||
|
v-model="membershipConfig.membershipFee"
|
||||||
|
label="Annual Membership Fee (€)"
|
||||||
|
type="number"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'Membership fee is required',
|
||||||
|
v => v > 0 || 'Fee must be greater than 0'
|
||||||
|
]"
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="membershipConfig.iban"
|
||||||
|
label="IBAN"
|
||||||
|
placeholder="DE89 3704 0044 0532 0130 00"
|
||||||
|
:rules="[v => !!v || 'IBAN is required']"
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="membershipConfig.accountHolder"
|
||||||
|
label="Account Holder Name"
|
||||||
|
placeholder="MonacoUSA Association"
|
||||||
|
:rules="[v => !!v || 'Account holder is required']"
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn text @click="showMembershipConfig = false">Cancel</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:loading="savingMembership"
|
||||||
|
:disabled="!membershipValid"
|
||||||
|
@click="saveMembershipConfig"
|
||||||
|
>
|
||||||
|
Save Configuration
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- View Member Dialog -->
|
||||||
|
<ViewMemberDialog
|
||||||
|
v-model="showViewDialog"
|
||||||
|
:member="selectedMember"
|
||||||
|
@edit="handleEditMember"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Edit Member Dialog -->
|
||||||
|
<EditMemberDialog
|
||||||
|
v-model="showEditDialog"
|
||||||
|
:member="selectedMember"
|
||||||
|
@member-updated="handleMemberUpdated"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Create User Dialog -->
|
||||||
|
<v-dialog v-model="showCreateUserDialog" max-width="600">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
<v-icon left>mdi-account-plus</v-icon>
|
||||||
|
Create User Account
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-alert type="info" variant="tonal" class="mb-4">
|
||||||
|
<v-alert-title>Create Portal Account</v-alert-title>
|
||||||
|
This will create a new user account in the MonacoUSA Portal with email verification.
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-form ref="createUserForm" v-model="createUserValid">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="newUser.firstName"
|
||||||
|
label="First Name"
|
||||||
|
:rules="[v => !!v || 'First name is required']"
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="newUser.lastName"
|
||||||
|
label="Last Name"
|
||||||
|
:rules="[v => !!v || 'Last name is required']"
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="newUser.email"
|
||||||
|
label="Email Address"
|
||||||
|
type="email"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'Email is required',
|
||||||
|
v => /.+@.+\..+/.test(v) || 'Email must be valid'
|
||||||
|
]"
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="newUser.role"
|
||||||
|
label="User Role"
|
||||||
|
:items="roleOptions"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn text @click="showCreateUserDialog = false">Cancel</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:loading="creatingUser"
|
||||||
|
:disabled="!createUserValid"
|
||||||
|
@click="createUserAccount"
|
||||||
|
>
|
||||||
|
Create Account
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'dashboard',
|
||||||
|
middleware: 'auth-admin'
|
||||||
|
});
|
||||||
|
|
||||||
|
const { firstName } = useAuth();
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const userCount = ref(0);
|
||||||
|
const loading = ref(false);
|
||||||
|
const showCreateUserDialog = ref(false);
|
||||||
|
const showAdminConfig = ref(false);
|
||||||
|
const showRecaptchaConfig = ref(false);
|
||||||
|
const showMembershipConfig = ref(false);
|
||||||
|
const showEmailConfig = ref(false);
|
||||||
|
|
||||||
|
// Dues management
|
||||||
|
const overdueCount = ref(0);
|
||||||
|
const overdueRefreshTrigger = ref(0);
|
||||||
|
const duesRefreshTrigger = ref(0);
|
||||||
|
|
||||||
|
// Data management
|
||||||
|
const assigningMemberIds = ref(false);
|
||||||
|
const backfillLoading = ref(false);
|
||||||
|
|
||||||
|
// Member dialog state
|
||||||
|
const showViewDialog = ref(false);
|
||||||
|
const showEditDialog = ref(false);
|
||||||
|
const selectedMember = ref(null);
|
||||||
|
|
||||||
|
// Create user dialog data
|
||||||
|
const createUserValid = ref(false);
|
||||||
|
const creatingUser = ref(false);
|
||||||
|
const newUser = ref({
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
role: 'user'
|
||||||
|
});
|
||||||
|
|
||||||
|
const roleOptions = [
|
||||||
|
{ title: 'User', value: 'user' },
|
||||||
|
{ title: 'Board Member', value: 'board' },
|
||||||
|
{ title: 'Administrator', value: 'admin' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// reCAPTCHA configuration data
|
||||||
|
const recaptchaValid = ref(false);
|
||||||
|
const savingRecaptcha = ref(false);
|
||||||
|
const recaptchaConfig = ref({
|
||||||
|
siteKey: '',
|
||||||
|
secretKey: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Membership configuration data
|
||||||
|
const membershipValid = ref(false);
|
||||||
|
const savingMembership = ref(false);
|
||||||
|
const membershipConfig = ref({
|
||||||
|
membershipFee: 50,
|
||||||
|
iban: '',
|
||||||
|
accountHolder: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentActivity = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'User Account Created',
|
||||||
|
description: 'New user account created for john.doe@monacousa.org',
|
||||||
|
time: '2 hours ago',
|
||||||
|
icon: 'mdi-account-plus',
|
||||||
|
color: 'success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Role Updated',
|
||||||
|
description: 'User role updated from User to Board Member',
|
||||||
|
time: '4 hours ago',
|
||||||
|
icon: 'mdi-shield-account',
|
||||||
|
color: 'warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'System Backup',
|
||||||
|
description: 'Automated system backup completed successfully',
|
||||||
|
time: '1 day ago',
|
||||||
|
icon: 'mdi-backup-restore',
|
||||||
|
color: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Password Reset',
|
||||||
|
description: 'Password reset requested for jane.smith@monacousa.org',
|
||||||
|
time: '2 days ago',
|
||||||
|
icon: 'mdi-key-change',
|
||||||
|
color: 'primary'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Load simplified admin stats (without system metrics)
|
||||||
|
const loadStats = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
// Simple user count without complex system metrics
|
||||||
|
const response = await $fetch<{ userCount: number }>('/api/admin/stats');
|
||||||
|
userCount.value = response.userCount || 0;
|
||||||
|
|
||||||
|
console.log('✅ Admin stats loaded:', { userCount: userCount.value });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to load admin stats:', error);
|
||||||
|
// Use fallback data
|
||||||
|
userCount.value = 25;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action methods (placeholders for now)
|
||||||
|
const manageUsers = () => {
|
||||||
|
window.open('https://auth.monacousa.org', '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewAuditLogs = () => {
|
||||||
|
console.log('Navigate to audit logs');
|
||||||
|
// TODO: Implement audit logs navigation
|
||||||
|
};
|
||||||
|
|
||||||
|
const showNocoDBSettings = ref(false);
|
||||||
|
|
||||||
|
const portalSettings = () => {
|
||||||
|
showNocoDBSettings.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettingsSaved = () => {
|
||||||
|
console.log('NocoDB settings saved successfully');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdminConfigSaved = () => {
|
||||||
|
console.log('Admin configuration saved successfully');
|
||||||
|
showAdminConfig.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle opening email configuration directly
|
||||||
|
const openEmailConfig = () => {
|
||||||
|
// Set the activeTab to email when opening the admin config dialog
|
||||||
|
showEmailConfig.value = true;
|
||||||
|
showAdminConfig.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for showEmailConfig to set the initial tab
|
||||||
|
watch(showEmailConfig, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
// This will be handled by the AdminConfigurationDialog to set initial tab
|
||||||
|
showEmailConfig.value = false; // Reset the flag
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveRecaptchaConfig = async () => {
|
||||||
|
if (!recaptchaValid.value) return;
|
||||||
|
|
||||||
|
savingRecaptcha.value = true;
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/admin/recaptcha-config', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
siteKey: recaptchaConfig.value.siteKey,
|
||||||
|
secretKey: recaptchaConfig.value.secretKey
|
||||||
|
}
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
if (response?.success) {
|
||||||
|
showRecaptchaConfig.value = false;
|
||||||
|
console.log('reCAPTCHA configuration saved successfully');
|
||||||
|
// TODO: Show success notification
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save reCAPTCHA configuration:', error);
|
||||||
|
// TODO: Show error notification
|
||||||
|
} finally {
|
||||||
|
savingRecaptcha.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveMembershipConfig = async () => {
|
||||||
|
if (!membershipValid.value) return;
|
||||||
|
|
||||||
|
savingMembership.value = true;
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/admin/registration-config', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
membershipFee: membershipConfig.value.membershipFee,
|
||||||
|
iban: membershipConfig.value.iban,
|
||||||
|
accountHolder: membershipConfig.value.accountHolder
|
||||||
|
}
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
if (response?.success) {
|
||||||
|
showMembershipConfig.value = false;
|
||||||
|
console.log('Membership configuration saved successfully');
|
||||||
|
// TODO: Show success notification
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save membership configuration:', error);
|
||||||
|
// TODO: Show error notification
|
||||||
|
} finally {
|
||||||
|
savingMembership.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUserAccount = async () => {
|
||||||
|
if (!createUserValid.value) return;
|
||||||
|
|
||||||
|
creatingUser.value = true;
|
||||||
|
try {
|
||||||
|
console.log('Creating user account:', newUser.value);
|
||||||
|
|
||||||
|
// TODO: Implement actual user creation using enhanced Keycloak API
|
||||||
|
// For now, just show success
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate API call
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
newUser.value = {
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
role: 'user'
|
||||||
|
};
|
||||||
|
|
||||||
|
showCreateUserDialog.value = false;
|
||||||
|
console.log('User account created successfully');
|
||||||
|
|
||||||
|
// TODO: Show success notification
|
||||||
|
// TODO: Refresh user list
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create user account:', error);
|
||||||
|
// TODO: Show error notification
|
||||||
|
} finally {
|
||||||
|
creatingUser.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUser = () => {
|
||||||
|
console.log('Create new user');
|
||||||
|
// TODO: Implement create user dialog/form
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateReport = () => {
|
||||||
|
console.log('Generate user report');
|
||||||
|
// TODO: Implement report generation
|
||||||
|
};
|
||||||
|
|
||||||
|
const manageRoles = () => {
|
||||||
|
console.log('Manage user roles');
|
||||||
|
// TODO: Implement role management
|
||||||
|
};
|
||||||
|
|
||||||
|
const systemMaintenance = () => {
|
||||||
|
console.log('System maintenance');
|
||||||
|
// TODO: Implement maintenance mode
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dues management handlers
|
||||||
|
const loadOverdueCount = async () => {
|
||||||
|
try {
|
||||||
|
const response = await $fetch<{ success: boolean; data: { count: number } }>('/api/members/overdue-count');
|
||||||
|
if (response.success) {
|
||||||
|
overdueCount.value = response.data.count;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error loading overdue count:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewOverdueMembers = () => {
|
||||||
|
// Navigate to member list with overdue filter applied
|
||||||
|
navigateTo('/dashboard/member-list');
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendDuesReminders = () => {
|
||||||
|
// Placeholder for dues reminder functionality
|
||||||
|
console.log('Send dues reminders - feature to be implemented');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusesUpdated = async (updatedCount: number) => {
|
||||||
|
console.log(`Successfully updated ${updatedCount} member${updatedCount !== 1 ? 's' : ''} to inactive status`);
|
||||||
|
|
||||||
|
// Refresh overdue count
|
||||||
|
await loadOverdueCount();
|
||||||
|
|
||||||
|
// Trigger banner refresh
|
||||||
|
overdueRefreshTrigger.value += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewMember = (member: any) => {
|
||||||
|
// Open the view dialog instead of navigating away
|
||||||
|
selectedMember.value = member;
|
||||||
|
showViewDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditMember = (member: any) => {
|
||||||
|
// Close the view dialog and open the edit dialog
|
||||||
|
showViewDialog.value = false;
|
||||||
|
selectedMember.value = member;
|
||||||
|
showEditDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToMembers = () => {
|
||||||
|
// Navigate to member list page
|
||||||
|
navigateTo('/dashboard/member-list');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMemberUpdated = (member: any) => {
|
||||||
|
console.log('Member updated:', member.FullName || `${member.first_name} ${member.last_name}`);
|
||||||
|
|
||||||
|
// Close edit dialog
|
||||||
|
showEditDialog.value = false;
|
||||||
|
|
||||||
|
// Trigger dues refresh
|
||||||
|
duesRefreshTrigger.value += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Data management functions
|
||||||
|
const assignMemberIds = async () => {
|
||||||
|
assigningMemberIds.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Starting member ID assignment...');
|
||||||
|
|
||||||
|
const response = await $fetch<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
totalMembers: number;
|
||||||
|
membersUpdated: number;
|
||||||
|
updatedMembers: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
memberId: string;
|
||||||
|
}>;
|
||||||
|
startingId: string | null;
|
||||||
|
endingId: string | null;
|
||||||
|
};
|
||||||
|
}>('/api/admin/assign-member-ids', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
console.log('✅ Member ID assignment completed:', {
|
||||||
|
totalMembers: response.data.totalMembers,
|
||||||
|
membersUpdated: response.data.membersUpdated,
|
||||||
|
startingId: response.data.startingId,
|
||||||
|
endingId: response.data.endingId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
alert(`Success! Assigned member IDs to ${response.data.membersUpdated} members.\nRange: ${response.data.startingId} to ${response.data.endingId}`);
|
||||||
|
|
||||||
|
// Refresh dues management data
|
||||||
|
duesRefreshTrigger.value += 1;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Failed to assign member IDs:', error);
|
||||||
|
alert(`Error: ${error.statusMessage || error.message || 'Failed to assign member IDs'}`);
|
||||||
|
} finally {
|
||||||
|
assigningMemberIds.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const backfillEventIds = async () => {
|
||||||
|
backfillLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Starting event ID backfill...');
|
||||||
|
|
||||||
|
const response = await $fetch<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
totalEvents: number;
|
||||||
|
eventsUpdated: number;
|
||||||
|
};
|
||||||
|
}>('/api/admin/backfill-event-ids', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
console.log('✅ Event ID backfill completed:', {
|
||||||
|
totalEvents: response.data.totalEvents,
|
||||||
|
eventsUpdated: response.data.eventsUpdated
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
alert(`Success! Assigned event IDs to ${response.data.eventsUpdated} events.`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Failed to backfill event IDs:', error);
|
||||||
|
alert(`Error: ${error.statusMessage || error.message || 'Failed to backfill event IDs'}`);
|
||||||
|
} finally {
|
||||||
|
backfillLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load stats and overdue count on component mount
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadStats();
|
||||||
|
await loadOverdueCount();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-card {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn {
|
||||||
|
text-transform: none !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,350 @@
|
||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<!-- Dues Payment Banner -->
|
||||||
|
<DuesPaymentBanner />
|
||||||
|
|
||||||
|
<!-- Welcome Header -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col>
|
||||||
|
<h1 class="text-h3 font-weight-bold" style="color: #a31515;">
|
||||||
|
Welcome Back, {{ firstName }}!
|
||||||
|
</h1>
|
||||||
|
<p class="text-h6 text-medium-emphasis">
|
||||||
|
MonacoUSA Board Portal
|
||||||
|
</p>
|
||||||
|
<div class="text-center">
|
||||||
|
<v-chip color="primary" variant="elevated" class="mt-2">
|
||||||
|
<v-icon start>mdi-shield-account</v-icon>
|
||||||
|
Board Member
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Board Tools -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card class="pa-4 text-center" elevation="2" hover>
|
||||||
|
<v-icon size="48" color="primary" class="mb-2">mdi-calendar</v-icon>
|
||||||
|
<h3 class="mb-2">Events</h3>
|
||||||
|
<p class="text-body-2 mb-4">View and manage association events</p>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="navigateToEvents"
|
||||||
|
>
|
||||||
|
View Events
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card class="pa-4 text-center" elevation="2" hover>
|
||||||
|
<v-icon size="48" color="primary" class="mb-2">mdi-account-group</v-icon>
|
||||||
|
<h3 class="mb-2">Members</h3>
|
||||||
|
<p class="text-body-2 mb-4">View and manage association members</p>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="navigateToMembers"
|
||||||
|
>
|
||||||
|
View Members
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Board Statistics -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<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="text-h4 font-weight-bold" style="color: #a31515;">{{ stats.totalMembers }}</div>
|
||||||
|
<div class="text-body-2">Total Members</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="3" class="text-center">
|
||||||
|
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ stats.activeMembers }}</div>
|
||||||
|
<div class="text-body-2">Active Members</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="6" class="text-center">
|
||||||
|
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ stats.upcomingEvents }}</div>
|
||||||
|
<div class="text-body-2">Upcoming Events</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<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>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 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"
|
||||||
|
/>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Member } from '~/utils/types';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'dashboard',
|
||||||
|
middleware: 'auth'
|
||||||
|
});
|
||||||
|
|
||||||
|
const { firstName, isBoard, isAdmin } = useAuth();
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
.v-card {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn {
|
||||||
|
text-transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-icon {
|
||||||
|
color: #a31515 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body-2 {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-chip {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,602 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-h4 font-weight-bold mb-2">Member Management</h1>
|
||||||
|
<p class="text-body-1 text-medium-emphasis">Manage and oversee all MonacoUSA members</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<v-btn
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
prepend-icon="mdi-download"
|
||||||
|
@click="exportMembers"
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
prepend-icon="mdi-account-plus"
|
||||||
|
@click="showAddMemberDialog = true"
|
||||||
|
>
|
||||||
|
Add Member
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics Cards -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12" sm="6" md="3">
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-caption text-medium-emphasis mb-1">Total Members</p>
|
||||||
|
<p class="text-h5 font-weight-bold">{{ stats.total }}</p>
|
||||||
|
</div>
|
||||||
|
<v-icon size="40" color="primary">mdi-account-group</v-icon>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="3">
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-caption text-medium-emphasis mb-1">Active Members</p>
|
||||||
|
<p class="text-h5 font-weight-bold text-success">{{ stats.active }}</p>
|
||||||
|
</div>
|
||||||
|
<v-icon size="40" color="success">mdi-check-circle</v-icon>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="3">
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-caption text-medium-emphasis mb-1">Pending Dues</p>
|
||||||
|
<p class="text-h5 font-weight-bold text-warning">{{ stats.pendingDues }}</p>
|
||||||
|
</div>
|
||||||
|
<v-icon size="40" color="warning">mdi-clock-alert</v-icon>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="3">
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-caption text-medium-emphasis mb-1">New This Month</p>
|
||||||
|
<p class="text-h5 font-weight-bold text-info">{{ stats.newThisMonth }}</p>
|
||||||
|
</div>
|
||||||
|
<v-icon size="40" color="info">mdi-account-plus</v-icon>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Filters and Search -->
|
||||||
|
<v-card class="mb-6" elevation="1">
|
||||||
|
<v-card-text>
|
||||||
|
<v-row align="center">
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchQuery"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
label="Search members"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="2">
|
||||||
|
<v-select
|
||||||
|
v-model="filterStatus"
|
||||||
|
:items="statusOptions"
|
||||||
|
label="Status"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="2">
|
||||||
|
<v-select
|
||||||
|
v-model="filterDues"
|
||||||
|
:items="duesOptions"
|
||||||
|
label="Dues Status"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="2">
|
||||||
|
<v-select
|
||||||
|
v-model="filterType"
|
||||||
|
:items="memberTypeOptions"
|
||||||
|
label="Member Type"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="2">
|
||||||
|
<v-btn
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
block
|
||||||
|
@click="resetFilters"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Members Table -->
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="filteredMembers"
|
||||||
|
:search="searchQuery"
|
||||||
|
:items-per-page="10"
|
||||||
|
class="elevation-0"
|
||||||
|
>
|
||||||
|
<!-- Member Name with Avatar -->
|
||||||
|
<template v-slot:item.name="{ item }">
|
||||||
|
<div class="d-flex align-center py-2">
|
||||||
|
<ProfileAvatar
|
||||||
|
:member-id="item.memberId"
|
||||||
|
:first-name="item.firstName"
|
||||||
|
:last-name="item.lastName"
|
||||||
|
size="small"
|
||||||
|
:show-badge="false"
|
||||||
|
class="mr-3"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div class="font-weight-medium">{{ item.firstName }} {{ item.lastName }}</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ item.memberId }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<template v-slot:item.email="{ item }">
|
||||||
|
<div class="text-body-2">{{ item.email }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<template v-slot:item.status="{ item }">
|
||||||
|
<v-chip
|
||||||
|
:color="item.status === 'Active' ? 'success' : 'grey'"
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ item.status }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Dues Status -->
|
||||||
|
<template v-slot:item.duesStatus="{ item }">
|
||||||
|
<v-chip
|
||||||
|
:color="getDuesColor(item.duesStatus)"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
{{ item.duesStatus }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Member Type -->
|
||||||
|
<template v-slot:item.memberType="{ item }">
|
||||||
|
<v-chip
|
||||||
|
size="small"
|
||||||
|
variant="flat"
|
||||||
|
:color="getMemberTypeColor(item.memberType)"
|
||||||
|
>
|
||||||
|
{{ item.memberType }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Join Date -->
|
||||||
|
<template v-slot:item.joinDate="{ item }">
|
||||||
|
<span class="text-body-2">{{ formatDate(item.joinDate) }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="viewMember(item)"
|
||||||
|
>
|
||||||
|
<v-icon size="small">mdi-eye</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="top">View Details</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="editMember(item)"
|
||||||
|
>
|
||||||
|
<v-icon size="small">mdi-pencil</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="top">Edit Member</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="sendEmail(item)"
|
||||||
|
>
|
||||||
|
<v-icon size="small">mdi-email</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="top">Send Email</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
|
<v-menu>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<v-icon size="small">mdi-dots-vertical</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item @click="sendDuesReminder(item)">
|
||||||
|
<v-list-item-title>Send Dues Reminder</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="viewPaymentHistory(item)">
|
||||||
|
<v-list-item-title>Payment History</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="toggleStatus(item)">
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ item.status === 'Active' ? 'Deactivate' : 'Activate' }}
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-divider />
|
||||||
|
<v-list-item @click="deleteMember(item)" class="text-error">
|
||||||
|
<v-list-item-title>Delete Member</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Add Member Dialog -->
|
||||||
|
<v-dialog v-model="showAddMemberDialog" max-width="600">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Add New Member</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form v-model="addMemberFormValid">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="newMember.firstName"
|
||||||
|
label="First Name"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="[v => !!v || 'Required']"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="newMember.lastName"
|
||||||
|
label="Last Name"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="[v => !!v || 'Required']"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="newMember.email"
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'Required',
|
||||||
|
v => /.+@.+/.test(v) || 'Invalid email'
|
||||||
|
]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="newMember.phone"
|
||||||
|
label="Phone"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="newMember.nationality"
|
||||||
|
label="Nationality"
|
||||||
|
:items="nationalities"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="newMember.memberType"
|
||||||
|
label="Member Type"
|
||||||
|
:items="['Regular', 'Premium', 'Honorary']"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="newMember.joinDate"
|
||||||
|
label="Join Date"
|
||||||
|
type="date"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="showAddMemberDialog = false">Cancel</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
:disabled="!addMemberFormValid"
|
||||||
|
@click="addMember"
|
||||||
|
>
|
||||||
|
Add Member
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'board',
|
||||||
|
middleware: 'board'
|
||||||
|
});
|
||||||
|
|
||||||
|
// State
|
||||||
|
const searchQuery = ref('');
|
||||||
|
const filterStatus = ref(null);
|
||||||
|
const filterDues = ref(null);
|
||||||
|
const filterType = ref(null);
|
||||||
|
const showAddMemberDialog = ref(false);
|
||||||
|
const addMemberFormValid = ref(true);
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
const stats = ref({
|
||||||
|
total: 156,
|
||||||
|
active: 142,
|
||||||
|
pendingDues: 23,
|
||||||
|
newThisMonth: 8
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter options
|
||||||
|
const statusOptions = ['Active', 'Inactive'];
|
||||||
|
const duesOptions = ['Paid', 'Pending', 'Overdue'];
|
||||||
|
const memberTypeOptions = ['Regular', 'Premium', 'Honorary', 'Board', 'Admin'];
|
||||||
|
const nationalities = ['United States', 'Monaco', 'France', 'Italy', 'United Kingdom', 'Germany', 'Other'];
|
||||||
|
|
||||||
|
// Table headers
|
||||||
|
const headers = [
|
||||||
|
{ title: 'Member', key: 'name', sortable: true },
|
||||||
|
{ title: 'Email', key: 'email', sortable: true },
|
||||||
|
{ title: 'Status', key: 'status', sortable: true },
|
||||||
|
{ title: 'Dues', key: 'duesStatus', sortable: true },
|
||||||
|
{ title: 'Type', key: 'memberType', sortable: true },
|
||||||
|
{ title: 'Joined', key: 'joinDate', sortable: true },
|
||||||
|
{ title: 'Actions', key: 'actions', sortable: false, align: 'center' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock members data
|
||||||
|
const members = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
memberId: 'MUSA-0001',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john.doe@example.com',
|
||||||
|
phone: '+1 234 567 8900',
|
||||||
|
status: 'Active',
|
||||||
|
duesStatus: 'Paid',
|
||||||
|
memberType: 'Premium',
|
||||||
|
joinDate: '2021-03-15',
|
||||||
|
nationality: 'United States'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
memberId: 'MUSA-0002',
|
||||||
|
firstName: 'Jane',
|
||||||
|
lastName: 'Smith',
|
||||||
|
email: 'jane.smith@example.com',
|
||||||
|
phone: '+1 234 567 8901',
|
||||||
|
status: 'Active',
|
||||||
|
duesStatus: 'Pending',
|
||||||
|
memberType: 'Regular',
|
||||||
|
joinDate: '2022-06-20',
|
||||||
|
nationality: 'United Kingdom'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
memberId: 'MUSA-0003',
|
||||||
|
firstName: 'Pierre',
|
||||||
|
lastName: 'Dupont',
|
||||||
|
email: 'pierre.dupont@example.com',
|
||||||
|
phone: '+33 6 12 34 56 78',
|
||||||
|
status: 'Active',
|
||||||
|
duesStatus: 'Paid',
|
||||||
|
memberType: 'Board',
|
||||||
|
joinDate: '2020-01-10',
|
||||||
|
nationality: 'France'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
memberId: 'MUSA-0004',
|
||||||
|
firstName: 'Maria',
|
||||||
|
lastName: 'Rossi',
|
||||||
|
email: 'maria.rossi@example.com',
|
||||||
|
phone: '+39 06 123 4567',
|
||||||
|
status: 'Inactive',
|
||||||
|
duesStatus: 'Overdue',
|
||||||
|
memberType: 'Regular',
|
||||||
|
joinDate: '2021-09-05',
|
||||||
|
nationality: 'Italy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
memberId: 'MUSA-0005',
|
||||||
|
firstName: 'Hans',
|
||||||
|
lastName: 'Mueller',
|
||||||
|
email: 'hans.mueller@example.com',
|
||||||
|
phone: '+49 30 12345678',
|
||||||
|
status: 'Active',
|
||||||
|
duesStatus: 'Paid',
|
||||||
|
memberType: 'Premium',
|
||||||
|
joinDate: '2022-02-28',
|
||||||
|
nationality: 'Germany'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// New member form
|
||||||
|
const newMember = ref({
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
nationality: '',
|
||||||
|
memberType: 'Regular',
|
||||||
|
joinDate: new Date().toISOString().split('T')[0]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const filteredMembers = computed(() => {
|
||||||
|
let filtered = members.value;
|
||||||
|
|
||||||
|
if (filterStatus.value) {
|
||||||
|
filtered = filtered.filter(m => m.status === filterStatus.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterDues.value) {
|
||||||
|
filtered = filtered.filter(m => m.duesStatus === filterDues.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterType.value) {
|
||||||
|
filtered = filtered.filter(m => m.memberType === filterType.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDuesColor = (status: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
'Paid': 'success',
|
||||||
|
'Pending': 'warning',
|
||||||
|
'Overdue': 'error'
|
||||||
|
};
|
||||||
|
return colors[status] || 'grey';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMemberTypeColor = (type: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
'Regular': 'info',
|
||||||
|
'Premium': 'purple',
|
||||||
|
'Honorary': 'orange',
|
||||||
|
'Board': 'error',
|
||||||
|
'Admin': 'pink'
|
||||||
|
};
|
||||||
|
return colors[type] || 'grey';
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
searchQuery.value = '';
|
||||||
|
filterStatus.value = null;
|
||||||
|
filterDues.value = null;
|
||||||
|
filterType.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportMembers = () => {
|
||||||
|
console.log('Exporting members');
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewMember = (member: any) => {
|
||||||
|
console.log('Viewing member:', member);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editMember = (member: any) => {
|
||||||
|
console.log('Editing member:', member);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendEmail = (member: any) => {
|
||||||
|
console.log('Sending email to:', member.email);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendDuesReminder = (member: any) => {
|
||||||
|
console.log('Sending dues reminder to:', member.email);
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewPaymentHistory = (member: any) => {
|
||||||
|
console.log('Viewing payment history for:', member);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleStatus = (member: any) => {
|
||||||
|
member.status = member.status === 'Active' ? 'Inactive' : 'Active';
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMember = (member: any) => {
|
||||||
|
console.log('Deleting member:', member);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMember = () => {
|
||||||
|
console.log('Adding new member:', newMember.value);
|
||||||
|
showAddMemberDialog.value = false;
|
||||||
|
// Reset form
|
||||||
|
newMember.value = {
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
nationality: '',
|
||||||
|
memberType: 'Regular',
|
||||||
|
joinDate: new Date().toISOString().split('T')[0]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gap-1 {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -27,13 +27,23 @@ const loading = ref(true);
|
||||||
|
|
||||||
// Route to tier-specific dashboard - auth middleware ensures user is authenticated
|
// Route to tier-specific dashboard - auth middleware ensures user is authenticated
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('🔄 Dashboard mounted, routing to tier-specific page...');
|
console.log('🔄 Dashboard mounted, routing to role-specific section...');
|
||||||
|
|
||||||
// Auth middleware has already verified authentication - just route to tier page
|
// Auth middleware has already verified authentication - route based on highest privilege
|
||||||
if (user.value && userTier.value) {
|
if (user.value && userTier.value) {
|
||||||
const tierRoute = `/dashboard/${userTier.value}`;
|
let targetRoute = '/member/dashboard';
|
||||||
console.log('🔄 Routing to tier-specific dashboard:', tierRoute);
|
|
||||||
navigateTo(tierRoute, { replace: true });
|
// Route to the highest privilege level section
|
||||||
|
if (userTier.value === 'admin') {
|
||||||
|
targetRoute = '/admin/dashboard';
|
||||||
|
} else if (userTier.value === 'board') {
|
||||||
|
targetRoute = '/board/dashboard';
|
||||||
|
} else {
|
||||||
|
targetRoute = '/member/dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 Routing to role-specific dashboard:', targetRoute);
|
||||||
|
navigateTo(targetRoute, { replace: true });
|
||||||
} else {
|
} else {
|
||||||
console.warn('❌ No user or tier found - this should not happen after auth middleware');
|
console.warn('❌ No user or tier found - this should not happen after auth middleware');
|
||||||
// Fallback - middleware should have caught this
|
// Fallback - middleware should have caught this
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,473 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex justify-space-between align-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-h4 font-weight-bold">Member Dashboard</h1>
|
||||||
|
<p class="text-body-1 text-medium-emphasis">Welcome back, {{ firstName }}</p>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
prepend-icon="mdi-account-edit"
|
||||||
|
@click="navigateTo('/member/profile')"
|
||||||
|
>
|
||||||
|
Edit Profile
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profile Info Card -->
|
||||||
|
<v-card class="mb-6" elevation="2">
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon color="error" class="mr-2">mdi-account</v-icon>
|
||||||
|
Profile Information
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="auto">
|
||||||
|
<ProfileAvatar
|
||||||
|
:member-id="memberData?.member_id"
|
||||||
|
:first-name="memberData?.first_name"
|
||||||
|
:last-name="memberData?.last_name"
|
||||||
|
size="large"
|
||||||
|
:show-badge="false"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col>
|
||||||
|
<div class="d-flex justify-space-between align-start mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-h6 font-weight-bold">{{ fullName }}</h3>
|
||||||
|
<p class="text-body-2 text-medium-emphasis">{{ email }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<v-chip
|
||||||
|
color="error"
|
||||||
|
variant="tonal"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ membershipType }}
|
||||||
|
</v-chip>
|
||||||
|
<v-chip
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
prepend-icon="mdi-star"
|
||||||
|
>
|
||||||
|
{{ memberLevel }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row class="mt-4">
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<div class="text-caption text-medium-emphasis">Member Since</div>
|
||||||
|
<div class="font-weight-medium">{{ memberSince }}</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<div class="text-caption text-medium-emphasis">Points</div>
|
||||||
|
<div class="font-weight-medium">{{ memberPoints.toLocaleString() }}</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<div class="text-caption text-medium-emphasis">Status</div>
|
||||||
|
<div class="font-weight-medium text-success">Active</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<!-- Upcoming Events -->
|
||||||
|
<v-col cols="12" lg="6">
|
||||||
|
<v-card elevation="2" class="h-100">
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon color="error" class="mr-2">mdi-calendar</v-icon>
|
||||||
|
Upcoming Events
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-subtitle>Your registered events and activities</v-card-subtitle>
|
||||||
|
<v-card-text>
|
||||||
|
<v-list lines="three" class="pa-0">
|
||||||
|
<template v-for="(event, index) in upcomingEvents" :key="event.id">
|
||||||
|
<v-list-item class="px-0">
|
||||||
|
<v-card variant="outlined" class="w-100">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-start mb-2">
|
||||||
|
<h4 class="text-body-1 font-weight-medium">{{ event.title }}</h4>
|
||||||
|
<v-chip
|
||||||
|
:color="event.status === 'confirmed' ? 'success' : 'warning'"
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
<v-icon start size="x-small">
|
||||||
|
{{ event.status === 'confirmed' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
||||||
|
</v-icon>
|
||||||
|
{{ event.status }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-3 text-caption text-medium-emphasis">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon size="x-small" class="mr-1">mdi-calendar</v-icon>
|
||||||
|
{{ formatDate(event.date) }}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon size="x-small" class="mr-1">mdi-clock-outline</v-icon>
|
||||||
|
{{ event.time }}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon size="x-small" class="mr-1">mdi-map-marker</v-icon>
|
||||||
|
{{ event.location }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
block
|
||||||
|
class="mt-4"
|
||||||
|
@click="navigateTo('/member/events')"
|
||||||
|
>
|
||||||
|
View All Events
|
||||||
|
</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- Payment Status -->
|
||||||
|
<v-col cols="12" lg="6">
|
||||||
|
<v-card elevation="2" class="h-100">
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon color="error" class="mr-2">mdi-credit-card</v-icon>
|
||||||
|
Payment Status
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-subtitle>Membership and payment information</v-card-subtitle>
|
||||||
|
<v-card-text>
|
||||||
|
<!-- Current Membership -->
|
||||||
|
<v-card variant="outlined" class="mb-4">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center mb-3">
|
||||||
|
<h4 class="text-body-1 font-weight-medium">Current Membership</h4>
|
||||||
|
<v-chip
|
||||||
|
color="success"
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
<v-icon start size="x-small">mdi-check-circle</v-icon>
|
||||||
|
Active
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
<div class="text-body-2">
|
||||||
|
<div class="d-flex justify-space-between py-1">
|
||||||
|
<span class="text-medium-emphasis">Plan:</span>
|
||||||
|
<span class="font-weight-medium">{{ membershipType }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-space-between py-1">
|
||||||
|
<span class="text-medium-emphasis">Next Payment:</span>
|
||||||
|
<span class="font-weight-medium">{{ nextPaymentDate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-space-between py-1">
|
||||||
|
<span class="text-medium-emphasis">Amount:</span>
|
||||||
|
<span class="font-weight-medium">${{ membershipAmount }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Payment History -->
|
||||||
|
<v-card variant="outlined" class="mb-4">
|
||||||
|
<v-card-text>
|
||||||
|
<h4 class="text-body-1 font-weight-medium mb-3">Payment History</h4>
|
||||||
|
<div class="text-body-2">
|
||||||
|
<div
|
||||||
|
v-for="payment in paymentHistory"
|
||||||
|
:key="payment.id"
|
||||||
|
class="d-flex justify-space-between align-center py-1"
|
||||||
|
>
|
||||||
|
<span class="text-medium-emphasis">{{ payment.date }}</span>
|
||||||
|
<div class="d-flex align-center gap-2">
|
||||||
|
<span class="font-weight-medium">${{ payment.amount }}</span>
|
||||||
|
<v-chip
|
||||||
|
color="success"
|
||||||
|
size="x-small"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
Paid
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
block
|
||||||
|
prepend-icon="mdi-credit-card"
|
||||||
|
@click="navigateTo('/member/payments')"
|
||||||
|
>
|
||||||
|
Update Payment Method
|
||||||
|
</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Recent Activity -->
|
||||||
|
<v-card elevation="2" class="mt-6">
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon color="error" class="mr-2">mdi-history</v-icon>
|
||||||
|
Recent Activity
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-subtitle>Your latest actions and updates</v-card-subtitle>
|
||||||
|
<v-card-text>
|
||||||
|
<v-timeline side="end" density="compact">
|
||||||
|
<v-timeline-item
|
||||||
|
v-for="activity in recentActivity"
|
||||||
|
:key="activity.id"
|
||||||
|
:dot-color="activity.color"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<template v-slot:icon>
|
||||||
|
<v-icon size="x-small">{{ activity.icon }}</v-icon>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<div class="text-body-2 font-weight-medium">{{ activity.description }}</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ activity.timestamp }}</div>
|
||||||
|
</div>
|
||||||
|
</v-timeline-item>
|
||||||
|
</v-timeline>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
block
|
||||||
|
class="mt-4"
|
||||||
|
@click="navigateTo('/member/activity')"
|
||||||
|
>
|
||||||
|
View All Activity
|
||||||
|
</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<v-row class="mt-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<h3 class="text-h6 mb-3">Quick Actions</h3>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="3">
|
||||||
|
<v-card
|
||||||
|
elevation="1"
|
||||||
|
class="text-center pa-4 cursor-pointer"
|
||||||
|
hover
|
||||||
|
@click="navigateTo('/member/events')"
|
||||||
|
>
|
||||||
|
<v-icon size="32" color="error" class="mb-2">mdi-calendar-plus</v-icon>
|
||||||
|
<div class="text-body-2">Register for Event</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="3">
|
||||||
|
<v-card
|
||||||
|
elevation="1"
|
||||||
|
class="text-center pa-4 cursor-pointer"
|
||||||
|
hover
|
||||||
|
@click="navigateTo('/member/directory')"
|
||||||
|
>
|
||||||
|
<v-icon size="32" color="error" class="mb-2">mdi-account-group</v-icon>
|
||||||
|
<div class="text-body-2">Member Directory</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="3">
|
||||||
|
<v-card
|
||||||
|
elevation="1"
|
||||||
|
class="text-center pa-4 cursor-pointer"
|
||||||
|
hover
|
||||||
|
@click="navigateTo('/member/resources')"
|
||||||
|
>
|
||||||
|
<v-icon size="32" color="error" class="mb-2">mdi-book-open-variant</v-icon>
|
||||||
|
<div class="text-body-2">Resources</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="3">
|
||||||
|
<v-card
|
||||||
|
elevation="1"
|
||||||
|
class="text-center pa-4 cursor-pointer"
|
||||||
|
hover
|
||||||
|
@click="navigateTo('/member/support')"
|
||||||
|
>
|
||||||
|
<v-icon size="32" color="error" class="mb-2">mdi-help-circle</v-icon>
|
||||||
|
<div class="text-body-2">Get Support</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Member } from '~/utils/types';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'member',
|
||||||
|
middleware: 'member'
|
||||||
|
});
|
||||||
|
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
const firstName = computed(() => memberData.value?.first_name || user.value?.firstName || 'Member');
|
||||||
|
const fullName = computed(() => {
|
||||||
|
if (memberData.value) {
|
||||||
|
return `${memberData.value.first_name} ${memberData.value.last_name}`;
|
||||||
|
}
|
||||||
|
return user.value?.name || 'Member';
|
||||||
|
});
|
||||||
|
const email = computed(() => memberData.value?.email || user.value?.email || '');
|
||||||
|
const membershipType = computed(() => 'Premium');
|
||||||
|
const memberLevel = computed(() => 'Gold');
|
||||||
|
const memberSince = computed(() => {
|
||||||
|
if (memberData.value?.join_date) {
|
||||||
|
return new Date(memberData.value.join_date).toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||||
|
}
|
||||||
|
return 'January 2023';
|
||||||
|
});
|
||||||
|
const memberPoints = ref(2450);
|
||||||
|
const nextPaymentDate = ref('Feb 15, 2024');
|
||||||
|
const membershipAmount = ref('99.00');
|
||||||
|
|
||||||
|
// Mock data - replace with actual API calls
|
||||||
|
const upcomingEvents = ref([
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
title: "Monthly Networking Event",
|
||||||
|
date: "2024-01-15",
|
||||||
|
time: "6:00 PM",
|
||||||
|
location: "Conference Center",
|
||||||
|
status: "confirmed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
title: "Workshop: Digital Marketing",
|
||||||
|
date: "2024-01-22",
|
||||||
|
time: "2:00 PM",
|
||||||
|
location: "Training Room A",
|
||||||
|
status: "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
title: "Annual Gala Dinner",
|
||||||
|
date: "2024-02-05",
|
||||||
|
time: "7:00 PM",
|
||||||
|
location: "Grand Ballroom",
|
||||||
|
status: "confirmed"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const paymentHistory = ref([
|
||||||
|
{ id: 1, date: 'Jan 15, 2024', amount: '99.00' },
|
||||||
|
{ id: 2, date: 'Dec 15, 2023', amount: '99.00' },
|
||||||
|
{ id: 3, date: 'Nov 15, 2023', amount: '99.00' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const recentActivity = ref([
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
type: "event",
|
||||||
|
description: "Attended Leadership Summit",
|
||||||
|
timestamp: "2 days ago",
|
||||||
|
icon: "mdi-account-group",
|
||||||
|
color: "error"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
type: "payment",
|
||||||
|
description: "Membership renewal completed",
|
||||||
|
timestamp: "1 week ago",
|
||||||
|
icon: "mdi-credit-card",
|
||||||
|
color: "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
type: "achievement",
|
||||||
|
description: "Earned Gold Level status",
|
||||||
|
timestamp: "2 weeks ago",
|
||||||
|
icon: "mdi-trophy",
|
||||||
|
color: "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
type: "profile",
|
||||||
|
description: "Updated profile information",
|
||||||
|
timestamp: "3 weeks ago",
|
||||||
|
icon: "mdi-account",
|
||||||
|
color: "info"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load real data on mount
|
||||||
|
onMounted(async () => {
|
||||||
|
// Load upcoming events
|
||||||
|
try {
|
||||||
|
const eventsRes = await $fetch('/api/member/events/upcoming');
|
||||||
|
if (eventsRes?.success && eventsRes?.data) {
|
||||||
|
// Map real events data
|
||||||
|
console.log('Loaded upcoming events:', eventsRes.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading events:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load payment information
|
||||||
|
try {
|
||||||
|
const paymentsRes = await $fetch('/api/member/payments/status');
|
||||||
|
if (paymentsRes?.success && paymentsRes?.data) {
|
||||||
|
// Update payment data
|
||||||
|
console.log('Loaded payment status:', paymentsRes.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading payment status:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-3 {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-100 {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-100 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,552 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-h4 font-weight-bold mb-2">Events</h1>
|
||||||
|
<p class="text-body-1 text-medium-emphasis">Discover and register for upcoming MonacoUSA events</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<v-card class="mb-6" elevation="1">
|
||||||
|
<v-card-text>
|
||||||
|
<v-row align="center">
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchQuery"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
label="Search events"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedCategory"
|
||||||
|
:items="categories"
|
||||||
|
label="Category"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedMonth"
|
||||||
|
:items="months"
|
||||||
|
label="Month"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="2">
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
block
|
||||||
|
@click="resetFilters"
|
||||||
|
>
|
||||||
|
Reset Filters
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Event Tabs -->
|
||||||
|
<v-tabs
|
||||||
|
v-model="tab"
|
||||||
|
color="error"
|
||||||
|
class="mb-6"
|
||||||
|
>
|
||||||
|
<v-tab value="upcoming">
|
||||||
|
<v-icon start>mdi-calendar-clock</v-icon>
|
||||||
|
Upcoming Events
|
||||||
|
</v-tab>
|
||||||
|
<v-tab value="registered">
|
||||||
|
<v-icon start>mdi-calendar-check</v-icon>
|
||||||
|
My Registrations
|
||||||
|
</v-tab>
|
||||||
|
<v-tab value="past">
|
||||||
|
<v-icon start>mdi-history</v-icon>
|
||||||
|
Past Events
|
||||||
|
</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
|
||||||
|
<!-- Tab Content -->
|
||||||
|
<v-window v-model="tab">
|
||||||
|
<!-- Upcoming Events Tab -->
|
||||||
|
<v-window-item value="upcoming">
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
v-for="event in upcomingEvents"
|
||||||
|
:key="event.id"
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
lg="4"
|
||||||
|
>
|
||||||
|
<v-card elevation="2" hover class="h-100 d-flex flex-column">
|
||||||
|
<!-- Event Image -->
|
||||||
|
<v-img
|
||||||
|
:src="event.image"
|
||||||
|
height="200"
|
||||||
|
cover
|
||||||
|
gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)"
|
||||||
|
>
|
||||||
|
<v-card-title class="text-white">
|
||||||
|
{{ event.title }}
|
||||||
|
</v-card-title>
|
||||||
|
</v-img>
|
||||||
|
|
||||||
|
<v-card-text class="flex-grow-1">
|
||||||
|
<!-- Event Details -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<v-chip
|
||||||
|
:color="getCategoryColor(event.category)"
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
{{ event.category }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-body-2 mb-3">{{ event.description }}</p>
|
||||||
|
|
||||||
|
<div class="text-caption text-medium-emphasis">
|
||||||
|
<div class="d-flex align-center mb-1">
|
||||||
|
<v-icon size="x-small" class="mr-2">mdi-calendar</v-icon>
|
||||||
|
{{ formatDate(event.date) }}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center mb-1">
|
||||||
|
<v-icon size="x-small" class="mr-2">mdi-clock-outline</v-icon>
|
||||||
|
{{ event.time }}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center mb-1">
|
||||||
|
<v-icon size="x-small" class="mr-2">mdi-map-marker</v-icon>
|
||||||
|
{{ event.location }}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon size="x-small" class="mr-2">mdi-account-group</v-icon>
|
||||||
|
{{ event.attendees }} / {{ event.capacity }} attendees
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="viewEventDetails(event)"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="flat"
|
||||||
|
color="error"
|
||||||
|
:disabled="event.attendees >= event.capacity"
|
||||||
|
@click="registerForEvent(event)"
|
||||||
|
>
|
||||||
|
{{ event.attendees >= event.capacity ? 'Full' : 'Register' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<v-card v-if="upcomingEvents.length === 0" class="text-center pa-8" elevation="0">
|
||||||
|
<v-icon size="64" color="grey-lighten-1">mdi-calendar-blank</v-icon>
|
||||||
|
<h3 class="text-h6 mt-4">No upcoming events</h3>
|
||||||
|
<p class="text-body-2 text-medium-emphasis">Check back later for new events</p>
|
||||||
|
</v-card>
|
||||||
|
</v-window-item>
|
||||||
|
|
||||||
|
<!-- Registered Events Tab -->
|
||||||
|
<v-window-item value="registered">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-list lines="three">
|
||||||
|
<v-list-item
|
||||||
|
v-for="registration in registeredEvents"
|
||||||
|
:key="registration.id"
|
||||||
|
class="py-3"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-avatar size="60" rounded="lg">
|
||||||
|
<v-img :src="registration.image" cover />
|
||||||
|
</v-avatar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item-title class="font-weight-medium">
|
||||||
|
{{ registration.title }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<div class="d-flex gap-3 mt-1">
|
||||||
|
<span><v-icon size="x-small">mdi-calendar</v-icon> {{ formatDate(registration.date) }}</span>
|
||||||
|
<span><v-icon size="x-small">mdi-clock-outline</v-icon> {{ registration.time }}</span>
|
||||||
|
<span><v-icon size="x-small">mdi-map-marker</v-icon> {{ registration.location }}</span>
|
||||||
|
</div>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
|
||||||
|
<template v-slot:append>
|
||||||
|
<div class="text-right">
|
||||||
|
<v-chip
|
||||||
|
color="success"
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<v-icon start size="x-small">mdi-check</v-icon>
|
||||||
|
Registered
|
||||||
|
</v-chip>
|
||||||
|
<div>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
@click="cancelRegistration(registration)"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-if="registeredEvents.length === 0" class="text-center pa-8">
|
||||||
|
<v-icon size="64" color="grey-lighten-1">mdi-calendar-remove</v-icon>
|
||||||
|
<h3 class="text-h6 mt-4">No registered events</h3>
|
||||||
|
<p class="text-body-2 text-medium-emphasis">Browse upcoming events to find something interesting</p>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-window-item>
|
||||||
|
|
||||||
|
<!-- Past Events Tab -->
|
||||||
|
<v-window-item value="past">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-timeline side="end" density="compact">
|
||||||
|
<v-timeline-item
|
||||||
|
v-for="event in pastEvents"
|
||||||
|
:key="event.id"
|
||||||
|
dot-color="grey"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<template v-slot:opposite>
|
||||||
|
<div class="text-caption text-medium-emphasis">
|
||||||
|
{{ formatDate(event.date) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-card-title class="text-h6">{{ event.title }}</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p class="text-body-2 mb-2">{{ event.description }}</p>
|
||||||
|
<div class="text-caption text-medium-emphasis">
|
||||||
|
<v-icon size="x-small">mdi-account-group</v-icon>
|
||||||
|
{{ event.attendees }} attendees
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
@click="viewEventPhotos(event)"
|
||||||
|
>
|
||||||
|
View Photos
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="viewEventDetails(event)"
|
||||||
|
>
|
||||||
|
View Details
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-timeline-item>
|
||||||
|
</v-timeline>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<v-card v-if="pastEvents.length === 0" class="text-center pa-8" elevation="0">
|
||||||
|
<v-icon size="64" color="grey-lighten-1">mdi-history</v-icon>
|
||||||
|
<h3 class="text-h6 mt-4">No past events</h3>
|
||||||
|
<p class="text-body-2 text-medium-emphasis">Past events will appear here</p>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-window-item>
|
||||||
|
</v-window>
|
||||||
|
|
||||||
|
<!-- Event Details Dialog -->
|
||||||
|
<v-dialog v-model="detailsDialog" max-width="600">
|
||||||
|
<v-card v-if="selectedEvent">
|
||||||
|
<v-img
|
||||||
|
:src="selectedEvent.image"
|
||||||
|
height="200"
|
||||||
|
cover
|
||||||
|
/>
|
||||||
|
<v-card-title>{{ selectedEvent.title }}</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-chip
|
||||||
|
:color="getCategoryColor(selectedEvent.category)"
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-3"
|
||||||
|
>
|
||||||
|
{{ selectedEvent.category }}
|
||||||
|
</v-chip>
|
||||||
|
|
||||||
|
<p class="mb-4">{{ selectedEvent.fullDescription || selectedEvent.description }}</p>
|
||||||
|
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-calendar</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ formatDate(selectedEvent.date) }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-clock-outline</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ selectedEvent.time }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-map-marker</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ selectedEvent.location }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-account-group</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ selectedEvent.attendees }} / {{ selectedEvent.capacity }} attendees</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="detailsDialog = false">Close</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
:disabled="selectedEvent.attendees >= selectedEvent.capacity"
|
||||||
|
@click="registerForEvent(selectedEvent)"
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'member',
|
||||||
|
middleware: 'member'
|
||||||
|
});
|
||||||
|
|
||||||
|
// State
|
||||||
|
const tab = ref('upcoming');
|
||||||
|
const searchQuery = ref('');
|
||||||
|
const selectedCategory = ref(null);
|
||||||
|
const selectedMonth = ref(null);
|
||||||
|
const detailsDialog = ref(false);
|
||||||
|
const selectedEvent = ref(null);
|
||||||
|
|
||||||
|
// Filter options
|
||||||
|
const categories = ref([
|
||||||
|
'Networking',
|
||||||
|
'Workshop',
|
||||||
|
'Social',
|
||||||
|
'Cultural',
|
||||||
|
'Business',
|
||||||
|
'Charity'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const months = ref([
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock event data
|
||||||
|
const upcomingEvents = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Monaco Business Networking',
|
||||||
|
description: 'Connect with fellow Monaco entrepreneurs and business leaders',
|
||||||
|
fullDescription: 'Join us for an evening of networking with Monaco\'s business community. This event brings together entrepreneurs, executives, and professionals from various industries.',
|
||||||
|
category: 'Networking',
|
||||||
|
date: '2024-02-15',
|
||||||
|
time: '6:00 PM - 8:00 PM',
|
||||||
|
location: 'Monaco Yacht Club',
|
||||||
|
image: 'https://picsum.photos/400/300?random=1',
|
||||||
|
attendees: 45,
|
||||||
|
capacity: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Digital Marketing Workshop',
|
||||||
|
description: 'Learn the latest digital marketing strategies and techniques',
|
||||||
|
category: 'Workshop',
|
||||||
|
date: '2024-02-22',
|
||||||
|
time: '2:00 PM - 5:00 PM',
|
||||||
|
location: 'Conference Center',
|
||||||
|
image: 'https://picsum.photos/400/300?random=2',
|
||||||
|
attendees: 28,
|
||||||
|
capacity: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Annual Gala Dinner',
|
||||||
|
description: 'Celebrate the year with an elegant evening of dining and entertainment',
|
||||||
|
category: 'Social',
|
||||||
|
date: '2024-03-05',
|
||||||
|
time: '7:00 PM - 11:00 PM',
|
||||||
|
location: 'Hotel Hermitage',
|
||||||
|
image: 'https://picsum.photos/400/300?random=3',
|
||||||
|
attendees: 120,
|
||||||
|
capacity: 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Monaco Grand Prix Viewing',
|
||||||
|
description: 'Watch the Monaco Grand Prix from our exclusive viewing area',
|
||||||
|
category: 'Social',
|
||||||
|
date: '2024-05-26',
|
||||||
|
time: '12:00 PM - 6:00 PM',
|
||||||
|
location: 'Private Terrace',
|
||||||
|
image: 'https://picsum.photos/400/300?random=4',
|
||||||
|
attendees: 75,
|
||||||
|
capacity: 75
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const registeredEvents = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Monaco Business Networking',
|
||||||
|
date: '2024-02-15',
|
||||||
|
time: '6:00 PM',
|
||||||
|
location: 'Monaco Yacht Club',
|
||||||
|
image: 'https://picsum.photos/400/300?random=1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Annual Gala Dinner',
|
||||||
|
date: '2024-03-05',
|
||||||
|
time: '7:00 PM',
|
||||||
|
location: 'Hotel Hermitage',
|
||||||
|
image: 'https://picsum.photos/400/300?random=3'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pastEvents = ref([
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'New Year Celebration',
|
||||||
|
description: 'Welcomed 2024 with a spectacular celebration',
|
||||||
|
date: '2024-01-01',
|
||||||
|
attendees: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: 'Investment Seminar',
|
||||||
|
description: 'Expert insights on investment strategies for 2024',
|
||||||
|
date: '2024-01-15',
|
||||||
|
attendees: 65
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoryColor = (category: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
'Networking': 'blue',
|
||||||
|
'Workshop': 'purple',
|
||||||
|
'Social': 'green',
|
||||||
|
'Cultural': 'orange',
|
||||||
|
'Business': 'indigo',
|
||||||
|
'Charity': 'pink'
|
||||||
|
};
|
||||||
|
return colors[category] || 'grey';
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
searchQuery.value = '';
|
||||||
|
selectedCategory.value = null;
|
||||||
|
selectedMonth.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewEventDetails = (event: any) => {
|
||||||
|
selectedEvent.value = event;
|
||||||
|
detailsDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerForEvent = (event: any) => {
|
||||||
|
console.log('Registering for event:', event.title);
|
||||||
|
// Add to registered events
|
||||||
|
if (!registeredEvents.value.find(e => e.id === event.id)) {
|
||||||
|
registeredEvents.value.push({
|
||||||
|
id: event.id,
|
||||||
|
title: event.title,
|
||||||
|
date: event.date,
|
||||||
|
time: event.time,
|
||||||
|
location: event.location,
|
||||||
|
image: event.image
|
||||||
|
});
|
||||||
|
}
|
||||||
|
detailsDialog.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelRegistration = (event: any) => {
|
||||||
|
console.log('Canceling registration for:', event.title);
|
||||||
|
const index = registeredEvents.value.findIndex(e => e.id === event.id);
|
||||||
|
if (index > -1) {
|
||||||
|
registeredEvents.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewEventPhotos = (event: any) => {
|
||||||
|
console.log('Viewing photos for:', event.title);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gap-3 {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,640 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-h4 font-weight-bold mb-2">My Profile</h1>
|
||||||
|
<p class="text-body-1 text-medium-emphasis">Manage your personal information and preferences</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profile Completion Alert -->
|
||||||
|
<v-alert
|
||||||
|
v-if="profileCompletion < 100"
|
||||||
|
type="info"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-6"
|
||||||
|
closable
|
||||||
|
>
|
||||||
|
<v-alert-title>Complete Your Profile</v-alert-title>
|
||||||
|
Your profile is {{ profileCompletion }}% complete. Add more information to help other members connect with you.
|
||||||
|
<v-progress-linear
|
||||||
|
:model-value="profileCompletion"
|
||||||
|
color="info"
|
||||||
|
class="mt-2"
|
||||||
|
height="6"
|
||||||
|
rounded
|
||||||
|
/>
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<!-- Left Column - Profile Card -->
|
||||||
|
<v-col cols="12" lg="4">
|
||||||
|
<v-card elevation="2" class="mb-6">
|
||||||
|
<v-card-text class="text-center pa-6">
|
||||||
|
<!-- Avatar -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<ProfileAvatar
|
||||||
|
:member-id="profile.memberId"
|
||||||
|
:first-name="profile.firstName"
|
||||||
|
:last-name="profile.lastName"
|
||||||
|
size="x-large"
|
||||||
|
:show-badge="false"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
class="mt-2"
|
||||||
|
@click="changeAvatar"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-camera</v-icon>
|
||||||
|
Change Photo
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<h2 class="text-h5 font-weight-bold mb-1">{{ profile.firstName }} {{ profile.lastName }}</h2>
|
||||||
|
<p class="text-body-2 text-medium-emphasis mb-3">{{ profile.title }}</p>
|
||||||
|
|
||||||
|
<!-- Member Badge -->
|
||||||
|
<v-chip
|
||||||
|
color="error"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-shield-star</v-icon>
|
||||||
|
{{ profile.memberType }} Member
|
||||||
|
</v-chip>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<v-row class="mt-4">
|
||||||
|
<v-col cols="4">
|
||||||
|
<div class="text-h6 font-weight-bold">{{ profile.eventsAttended }}</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">Events</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="4">
|
||||||
|
<div class="text-h6 font-weight-bold">{{ profile.connections }}</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">Connections</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="4">
|
||||||
|
<div class="text-h6 font-weight-bold">{{ profile.yearJoined }}</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">Joined</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-card-title class="text-body-1">Quick Actions</v-card-title>
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item @click="downloadMemberCard">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="error">mdi-card-account-details</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Download Member Card</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="exportData">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="error">mdi-download</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Export My Data</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="privacySettings">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="error">mdi-shield-lock</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Privacy Settings</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- Right Column - Profile Details -->
|
||||||
|
<v-col cols="12" lg="8">
|
||||||
|
<!-- Tab Navigation -->
|
||||||
|
<v-tabs
|
||||||
|
v-model="activeTab"
|
||||||
|
color="error"
|
||||||
|
class="mb-6"
|
||||||
|
>
|
||||||
|
<v-tab value="personal">
|
||||||
|
<v-icon start>mdi-account</v-icon>
|
||||||
|
Personal Info
|
||||||
|
</v-tab>
|
||||||
|
<v-tab value="contact">
|
||||||
|
<v-icon start>mdi-phone</v-icon>
|
||||||
|
Contact
|
||||||
|
</v-tab>
|
||||||
|
<v-tab value="professional">
|
||||||
|
<v-icon start>mdi-briefcase</v-icon>
|
||||||
|
Professional
|
||||||
|
</v-tab>
|
||||||
|
<v-tab value="preferences">
|
||||||
|
<v-icon start>mdi-cog</v-icon>
|
||||||
|
Preferences
|
||||||
|
</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
|
||||||
|
<!-- Tab Content -->
|
||||||
|
<v-window v-model="activeTab">
|
||||||
|
<!-- Personal Info Tab -->
|
||||||
|
<v-window-item value="personal">
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-card-title>
|
||||||
|
Personal Information
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
v-if="!editingPersonal"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="editingPersonal = true"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-pencil</v-icon>
|
||||||
|
Edit
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form v-model="personalFormValid">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profile.firstName"
|
||||||
|
label="First Name"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingPersonal"
|
||||||
|
:rules="editingPersonal ? [v => !!v || 'Required'] : []"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profile.lastName"
|
||||||
|
label="Last Name"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingPersonal"
|
||||||
|
:rules="editingPersonal ? [v => !!v || 'Required'] : []"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profile.dateOfBirth"
|
||||||
|
label="Date of Birth"
|
||||||
|
type="date"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingPersonal"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="profile.nationality"
|
||||||
|
label="Nationality"
|
||||||
|
:items="nationalities"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingPersonal"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="profile.bio"
|
||||||
|
label="Bio"
|
||||||
|
variant="outlined"
|
||||||
|
rows="3"
|
||||||
|
:readonly="!editingPersonal"
|
||||||
|
placeholder="Tell us about yourself..."
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions v-if="editingPersonal">
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="cancelEditPersonal">Cancel</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
:disabled="!personalFormValid"
|
||||||
|
@click="savePersonal"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-window-item>
|
||||||
|
|
||||||
|
<!-- Contact Tab -->
|
||||||
|
<v-window-item value="contact">
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-card-title>
|
||||||
|
Contact Information
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
v-if="!editingContact"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="editingContact = true"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-pencil</v-icon>
|
||||||
|
Edit
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form v-model="contactFormValid">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profile.email"
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingContact"
|
||||||
|
:rules="editingContact ? [v => !!v || 'Required', v => /.+@.+/.test(v) || 'Invalid email'] : []"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profile.phone"
|
||||||
|
label="Phone"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingContact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profile.address"
|
||||||
|
label="Address"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingContact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profile.city"
|
||||||
|
label="City"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingContact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profile.state"
|
||||||
|
label="State"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingContact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profile.zipCode"
|
||||||
|
label="ZIP Code"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingContact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions v-if="editingContact">
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="cancelEditContact">Cancel</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
:disabled="!contactFormValid"
|
||||||
|
@click="saveContact"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-window-item>
|
||||||
|
|
||||||
|
<!-- Professional Tab -->
|
||||||
|
<v-window-item value="professional">
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-card-title>
|
||||||
|
Professional Information
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
v-if="!editingProfessional"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="editingProfessional = true"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-pencil</v-icon>
|
||||||
|
Edit
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form v-model="professionalFormValid">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profile.company"
|
||||||
|
label="Company"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingProfessional"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profile.title"
|
||||||
|
label="Job Title"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingProfessional"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="profile.industry"
|
||||||
|
label="Industry"
|
||||||
|
:items="industries"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingProfessional"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profile.linkedin"
|
||||||
|
label="LinkedIn Profile"
|
||||||
|
variant="outlined"
|
||||||
|
:readonly="!editingProfessional"
|
||||||
|
placeholder="https://linkedin.com/in/..."
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="profile.expertise"
|
||||||
|
label="Areas of Expertise"
|
||||||
|
variant="outlined"
|
||||||
|
rows="2"
|
||||||
|
:readonly="!editingProfessional"
|
||||||
|
placeholder="e.g., Finance, Marketing, Technology..."
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions v-if="editingProfessional">
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="cancelEditProfessional">Cancel</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
:disabled="!professionalFormValid"
|
||||||
|
@click="saveProfessional"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-window-item>
|
||||||
|
|
||||||
|
<!-- Preferences Tab -->
|
||||||
|
<v-window-item value="preferences">
|
||||||
|
<v-card elevation="1" class="mb-4">
|
||||||
|
<v-card-title>Communication Preferences</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-switch
|
||||||
|
v-model="preferences.emailNotifications"
|
||||||
|
label="Email Notifications"
|
||||||
|
color="error"
|
||||||
|
hide-details
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
<v-switch
|
||||||
|
v-model="preferences.eventReminders"
|
||||||
|
label="Event Reminders"
|
||||||
|
color="error"
|
||||||
|
hide-details
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
<v-switch
|
||||||
|
v-model="preferences.newsletter"
|
||||||
|
label="Monthly Newsletter"
|
||||||
|
color="error"
|
||||||
|
hide-details
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
<v-switch
|
||||||
|
v-model="preferences.memberUpdates"
|
||||||
|
label="Member Updates"
|
||||||
|
color="error"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-card-title>Privacy Settings</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-switch
|
||||||
|
v-model="preferences.profileVisible"
|
||||||
|
label="Profile visible to other members"
|
||||||
|
color="error"
|
||||||
|
hide-details
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
<v-switch
|
||||||
|
v-model="preferences.showEmail"
|
||||||
|
label="Show email in member directory"
|
||||||
|
color="error"
|
||||||
|
hide-details
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
<v-switch
|
||||||
|
v-model="preferences.showPhone"
|
||||||
|
label="Show phone in member directory"
|
||||||
|
color="error"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
@click="savePreferences"
|
||||||
|
>
|
||||||
|
Save Preferences
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-window-item>
|
||||||
|
</v-window>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Member } from '~/utils/types';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'member',
|
||||||
|
middleware: 'member'
|
||||||
|
});
|
||||||
|
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const activeTab = ref('personal');
|
||||||
|
const editingPersonal = ref(false);
|
||||||
|
const editingContact = ref(false);
|
||||||
|
const editingProfessional = ref(false);
|
||||||
|
const personalFormValid = ref(true);
|
||||||
|
const contactFormValid = ref(true);
|
||||||
|
const professionalFormValid = ref(true);
|
||||||
|
|
||||||
|
// Profile data
|
||||||
|
const profile = ref({
|
||||||
|
memberId: 'MUSA-0001',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john.doe@example.com',
|
||||||
|
phone: '+1 234 567 8900',
|
||||||
|
dateOfBirth: '1985-06-15',
|
||||||
|
nationality: 'United States',
|
||||||
|
bio: 'Passionate about business and innovation. Active member of the Monaco business community.',
|
||||||
|
address: '123 Main Street',
|
||||||
|
city: 'Monaco',
|
||||||
|
state: 'MC',
|
||||||
|
zipCode: '98000',
|
||||||
|
company: 'Tech Innovations Inc.',
|
||||||
|
title: 'CEO & Founder',
|
||||||
|
industry: 'Technology',
|
||||||
|
linkedin: 'https://linkedin.com/in/johndoe',
|
||||||
|
expertise: 'Technology, Innovation, Business Strategy',
|
||||||
|
memberType: 'Premium',
|
||||||
|
eventsAttended: 24,
|
||||||
|
connections: 156,
|
||||||
|
yearJoined: '2021'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
const preferences = ref({
|
||||||
|
emailNotifications: true,
|
||||||
|
eventReminders: true,
|
||||||
|
newsletter: true,
|
||||||
|
memberUpdates: false,
|
||||||
|
profileVisible: true,
|
||||||
|
showEmail: false,
|
||||||
|
showPhone: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Options
|
||||||
|
const nationalities = ref([
|
||||||
|
'United States',
|
||||||
|
'Monaco',
|
||||||
|
'France',
|
||||||
|
'Italy',
|
||||||
|
'United Kingdom',
|
||||||
|
'Germany',
|
||||||
|
'Spain',
|
||||||
|
'Other'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const industries = ref([
|
||||||
|
'Technology',
|
||||||
|
'Finance',
|
||||||
|
'Healthcare',
|
||||||
|
'Real Estate',
|
||||||
|
'Hospitality',
|
||||||
|
'Manufacturing',
|
||||||
|
'Retail',
|
||||||
|
'Education',
|
||||||
|
'Other'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const profileCompletion = computed(() => {
|
||||||
|
let completed = 0;
|
||||||
|
const fields = [
|
||||||
|
profile.value.firstName,
|
||||||
|
profile.value.lastName,
|
||||||
|
profile.value.email,
|
||||||
|
profile.value.phone,
|
||||||
|
profile.value.dateOfBirth,
|
||||||
|
profile.value.nationality,
|
||||||
|
profile.value.bio,
|
||||||
|
profile.value.address,
|
||||||
|
profile.value.company,
|
||||||
|
profile.value.title
|
||||||
|
];
|
||||||
|
|
||||||
|
fields.forEach(field => {
|
||||||
|
if (field) completed += 10;
|
||||||
|
});
|
||||||
|
|
||||||
|
return completed;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const changeAvatar = () => {
|
||||||
|
console.log('Change avatar');
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadMemberCard = () => {
|
||||||
|
console.log('Download member card');
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportData = () => {
|
||||||
|
console.log('Export user data');
|
||||||
|
};
|
||||||
|
|
||||||
|
const privacySettings = () => {
|
||||||
|
activeTab.value = 'preferences';
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEditPersonal = () => {
|
||||||
|
editingPersonal.value = false;
|
||||||
|
// Reset form if needed
|
||||||
|
};
|
||||||
|
|
||||||
|
const savePersonal = () => {
|
||||||
|
console.log('Saving personal info');
|
||||||
|
editingPersonal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEditContact = () => {
|
||||||
|
editingContact.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveContact = () => {
|
||||||
|
console.log('Saving contact info');
|
||||||
|
editingContact.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEditProfessional = () => {
|
||||||
|
editingProfessional.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveProfessional = () => {
|
||||||
|
console.log('Saving professional info');
|
||||||
|
editingProfessional.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const savePreferences = () => {
|
||||||
|
console.log('Saving preferences', preferences.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load real member data on mount
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const { data: sessionData } = await $fetch<{ success: boolean; member: Member | null }>('/api/auth/session');
|
||||||
|
if (sessionData?.member) {
|
||||||
|
// Map real data to profile
|
||||||
|
profile.value.firstName = sessionData.member.first_name || profile.value.firstName;
|
||||||
|
profile.value.lastName = sessionData.member.last_name || profile.value.lastName;
|
||||||
|
profile.value.email = sessionData.member.email || profile.value.email;
|
||||||
|
profile.value.phone = sessionData.member.phone || profile.value.phone;
|
||||||
|
profile.value.nationality = sessionData.member.nationality || profile.value.nationality;
|
||||||
|
profile.value.memberId = sessionData.member.member_id || profile.value.memberId;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading member data:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Custom styles if needed */
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,506 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-h4 font-weight-bold mb-2">Resources</h1>
|
||||||
|
<p class="text-body-1 text-medium-emphasis">Access documents, guides, and helpful resources</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<v-card class="mb-6" elevation="1">
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchQuery"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
label="Search resources"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Resource Categories -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.id"
|
||||||
|
cols="6"
|
||||||
|
sm="4"
|
||||||
|
md="3"
|
||||||
|
>
|
||||||
|
<v-card
|
||||||
|
:color="selectedCategory === category.id ? 'error' : undefined"
|
||||||
|
:variant="selectedCategory === category.id ? 'tonal' : 'outlined'"
|
||||||
|
class="text-center pa-4 cursor-pointer"
|
||||||
|
hover
|
||||||
|
@click="selectedCategory = selectedCategory === category.id ? null : category.id"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
size="32"
|
||||||
|
:color="selectedCategory === category.id ? 'error' : 'grey'"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
{{ category.icon }}
|
||||||
|
</v-icon>
|
||||||
|
<div class="text-body-2 font-weight-medium">{{ category.name }}</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ category.count }} items</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Resources Grid -->
|
||||||
|
<v-row>
|
||||||
|
<!-- Documents Section -->
|
||||||
|
<v-col cols="12">
|
||||||
|
<h3 class="text-h6 mb-3">
|
||||||
|
<v-icon start color="error">mdi-file-document</v-icon>
|
||||||
|
Documents
|
||||||
|
</h3>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
v-for="doc in filteredDocuments"
|
||||||
|
:key="doc.id"
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
lg="4"
|
||||||
|
>
|
||||||
|
<v-card elevation="1" hover>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex align-center mb-2">
|
||||||
|
<v-icon :color="getFileIconColor(doc.type)" class="mr-3">
|
||||||
|
{{ getFileIcon(doc.type) }}
|
||||||
|
</v-icon>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="font-weight-medium">{{ doc.title }}</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ doc.size }} • {{ doc.date }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-body-2 text-medium-emphasis mb-3">{{ doc.description }}</p>
|
||||||
|
<v-chip
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
color="grey"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
{{ doc.category }}
|
||||||
|
</v-chip>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
@click="viewDocument(doc)"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-eye</v-icon>
|
||||||
|
View
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="downloadDocument(doc)"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-download</v-icon>
|
||||||
|
Download
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Guides Section -->
|
||||||
|
<v-row class="mt-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<h3 class="text-h6 mb-3">
|
||||||
|
<v-icon start color="error">mdi-book-open-variant</v-icon>
|
||||||
|
Guides & Tutorials
|
||||||
|
</h3>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-expansion-panels variant="accordion">
|
||||||
|
<v-expansion-panel
|
||||||
|
v-for="guide in guides"
|
||||||
|
:key="guide.id"
|
||||||
|
>
|
||||||
|
<v-expansion-panel-title>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon class="mr-3" :color="guide.color">{{ guide.icon }}</v-icon>
|
||||||
|
<div>
|
||||||
|
<div class="font-weight-medium">{{ guide.title }}</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ guide.duration }} • {{ guide.level }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-expansion-panel-title>
|
||||||
|
<v-expansion-panel-text>
|
||||||
|
<p class="mb-3">{{ guide.description }}</p>
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item
|
||||||
|
v-for="(step, index) in guide.steps"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-avatar size="24" color="error" variant="tonal">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</v-avatar>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ step }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
class="mt-3"
|
||||||
|
@click="startGuide(guide)"
|
||||||
|
>
|
||||||
|
Start Guide
|
||||||
|
</v-btn>
|
||||||
|
</v-expansion-panel-text>
|
||||||
|
</v-expansion-panel>
|
||||||
|
</v-expansion-panels>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Quick Links Section -->
|
||||||
|
<v-row class="mt-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<h3 class="text-h6 mb-3">
|
||||||
|
<v-icon start color="error">mdi-link-variant</v-icon>
|
||||||
|
Quick Links
|
||||||
|
</h3>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-list lines="two">
|
||||||
|
<v-list-item
|
||||||
|
v-for="link in quickLinks"
|
||||||
|
:key="link.id"
|
||||||
|
:href="link.url"
|
||||||
|
target="_blank"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-avatar color="error" variant="tonal">
|
||||||
|
<v-icon>{{ link.icon }}</v-icon>
|
||||||
|
</v-avatar>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ link.title }}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ link.description }}</v-list-item-subtitle>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-icon>mdi-open-in-new</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- FAQs Section -->
|
||||||
|
<v-row class="mt-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<h3 class="text-h6 mb-3">
|
||||||
|
<v-icon start color="error">mdi-help-circle</v-icon>
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h3>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="1">
|
||||||
|
<v-list>
|
||||||
|
<template v-for="(faq, index) in faqs" :key="faq.id">
|
||||||
|
<v-list-item @click="faq.expanded = !faq.expanded">
|
||||||
|
<v-list-item-title class="font-weight-medium">
|
||||||
|
{{ faq.question }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-icon>
|
||||||
|
{{ faq.expanded ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
<v-expand-transition>
|
||||||
|
<div v-show="faq.expanded">
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-subtitle class="text-wrap">
|
||||||
|
{{ faq.answer }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</div>
|
||||||
|
</v-expand-transition>
|
||||||
|
<v-divider v-if="index < faqs.length - 1" />
|
||||||
|
</template>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'member',
|
||||||
|
middleware: 'member'
|
||||||
|
});
|
||||||
|
|
||||||
|
// State
|
||||||
|
const searchQuery = ref('');
|
||||||
|
const selectedCategory = ref<string | null>(null);
|
||||||
|
|
||||||
|
// Categories
|
||||||
|
const categories = ref([
|
||||||
|
{ id: 'membership', name: 'Membership', icon: 'mdi-card-account-details', count: 5 },
|
||||||
|
{ id: 'events', name: 'Events', icon: 'mdi-calendar', count: 8 },
|
||||||
|
{ id: 'finance', name: 'Finance', icon: 'mdi-currency-usd', count: 4 },
|
||||||
|
{ id: 'governance', name: 'Governance', icon: 'mdi-gavel', count: 6 },
|
||||||
|
{ id: 'guides', name: 'Guides', icon: 'mdi-book-open', count: 10 },
|
||||||
|
{ id: 'forms', name: 'Forms', icon: 'mdi-file-document-edit', count: 7 },
|
||||||
|
{ id: 'policies', name: 'Policies', icon: 'mdi-shield-check', count: 5 },
|
||||||
|
{ id: 'other', name: 'Other', icon: 'mdi-folder', count: 3 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
const documents = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Member Handbook 2024',
|
||||||
|
description: 'Complete guide to membership benefits and responsibilities',
|
||||||
|
category: 'membership',
|
||||||
|
type: 'pdf',
|
||||||
|
size: '2.4 MB',
|
||||||
|
date: 'Jan 2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Annual Report 2023',
|
||||||
|
description: 'Financial statements and organizational achievements',
|
||||||
|
category: 'finance',
|
||||||
|
type: 'pdf',
|
||||||
|
size: '5.1 MB',
|
||||||
|
date: 'Mar 2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Event Planning Guide',
|
||||||
|
description: 'How to organize and host MonacoUSA events',
|
||||||
|
category: 'events',
|
||||||
|
type: 'docx',
|
||||||
|
size: '1.2 MB',
|
||||||
|
date: 'Feb 2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Bylaws and Constitution',
|
||||||
|
description: 'Official governing documents of MonacoUSA',
|
||||||
|
category: 'governance',
|
||||||
|
type: 'pdf',
|
||||||
|
size: '890 KB',
|
||||||
|
date: 'Jan 2023'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'Membership Application Form',
|
||||||
|
description: 'Form for new member applications',
|
||||||
|
category: 'forms',
|
||||||
|
type: 'pdf',
|
||||||
|
size: '245 KB',
|
||||||
|
date: 'Jan 2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: 'Privacy Policy',
|
||||||
|
description: 'How we handle and protect your personal information',
|
||||||
|
category: 'policies',
|
||||||
|
type: 'pdf',
|
||||||
|
size: '180 KB',
|
||||||
|
date: 'Dec 2023'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Guides
|
||||||
|
const guides = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Getting Started with MonacoUSA',
|
||||||
|
description: 'A comprehensive guide for new members to navigate the portal and make the most of their membership',
|
||||||
|
duration: '10 min',
|
||||||
|
level: 'Beginner',
|
||||||
|
icon: 'mdi-rocket-launch',
|
||||||
|
color: 'green',
|
||||||
|
expanded: false,
|
||||||
|
steps: [
|
||||||
|
'Complete your profile information',
|
||||||
|
'Explore upcoming events',
|
||||||
|
'Connect with other members',
|
||||||
|
'Access member resources',
|
||||||
|
'Set up payment methods'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'How to Register for Events',
|
||||||
|
description: 'Step-by-step instructions for browsing and registering for MonacoUSA events',
|
||||||
|
duration: '5 min',
|
||||||
|
level: 'Beginner',
|
||||||
|
icon: 'mdi-calendar-plus',
|
||||||
|
color: 'blue',
|
||||||
|
expanded: false,
|
||||||
|
steps: [
|
||||||
|
'Navigate to the Events page',
|
||||||
|
'Browse available events',
|
||||||
|
'Click on an event for details',
|
||||||
|
'Click the Register button',
|
||||||
|
'Confirm your registration'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Managing Your Dues and Payments',
|
||||||
|
description: 'Learn how to view payment history, update payment methods, and manage your dues',
|
||||||
|
duration: '7 min',
|
||||||
|
level: 'Intermediate',
|
||||||
|
icon: 'mdi-credit-card',
|
||||||
|
color: 'purple',
|
||||||
|
expanded: false,
|
||||||
|
steps: [
|
||||||
|
'Access your payment dashboard',
|
||||||
|
'Review payment history',
|
||||||
|
'Update payment method',
|
||||||
|
'Set up automatic payments',
|
||||||
|
'Download payment receipts'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Quick Links
|
||||||
|
const quickLinks = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Monaco Government Portal',
|
||||||
|
description: 'Official Monaco government website',
|
||||||
|
url: 'https://www.gouv.mc',
|
||||||
|
icon: 'mdi-bank'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'US Embassy in France',
|
||||||
|
description: 'Consular services for US citizens',
|
||||||
|
url: 'https://fr.usembassy.gov',
|
||||||
|
icon: 'mdi-flag'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Monaco Economic Board',
|
||||||
|
description: 'Business and investment opportunities',
|
||||||
|
url: 'https://www.monacoeconomicboard.mc',
|
||||||
|
icon: 'mdi-briefcase'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Visit Monaco',
|
||||||
|
description: 'Tourism and cultural information',
|
||||||
|
url: 'https://www.visitmonaco.com',
|
||||||
|
icon: 'mdi-map'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// FAQs
|
||||||
|
const faqs = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
question: 'How do I update my contact information?',
|
||||||
|
answer: 'You can update your contact information by going to your Profile page and clicking the Edit button in the Contact Information section. Make your changes and click Save to update your information.',
|
||||||
|
expanded: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
question: 'When are membership dues payable?',
|
||||||
|
answer: 'Annual membership dues are payable at the beginning of each calendar year. You will receive a reminder email in December with payment instructions. You can pay online through the portal or by bank transfer.',
|
||||||
|
expanded: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
question: 'How do I cancel my event registration?',
|
||||||
|
answer: 'To cancel an event registration, go to the Events page, click on "My Registrations" tab, find the event you want to cancel, and click the Cancel button. Please note that cancellation policies may vary by event.',
|
||||||
|
expanded: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
question: 'Who can I contact for technical support?',
|
||||||
|
answer: 'For technical support, please email support@monacousa.org or use the Contact Support button in your dashboard. Our support team typically responds within 24-48 hours.',
|
||||||
|
expanded: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
question: 'How do I access member-only content?',
|
||||||
|
answer: 'Member-only content is automatically available once you log in to the portal. If you\'re having trouble accessing content, please ensure your membership is active and your dues are current.',
|
||||||
|
expanded: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const filteredDocuments = computed(() => {
|
||||||
|
let filtered = documents.value;
|
||||||
|
|
||||||
|
if (selectedCategory.value) {
|
||||||
|
filtered = filtered.filter(doc => doc.category === selectedCategory.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery.value) {
|
||||||
|
const query = searchQuery.value.toLowerCase();
|
||||||
|
filtered = filtered.filter(doc =>
|
||||||
|
doc.title.toLowerCase().includes(query) ||
|
||||||
|
doc.description.toLowerCase().includes(query) ||
|
||||||
|
doc.category.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const getFileIcon = (type: string) => {
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
pdf: 'mdi-file-pdf-box',
|
||||||
|
docx: 'mdi-file-word',
|
||||||
|
xlsx: 'mdi-file-excel',
|
||||||
|
pptx: 'mdi-file-powerpoint',
|
||||||
|
default: 'mdi-file-document'
|
||||||
|
};
|
||||||
|
return icons[type] || icons.default;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileIconColor = (type: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
pdf: 'red',
|
||||||
|
docx: 'blue',
|
||||||
|
xlsx: 'green',
|
||||||
|
pptx: 'orange',
|
||||||
|
default: 'grey'
|
||||||
|
};
|
||||||
|
return colors[type] || colors.default;
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewDocument = (doc: any) => {
|
||||||
|
console.log('Viewing document:', doc.title);
|
||||||
|
// Open document in new tab or modal
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadDocument = (doc: any) => {
|
||||||
|
console.log('Downloading document:', doc.title);
|
||||||
|
// Trigger download
|
||||||
|
};
|
||||||
|
|
||||||
|
const startGuide = (guide: any) => {
|
||||||
|
console.log('Starting guide:', guide.title);
|
||||||
|
// Navigate to guide or open tutorial
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-wrap {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
:enter="{ opacity: 1, y: 0 }"
|
:enter="{ opacity: 1, y: 0 }"
|
||||||
class="members-header__content"
|
class="members-header__content"
|
||||||
>
|
>
|
||||||
<h1 class="members-header__title">Members Directory</h1>
|
<h1 class="members-header__title">Member Management</h1>
|
||||||
<p class="members-header__subtitle">Connect with {{ totalMembers }} MonacoUSA members worldwide</p>
|
<p class="members-header__subtitle">Manage {{ totalMembers }} MonacoUSA members</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -19,10 +19,10 @@
|
||||||
class="members-header__actions"
|
class="members-header__actions"
|
||||||
>
|
>
|
||||||
<MonacoButton variant="glass" icon="download">
|
<MonacoButton variant="glass" icon="download">
|
||||||
Export Directory
|
Export Members
|
||||||
</MonacoButton>
|
</MonacoButton>
|
||||||
<MonacoButton variant="primary" icon="user-plus">
|
<MonacoButton variant="primary" icon="user-plus">
|
||||||
Invite Member
|
Add Member
|
||||||
</MonacoButton>
|
</MonacoButton>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
<div class="members-controls__search">
|
<div class="members-controls__search">
|
||||||
<FloatingInput
|
<FloatingInput
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
label="Search members by name, company, or skills..."
|
label="Search members by name or email..."
|
||||||
leftIcon="search"
|
leftIcon="search"
|
||||||
variant="glass"
|
variant="glass"
|
||||||
clearable
|
clearable
|
||||||
|
|
@ -46,26 +46,25 @@
|
||||||
|
|
||||||
<div class="members-controls__filters">
|
<div class="members-controls__filters">
|
||||||
<select class="filter-select">
|
<select class="filter-select">
|
||||||
<option value="">All Roles</option>
|
<option value="">Member Status</option>
|
||||||
<option value="board">Board Member</option>
|
<option value="active">Active</option>
|
||||||
<option value="executive">Executive</option>
|
<option value="inactive">Inactive</option>
|
||||||
<option value="member">Member</option>
|
<option value="pending">Pending</option>
|
||||||
<option value="honorary">Honorary</option>
|
<option value="suspended">Suspended</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select class="filter-select">
|
<select class="filter-select">
|
||||||
<option value="">All Industries</option>
|
<option value="">Dues Status</option>
|
||||||
<option value="tech">Technology</option>
|
<option value="current">Current</option>
|
||||||
<option value="finance">Finance</option>
|
<option value="overdue">Overdue</option>
|
||||||
<option value="real-estate">Real Estate</option>
|
<option value="exempt">Exempt</option>
|
||||||
<option value="hospitality">Hospitality</option>
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select class="filter-select">
|
<select class="filter-select">
|
||||||
<option value="">Sort By</option>
|
<option value="">Sort By</option>
|
||||||
<option value="name">Name (A-Z)</option>
|
<option value="name">Name (A-Z)</option>
|
||||||
<option value="recent">Recently Joined</option>
|
<option value="joined">Join Date</option>
|
||||||
<option value="active">Most Active</option>
|
<option value="dues">Dues Status</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -78,16 +77,20 @@
|
||||||
class="members-stats"
|
class="members-stats"
|
||||||
>
|
>
|
||||||
<div class="stat-chip">
|
<div class="stat-chip">
|
||||||
<span class="stat-chip__value">{{ filteredMembers.length }}</span>
|
<span class="stat-chip__value">{{ totalMembers }}</span>
|
||||||
<span class="stat-chip__label">Results</span>
|
<span class="stat-chip__label">Total Members</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-chip">
|
<div class="stat-chip">
|
||||||
<span class="stat-chip__value">{{ onlineCount }}</span>
|
<span class="stat-chip__value">{{ activeMembers }}</span>
|
||||||
<span class="stat-chip__label">Online Now</span>
|
<span class="stat-chip__label">Active Members</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-chip">
|
<div class="stat-chip">
|
||||||
<span class="stat-chip__value">{{ newThisMonth }}</span>
|
<span class="stat-chip__value">{{ overdueDues }}</span>
|
||||||
<span class="stat-chip__label">New This Month</span>
|
<span class="stat-chip__label">Overdue Dues</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-chip">
|
||||||
|
<span class="stat-chip__value">{{ inactiveMembers }}</span>
|
||||||
|
<span class="stat-chip__label">Inactive Members</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -101,35 +104,126 @@
|
||||||
<div class="view-toggle">
|
<div class="view-toggle">
|
||||||
<button
|
<button
|
||||||
class="view-toggle__btn"
|
class="view-toggle__btn"
|
||||||
:class="{ 'view-toggle__btn--active': viewMode === 'grid' }"
|
:class="{ 'view-toggle__btn--active': viewMode === 'table' }"
|
||||||
@click="viewMode = 'grid'"
|
@click="viewMode = 'table'"
|
||||||
>
|
>
|
||||||
<span>⊞</span> Grid View
|
<span>☰</span> Table View
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="view-toggle__btn"
|
class="view-toggle__btn"
|
||||||
:class="{ 'view-toggle__btn--active': viewMode === 'list' }"
|
:class="{ 'view-toggle__btn--active': viewMode === 'cards' }"
|
||||||
@click="viewMode = 'list'"
|
@click="viewMode = 'cards'"
|
||||||
>
|
>
|
||||||
<span>☰</span> List View
|
<span>⊞</span> Card View
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="view-toggle__btn"
|
|
||||||
:class="{ 'view-toggle__btn--active': viewMode === 'compact' }"
|
|
||||||
@click="viewMode = 'compact'"
|
|
||||||
>
|
|
||||||
<span>▦</span> Compact
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Members Grid/List -->
|
<!-- Members Table/Cards -->
|
||||||
<div
|
<div
|
||||||
class="members-container"
|
class="members-container"
|
||||||
:class="`members-container--${viewMode}`"
|
:class="`members-container--${viewMode}`"
|
||||||
>
|
>
|
||||||
<!-- Grid View -->
|
<!-- Table View -->
|
||||||
<template v-if="viewMode === 'grid'">
|
<template v-if="viewMode === 'table'">
|
||||||
|
<div class="members-table">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Member</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Dues</th>
|
||||||
|
<th>Join Date</th>
|
||||||
|
<th>Portal Account</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(member, index) in filteredMembers"
|
||||||
|
:key="member.id"
|
||||||
|
v-motion
|
||||||
|
:initial="{ opacity: 0, x: -20 }"
|
||||||
|
:enter="{
|
||||||
|
opacity: 1,
|
||||||
|
x: 0,
|
||||||
|
transition: { delay: 400 + (index * 30) }
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<div class="member-cell">
|
||||||
|
<div class="member-cell__avatar">
|
||||||
|
<img v-if="member.avatar" :src="member.avatar" :alt="member.name" />
|
||||||
|
<div v-else class="avatar-placeholder-small">{{ getInitials(member.name) }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="member-cell__name">{{ member.name }}</div>
|
||||||
|
<div class="member-cell__email">{{ member.email }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="status-badge"
|
||||||
|
:class="`status-badge--${member.memberStatus}`"
|
||||||
|
>
|
||||||
|
{{ member.memberStatus }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="dues-badge"
|
||||||
|
:class="`dues-badge--${member.duesStatus}`"
|
||||||
|
>
|
||||||
|
{{ member.duesStatus }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ member.joinDate }}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="portal-status"
|
||||||
|
:class="{ 'portal-status--active': member.hasPortalAccess }"
|
||||||
|
>
|
||||||
|
{{ member.hasPortalAccess ? 'Active' : 'None' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="table-actions">
|
||||||
|
<MonacoButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
icon="edit"
|
||||||
|
@click="editMember(member)"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</MonacoButton>
|
||||||
|
<MonacoButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
icon="mail"
|
||||||
|
@click="emailMember(member)"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</MonacoButton>
|
||||||
|
<MonacoButton
|
||||||
|
v-if="!member.hasPortalAccess"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
icon="user-plus"
|
||||||
|
@click="inviteToPortal(member)"
|
||||||
|
>
|
||||||
|
Invite
|
||||||
|
</MonacoButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Card View -->
|
||||||
|
<template v-else-if="viewMode === 'cards'">
|
||||||
<MemberCard
|
<MemberCard
|
||||||
v-for="(member, index) in filteredMembers"
|
v-for="(member, index) in filteredMembers"
|
||||||
:key="member.id"
|
:key="member.id"
|
||||||
|
|
@ -137,14 +231,11 @@
|
||||||
:featured="member.featured"
|
:featured="member.featured"
|
||||||
:delay="index"
|
:delay="index"
|
||||||
variant="glass"
|
variant="glass"
|
||||||
@click="selectMember"
|
@click="editMember"
|
||||||
@connect="connectMember"
|
@email="emailMember"
|
||||||
@message="messageMember"
|
@invite="inviteToPortal"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- List View -->
|
|
||||||
<template v-else-if="viewMode === 'list'">
|
|
||||||
<div
|
<div
|
||||||
v-for="(member, index) in filteredMembers"
|
v-for="(member, index) in filteredMembers"
|
||||||
:key="member.id"
|
:key="member.id"
|
||||||
|
|
@ -189,58 +280,25 @@
|
||||||
|
|
||||||
<div class="member-list-item__actions">
|
<div class="member-list-item__actions">
|
||||||
<MonacoButton
|
<MonacoButton
|
||||||
:variant="member.connected ? 'ghost' : 'primary'"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click.stop="connectMember(member)"
|
icon="edit"
|
||||||
|
@click.stop="editMember(member)"
|
||||||
>
|
>
|
||||||
{{ member.connected ? 'Connected' : 'Connect' }}
|
Edit
|
||||||
</MonacoButton>
|
</MonacoButton>
|
||||||
<MonacoButton
|
<MonacoButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
icon="message"
|
icon="mail"
|
||||||
@click.stop="messageMember(member)"
|
@click.stop="emailMember(member)"
|
||||||
>
|
>
|
||||||
Message
|
Email
|
||||||
</MonacoButton>
|
</MonacoButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Compact View -->
|
|
||||||
<template v-else>
|
|
||||||
<div class="compact-grid">
|
|
||||||
<div
|
|
||||||
v-for="(member, index) in filteredMembers"
|
|
||||||
:key="member.id"
|
|
||||||
v-motion
|
|
||||||
:initial="{ opacity: 0, scale: 0.9 }"
|
|
||||||
:enter="{
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1,
|
|
||||||
transition: { delay: 400 + (index * 20) }
|
|
||||||
}"
|
|
||||||
class="compact-card"
|
|
||||||
@click="selectMember(member)"
|
|
||||||
>
|
|
||||||
<div class="compact-card__avatar">
|
|
||||||
<img v-if="member.avatar" :src="member.avatar" :alt="member.name" />
|
|
||||||
<div v-else class="avatar-placeholder-small">{{ getInitials(member.name) }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="compact-card__info">
|
|
||||||
<h4 class="compact-card__name">{{ member.name }}</h4>
|
|
||||||
<p class="compact-card__role">{{ member.title }}</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="compact-card__connect"
|
|
||||||
:class="{ 'compact-card__connect--connected': member.connected }"
|
|
||||||
@click.stop="connectMember(member)"
|
|
||||||
>
|
|
||||||
{{ member.connected ? '✓' : '+' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Load More -->
|
<!-- Load More -->
|
||||||
|
|
@ -255,31 +313,28 @@
|
||||||
</MonacoButton>
|
</MonacoButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Floating Stats Widget -->
|
<!-- Admin Quick Actions Widget -->
|
||||||
<GlassCard
|
<GlassCard
|
||||||
variant="glass"
|
variant="glass"
|
||||||
class="network-widget"
|
class="admin-widget"
|
||||||
:animated="true"
|
:animated="true"
|
||||||
:delay="900"
|
:delay="900"
|
||||||
>
|
>
|
||||||
<h4 class="network-widget__title">Your Network</h4>
|
<h4 class="admin-widget__title">Quick Actions</h4>
|
||||||
<div class="network-stats">
|
<div class="admin-actions">
|
||||||
<div class="network-stat">
|
<MonacoButton variant="glass" size="sm" icon="download" block>
|
||||||
<span class="network-stat__value">47</span>
|
Export Member List
|
||||||
<span class="network-stat__label">Connections</span>
|
</MonacoButton>
|
||||||
</div>
|
<MonacoButton variant="glass" size="sm" icon="mail" block>
|
||||||
<div class="network-stat">
|
Send Dues Reminder
|
||||||
<span class="network-stat__value">12</span>
|
</MonacoButton>
|
||||||
<span class="network-stat__label">Pending</span>
|
<MonacoButton variant="glass" size="sm" icon="chart" block>
|
||||||
</div>
|
View Reports
|
||||||
<div class="network-stat">
|
</MonacoButton>
|
||||||
<span class="network-stat__value">5</span>
|
<MonacoButton variant="primary" size="sm" icon="user-plus" block>
|
||||||
<span class="network-stat__label">New Messages</span>
|
Add New Member
|
||||||
</div>
|
</MonacoButton>
|
||||||
</div>
|
</div>
|
||||||
<MonacoButton variant="primary" size="sm" block>
|
|
||||||
View All Connections
|
|
||||||
</MonacoButton>
|
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -292,95 +347,90 @@ import FloatingInput from '~/components/ui/FloatingInput.vue'
|
||||||
import MemberCard from '~/components/ui/MemberCard.vue'
|
import MemberCard from '~/components/ui/MemberCard.vue'
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const viewMode = ref('grid')
|
const viewMode = ref('table')
|
||||||
const totalMembers = ref(1234)
|
const totalMembers = ref(156)
|
||||||
const onlineCount = ref(89)
|
const activeMembers = ref(142)
|
||||||
const newThisMonth = ref(23)
|
const overdueDues = ref(8)
|
||||||
|
const inactiveMembers = ref(14)
|
||||||
|
|
||||||
const members = ref([
|
const members = ref([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Alexandra Martin',
|
name: 'Alexandra Martin',
|
||||||
|
email: 'alexandra.martin@example.com',
|
||||||
avatar: '/api/placeholder/150/150',
|
avatar: '/api/placeholder/150/150',
|
||||||
title: 'CEO',
|
title: 'CEO',
|
||||||
company: 'Monaco Ventures',
|
company: 'Monaco Ventures',
|
||||||
role: 'Board Member',
|
memberStatus: 'active',
|
||||||
status: 'online',
|
duesStatus: 'current',
|
||||||
tags: ['Leadership', 'Strategy', 'Investment'],
|
hasPortalAccess: true,
|
||||||
joinDate: '2021',
|
joinDate: 'Jan 2021',
|
||||||
connections: 234,
|
lastActive: '2 hours ago'
|
||||||
connected: false,
|
|
||||||
featured: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'Jean-Paul Rousseau',
|
name: 'Jean-Paul Rousseau',
|
||||||
|
email: 'jp.rousseau@example.com',
|
||||||
avatar: '/api/placeholder/150/150',
|
avatar: '/api/placeholder/150/150',
|
||||||
title: 'Managing Director',
|
title: 'Managing Director',
|
||||||
company: 'Riviera Capital',
|
company: 'Riviera Capital',
|
||||||
role: 'Executive',
|
memberStatus: 'active',
|
||||||
status: 'online',
|
duesStatus: 'current',
|
||||||
tags: ['Finance', 'Real Estate', 'Banking'],
|
hasPortalAccess: true,
|
||||||
joinDate: '2020',
|
joinDate: 'Mar 2020',
|
||||||
connections: 189,
|
lastActive: '1 day ago'
|
||||||
connected: true,
|
|
||||||
featured: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
name: 'Sarah Chen',
|
name: 'Sarah Chen',
|
||||||
|
email: 'sarah.chen@example.com',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
title: 'VP of Technology',
|
title: 'VP of Technology',
|
||||||
company: 'TechMonaco',
|
company: 'TechMonaco',
|
||||||
role: 'Member',
|
memberStatus: 'active',
|
||||||
status: 'offline',
|
duesStatus: 'overdue',
|
||||||
tags: ['Technology', 'AI', 'Innovation', 'Blockchain'],
|
hasPortalAccess: false,
|
||||||
joinDate: '2022',
|
joinDate: 'Jun 2022',
|
||||||
connections: 156,
|
lastActive: 'Never'
|
||||||
connected: false,
|
|
||||||
featured: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
name: 'Marcus Williams',
|
name: 'Marcus Williams',
|
||||||
|
email: 'marcus.w@example.com',
|
||||||
avatar: '/api/placeholder/150/150',
|
avatar: '/api/placeholder/150/150',
|
||||||
title: 'Partner',
|
title: 'Partner',
|
||||||
company: 'Monaco Law Group',
|
company: 'Monaco Law Group',
|
||||||
role: 'Member',
|
memberStatus: 'inactive',
|
||||||
status: 'away',
|
duesStatus: 'overdue',
|
||||||
tags: ['Legal', 'Corporate Law', 'M&A'],
|
hasPortalAccess: false,
|
||||||
joinDate: '2021',
|
joinDate: 'Sep 2021',
|
||||||
connections: 98,
|
lastActive: '3 months ago'
|
||||||
connected: false,
|
|
||||||
featured: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
name: 'Isabella Romano',
|
name: 'Isabella Romano',
|
||||||
|
email: 'isabella@example.com',
|
||||||
avatar: '/api/placeholder/150/150',
|
avatar: '/api/placeholder/150/150',
|
||||||
title: 'Creative Director',
|
title: 'Creative Director',
|
||||||
company: 'Romano Design Studio',
|
company: 'Romano Design Studio',
|
||||||
role: 'Member',
|
memberStatus: 'active',
|
||||||
status: 'online',
|
duesStatus: 'current',
|
||||||
tags: ['Design', 'Branding', 'Marketing'],
|
hasPortalAccess: true,
|
||||||
joinDate: '2023',
|
joinDate: 'Feb 2023',
|
||||||
connections: 67,
|
lastActive: '5 days ago'
|
||||||
connected: true,
|
|
||||||
featured: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
name: 'Thomas Anderson',
|
name: 'Thomas Anderson',
|
||||||
|
email: 't.anderson@example.com',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
title: 'Investment Manager',
|
title: 'Investment Manager',
|
||||||
company: 'Monte Carlo Holdings',
|
company: 'Monte Carlo Holdings',
|
||||||
role: 'Honorary',
|
memberStatus: 'pending',
|
||||||
status: 'offline',
|
duesStatus: 'exempt',
|
||||||
tags: ['Investment', 'Wealth Management', 'Private Equity'],
|
hasPortalAccess: false,
|
||||||
joinDate: '2019',
|
joinDate: 'Nov 2019',
|
||||||
connections: 321,
|
lastActive: 'Never'
|
||||||
connected: false,
|
|
||||||
featured: true
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
@ -390,9 +440,8 @@ const filteredMembers = computed(() => {
|
||||||
const query = searchQuery.value.toLowerCase()
|
const query = searchQuery.value.toLowerCase()
|
||||||
return members.value.filter(member =>
|
return members.value.filter(member =>
|
||||||
member.name.toLowerCase().includes(query) ||
|
member.name.toLowerCase().includes(query) ||
|
||||||
member.company?.toLowerCase().includes(query) ||
|
member.email?.toLowerCase().includes(query) ||
|
||||||
member.title?.toLowerCase().includes(query) ||
|
member.company?.toLowerCase().includes(query)
|
||||||
member.tags?.some(tag => tag.toLowerCase().includes(query))
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -400,17 +449,19 @@ const getInitials = (name: string) => {
|
||||||
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectMember = (member: any) => {
|
const editMember = (member: any) => {
|
||||||
console.log('Selected member:', member)
|
console.log('Edit member:', member)
|
||||||
|
// Would open edit dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectMember = (member: any) => {
|
const emailMember = (member: any) => {
|
||||||
member.connected = !member.connected
|
console.log('Email member:', member)
|
||||||
console.log('Connect with:', member)
|
// Would open email compose
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageMember = (member: any) => {
|
const inviteToPortal = (member: any) => {
|
||||||
console.log('Message:', member)
|
console.log('Invite to portal:', member)
|
||||||
|
// Would send portal invitation
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -566,25 +617,146 @@ const messageMember = (member: any) => {
|
||||||
.members-container {
|
.members-container {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
&--grid {
|
&--cards {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--list {
|
&--table {
|
||||||
display: flex;
|
overflow-x: auto;
|
||||||
flex-direction: column;
|
}
|
||||||
gap: 1rem;
|
}
|
||||||
|
|
||||||
|
.members-table {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--compact {
|
th {
|
||||||
.compact-grid {
|
padding: 1rem;
|
||||||
display: grid;
|
text-align: left;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
font-size: 0.875rem;
|
||||||
gap: 1rem;
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border-bottom: 2px solid rgba(220, 38, 38, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
|
||||||
|
&__avatar {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #27272a;
|
||||||
|
margin-bottom: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__email {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: capitalize;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--inactive {
|
||||||
|
background: rgba(107, 114, 128, 0.1);
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pending {
|
||||||
|
background: rgba(251, 146, 60, 0.1);
|
||||||
|
color: #fb923c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--suspended {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dues-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: capitalize;
|
||||||
|
|
||||||
|
&--current {
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--overdue {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--exempt {
|
||||||
|
background: rgba(107, 114, 128, 0.1);
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-list-item {
|
.member-list-item {
|
||||||
|
|
@ -798,7 +970,7 @@ const messageMember = (member: any) => {
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-widget {
|
.admin-widget {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 2rem;
|
bottom: 2rem;
|
||||||
right: 2rem;
|
right: 2rem;
|
||||||
|
|
@ -814,32 +986,10 @@ const messageMember = (member: any) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-stats {
|
.admin-actions {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-stat {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 0.75rem;
|
||||||
padding: 0.5rem;
|
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
&__value {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label {
|
|
||||||
font-size: 0.625rem;
|
|
||||||
color: #6b7280;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Responsive
|
// Responsive
|
||||||
|
|
@ -876,8 +1026,21 @@ const messageMember = (member: any) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-widget {
|
.admin-widget {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.members-table {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="profile-mockup">
|
<div class="profile-mockup">
|
||||||
<!-- Profile Header -->
|
<!-- Admin Header -->
|
||||||
<div
|
<div
|
||||||
v-motion
|
v-motion
|
||||||
:initial="{ opacity: 0, y: -20 }"
|
:initial="{ opacity: 0, y: -20 }"
|
||||||
|
|
@ -22,51 +22,64 @@
|
||||||
<div v-else class="profile-avatar__placeholder">
|
<div v-else class="profile-avatar__placeholder">
|
||||||
{{ initials }}
|
{{ initials }}
|
||||||
</div>
|
</div>
|
||||||
<button class="profile-avatar__edit">
|
|
||||||
<span>📷</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="profile-status">
|
<div class="member-status-badge">
|
||||||
<span class="profile-status__indicator"></span>
|
<span
|
||||||
<span class="profile-status__text">Online</span>
|
class="status-indicator"
|
||||||
|
:class="`status-indicator--${profile.memberStatus}`"
|
||||||
|
></span>
|
||||||
|
<span class="status-text">{{ profile.memberStatus }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="profile-header__info">
|
<div class="profile-header__info">
|
||||||
<h1 class="profile-name">{{ profile.name }}</h1>
|
<h1 class="profile-name">{{ profile.name }}</h1>
|
||||||
<p class="profile-title">{{ profile.title }} at {{ profile.company }}</p>
|
<p class="profile-title">{{ profile.title }} at {{ profile.company }}</p>
|
||||||
<p class="profile-bio">{{ profile.bio }}</p>
|
<p class="profile-member-id">Member ID: #{{ profile.memberId }}</p>
|
||||||
|
|
||||||
<div class="profile-badges">
|
<div class="profile-badges">
|
||||||
<span class="badge badge--verified">✓ Verified Member</span>
|
<span
|
||||||
<span class="badge badge--board">Board Member</span>
|
class="badge"
|
||||||
<span class="badge badge--year">Member Since {{ profile.memberSince }}</span>
|
:class="`badge--${profile.memberStatus}`"
|
||||||
|
>
|
||||||
|
{{ profile.memberStatus }} Member
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="badge"
|
||||||
|
:class="`badge--${profile.duesStatus}`"
|
||||||
|
>
|
||||||
|
Dues: {{ profile.duesStatus }}
|
||||||
|
</span>
|
||||||
|
<span class="badge badge--year">Joined {{ profile.memberSince }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="profile-header__actions">
|
<div class="profile-header__actions">
|
||||||
<MonacoButton variant="primary" icon="edit">
|
<MonacoButton variant="primary" icon="edit">
|
||||||
Edit Profile
|
Edit Member
|
||||||
</MonacoButton>
|
</MonacoButton>
|
||||||
<MonacoButton variant="glass" icon="share">
|
<MonacoButton variant="glass" icon="mail">
|
||||||
Share
|
Send Email
|
||||||
</MonacoButton>
|
</MonacoButton>
|
||||||
<MonacoButton variant="ghost" icon="settings">
|
<MonacoButton variant="ghost" icon="ban">
|
||||||
Settings
|
Suspend
|
||||||
|
</MonacoButton>
|
||||||
|
<MonacoButton variant="ghost" icon="trash">
|
||||||
|
Remove
|
||||||
</MonacoButton>
|
</MonacoButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats Overview -->
|
<!-- Admin Stats Overview -->
|
||||||
<div
|
<div
|
||||||
v-motion
|
v-motion
|
||||||
:initial="{ opacity: 0, y: 20 }"
|
:initial="{ opacity: 0, y: 20 }"
|
||||||
:enter="{ opacity: 1, y: 0, transition: { delay: 200 } }"
|
:enter="{ opacity: 1, y: 0, transition: { delay: 200 } }"
|
||||||
class="profile-stats"
|
class="profile-stats"
|
||||||
>
|
>
|
||||||
<div class="stat-card" v-for="stat in stats" :key="stat.label">
|
<div class="stat-card" v-for="stat in adminStats" :key="stat.label">
|
||||||
<span class="stat-card__value">{{ stat.value }}</span>
|
<span class="stat-card__value">{{ stat.value }}</span>
|
||||||
<span class="stat-card__label">{{ stat.label }}</span>
|
<span class="stat-card__label">{{ stat.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -116,63 +129,92 @@
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
|
|
||||||
<!-- Skills & Expertise -->
|
<!-- Account Information -->
|
||||||
<GlassCard
|
<GlassCard
|
||||||
title="Skills & Expertise"
|
title="Account Information"
|
||||||
variant="glass"
|
variant="glass"
|
||||||
:delay="400"
|
:delay="400"
|
||||||
>
|
>
|
||||||
<div class="skills-cloud">
|
<div class="account-info">
|
||||||
<span
|
<div class="account-item">
|
||||||
v-for="skill in profile.skills"
|
<span class="account-item__label">Portal Access</span>
|
||||||
:key="skill"
|
<span
|
||||||
class="skill-tag"
|
class="account-item__value"
|
||||||
:class="`skill-tag--${getSkillLevel(skill)}`"
|
:class="{ 'account-item__value--active': profile.hasPortalAccess }"
|
||||||
>
|
>
|
||||||
{{ skill }}
|
{{ profile.hasPortalAccess ? 'Active' : 'No Access' }}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="account-item">
|
||||||
|
<span class="account-item__label">Last Login</span>
|
||||||
|
<span class="account-item__value">{{ profile.lastLogin }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="account-item">
|
||||||
|
<span class="account-item__label">Login Count</span>
|
||||||
|
<span class="account-item__value">{{ profile.loginCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="account-item">
|
||||||
|
<span class="account-item__label">Account Created</span>
|
||||||
|
<span class="account-item__value">{{ profile.accountCreated }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<MonacoButton
|
||||||
|
v-if="!profile.hasPortalAccess"
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Send Portal Invitation
|
||||||
|
</MonacoButton>
|
||||||
|
<MonacoButton
|
||||||
|
v-else
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Reset Password
|
||||||
|
</MonacoButton>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
|
|
||||||
<!-- Membership Details -->
|
<!-- Payment History -->
|
||||||
<GlassCard
|
<GlassCard
|
||||||
title="Membership"
|
title="Payment History"
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
:delay="500"
|
:delay="500"
|
||||||
>
|
>
|
||||||
<div class="membership-details">
|
<div class="payment-history">
|
||||||
<div class="membership-item">
|
<div
|
||||||
<span class="membership-item__label">Status</span>
|
v-for="payment in paymentHistory"
|
||||||
<span class="membership-item__value membership-item__value--active">
|
:key="payment.id"
|
||||||
Active
|
class="payment-item"
|
||||||
|
>
|
||||||
|
<div class="payment-item__date">{{ payment.date }}</div>
|
||||||
|
<div class="payment-item__info">
|
||||||
|
<span class="payment-item__type">{{ payment.type }}</span>
|
||||||
|
<span class="payment-item__amount">${{ payment.amount }}</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="payment-item__status"
|
||||||
|
:class="`payment-item__status--${payment.status}`"
|
||||||
|
>
|
||||||
|
{{ payment.status }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="membership-item">
|
|
||||||
<span class="membership-item__label">Type</span>
|
|
||||||
<span class="membership-item__value">Executive</span>
|
|
||||||
</div>
|
|
||||||
<div class="membership-item">
|
|
||||||
<span class="membership-item__label">Dues</span>
|
|
||||||
<span class="membership-item__value membership-item__value--paid">
|
|
||||||
Paid through 2025
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="membership-item">
|
|
||||||
<span class="membership-item__label">Next Renewal</span>
|
|
||||||
<span class="membership-item__value">January 2025</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<MonacoButton variant="primary" size="sm" block>
|
<MonacoButton variant="primary" size="sm" block>
|
||||||
Manage Membership
|
Record Payment
|
||||||
|
</MonacoButton>
|
||||||
|
<MonacoButton variant="ghost" size="sm" block>
|
||||||
|
Send Invoice
|
||||||
</MonacoButton>
|
</MonacoButton>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Column -->
|
<!-- Main Column -->
|
||||||
<div class="profile-grid__main">
|
<div class="profile-grid__main">
|
||||||
<!-- Activity Timeline -->
|
<!-- Administrative Actions Log -->
|
||||||
<GlassCard
|
<GlassCard
|
||||||
title="Recent Activity"
|
title="Administrative Actions"
|
||||||
variant="glass"
|
variant="glass"
|
||||||
:delay="350"
|
:delay="350"
|
||||||
>
|
>
|
||||||
|
|
@ -203,22 +245,33 @@
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
|
|
||||||
<!-- Achievements -->
|
<!-- Member Notes -->
|
||||||
<GlassCard
|
<GlassCard
|
||||||
title="Achievements"
|
title="Internal Notes"
|
||||||
variant="glass"
|
variant="glass"
|
||||||
:delay="450"
|
:delay="450"
|
||||||
>
|
>
|
||||||
<div class="achievements-grid">
|
<div class="notes-section">
|
||||||
<div
|
<div
|
||||||
v-for="achievement in achievements"
|
v-for="note in memberNotes"
|
||||||
:key="achievement.id"
|
:key="note.id"
|
||||||
class="achievement-card"
|
class="note-item"
|
||||||
>
|
>
|
||||||
<div class="achievement-card__icon">{{ achievement.icon }}</div>
|
<div class="note-item__header">
|
||||||
<h4 class="achievement-card__title">{{ achievement.title }}</h4>
|
<span class="note-item__author">{{ note.author }}</span>
|
||||||
<p class="achievement-card__description">{{ achievement.description }}</p>
|
<span class="note-item__date">{{ note.date }}</span>
|
||||||
<span class="achievement-card__date">{{ achievement.date }}</span>
|
</div>
|
||||||
|
<p class="note-item__content">{{ note.content }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="add-note">
|
||||||
|
<textarea
|
||||||
|
placeholder="Add a new note..."
|
||||||
|
class="note-input"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
<MonacoButton variant="primary" size="sm">
|
||||||
|
Add Note
|
||||||
|
</MonacoButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
|
|
@ -253,60 +306,54 @@
|
||||||
|
|
||||||
<!-- Right Column -->
|
<!-- Right Column -->
|
||||||
<div class="profile-grid__aside">
|
<div class="profile-grid__aside">
|
||||||
<!-- Quick Stats -->
|
<!-- Admin Quick Actions -->
|
||||||
<GlassCard
|
<GlassCard
|
||||||
variant="glass"
|
variant="glass"
|
||||||
:delay="400"
|
:delay="400"
|
||||||
>
|
>
|
||||||
<h3 class="card-title">Network Stats</h3>
|
<h3 class="card-title">Quick Actions</h3>
|
||||||
<div class="quick-stats">
|
<div class="admin-actions">
|
||||||
<div class="quick-stat">
|
<MonacoButton variant="glass" size="sm" icon="mail" block>
|
||||||
<span class="quick-stat__icon">👥</span>
|
Send Email
|
||||||
<span class="quick-stat__value">234</span>
|
</MonacoButton>
|
||||||
<span class="quick-stat__label">Connections</span>
|
<MonacoButton variant="glass" size="sm" icon="dollar" block>
|
||||||
</div>
|
Record Payment
|
||||||
<div class="quick-stat">
|
</MonacoButton>
|
||||||
<span class="quick-stat__icon">📅</span>
|
<MonacoButton variant="glass" size="sm" icon="refresh" block>
|
||||||
<span class="quick-stat__value">45</span>
|
Reset Password
|
||||||
<span class="quick-stat__label">Events</span>
|
</MonacoButton>
|
||||||
</div>
|
<MonacoButton variant="glass" size="sm" icon="user-plus" block>
|
||||||
<div class="quick-stat">
|
Add to Group
|
||||||
<span class="quick-stat__icon">🏆</span>
|
</MonacoButton>
|
||||||
<span class="quick-stat__value">12</span>
|
<MonacoButton variant="primary" size="sm" icon="chart" block>
|
||||||
<span class="quick-stat__label">Awards</span>
|
View Reports
|
||||||
</div>
|
</MonacoButton>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
|
|
||||||
<!-- Connected Members -->
|
<!-- Member Flags & Warnings -->
|
||||||
<GlassCard
|
<GlassCard
|
||||||
title="Connections"
|
title="Flags & Warnings"
|
||||||
variant="glass"
|
variant="glass"
|
||||||
:delay="500"
|
:delay="500"
|
||||||
>
|
>
|
||||||
<div class="connections-list">
|
<div class="warnings-list">
|
||||||
<div
|
<div
|
||||||
v-for="connection in connections"
|
v-for="warning in memberWarnings"
|
||||||
:key="connection.id"
|
:key="warning.id"
|
||||||
class="connection-item"
|
class="warning-item"
|
||||||
|
:class="`warning-item--${warning.severity}`"
|
||||||
>
|
>
|
||||||
<img
|
<span class="warning-item__icon">{{ warning.icon }}</span>
|
||||||
v-if="connection.avatar"
|
<div class="warning-item__content">
|
||||||
:src="connection.avatar"
|
<h5 class="warning-item__title">{{ warning.title }}</h5>
|
||||||
:alt="connection.name"
|
<p class="warning-item__description">{{ warning.description }}</p>
|
||||||
class="connection-item__avatar"
|
<span class="warning-item__date">{{ warning.date }}</span>
|
||||||
/>
|
|
||||||
<div v-else class="connection-item__avatar-placeholder">
|
|
||||||
{{ connection.name.split(' ').map(n => n[0]).join('') }}
|
|
||||||
</div>
|
|
||||||
<div class="connection-item__info">
|
|
||||||
<h5 class="connection-item__name">{{ connection.name }}</h5>
|
|
||||||
<p class="connection-item__title">{{ connection.title }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MonacoButton variant="primary" size="sm" block>
|
<MonacoButton variant="ghost" size="sm" block>
|
||||||
View All Connections
|
Add Flag
|
||||||
</MonacoButton>
|
</MonacoButton>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -323,82 +370,81 @@ const profile = ref({
|
||||||
name: 'Alexandra Martin',
|
name: 'Alexandra Martin',
|
||||||
title: 'CEO & Founder',
|
title: 'CEO & Founder',
|
||||||
company: 'Monaco Ventures',
|
company: 'Monaco Ventures',
|
||||||
bio: 'Passionate about fostering Monaco-US business relationships and cultural exchange. Leading innovation in international commerce.',
|
memberId: 'MCA2021-0234',
|
||||||
avatar: '/api/placeholder/200/200',
|
avatar: '/api/placeholder/200/200',
|
||||||
email: 'alexandra@monacoventures.com',
|
email: 'alexandra@monacoventures.com',
|
||||||
phone: '+1 (555) 123-4567',
|
phone: '+1 (555) 123-4567',
|
||||||
linkedin: 'linkedin.com/in/alexandra-martin',
|
linkedin: 'linkedin.com/in/alexandra-martin',
|
||||||
location: 'Monaco & New York',
|
location: 'Monaco & New York',
|
||||||
memberSince: '2021',
|
memberSince: 'January 2021',
|
||||||
skills: [
|
memberStatus: 'active',
|
||||||
'Leadership', 'Strategy', 'Investment', 'International Business',
|
duesStatus: 'current',
|
||||||
'Networking', 'Public Speaking', 'Venture Capital', 'M&A'
|
hasPortalAccess: true,
|
||||||
]
|
lastLogin: '2 hours ago',
|
||||||
|
loginCount: 234,
|
||||||
|
accountCreated: 'Jan 15, 2021'
|
||||||
})
|
})
|
||||||
|
|
||||||
const initials = computed(() => {
|
const initials = computed(() => {
|
||||||
return profile.value.name.split(' ').map(n => n[0]).join('').toUpperCase()
|
return profile.value.name.split(' ').map(n => n[0]).join('').toUpperCase()
|
||||||
})
|
})
|
||||||
|
|
||||||
const stats = ref([
|
const adminStats = ref([
|
||||||
{ label: 'Years Active', value: '3' },
|
{ label: 'Total Paid', value: '$12,500' },
|
||||||
{ label: 'Events Hosted', value: '24' },
|
{ label: 'Portal Logins', value: '234' },
|
||||||
{ label: 'Connections', value: '234' },
|
{ label: 'Events Attended', value: '18' },
|
||||||
{ label: 'Contributions', value: '156' }
|
{ label: 'Days Overdue', value: '0' }
|
||||||
])
|
])
|
||||||
|
|
||||||
const activities = ref([
|
const activities = ref([
|
||||||
{
|
{
|
||||||
type: 'event',
|
type: 'admin',
|
||||||
icon: '📅',
|
icon: '✏',
|
||||||
title: 'Attended Monaco Winter Gala',
|
title: 'Profile Updated',
|
||||||
description: 'Networked with 50+ members at the annual gala',
|
description: 'Admin Jane Doe updated contact information',
|
||||||
time: '2 days ago'
|
time: '2 days ago'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'achievement',
|
type: 'payment',
|
||||||
icon: '🏆',
|
icon: '💵',
|
||||||
title: 'Earned Top Contributor Badge',
|
title: 'Annual Dues Paid',
|
||||||
description: 'Recognized for outstanding community engagement',
|
description: 'Payment of $2,500 processed successfully',
|
||||||
time: '1 week ago'
|
time: '1 month ago'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'connection',
|
type: 'email',
|
||||||
icon: '🤝',
|
icon: '✉',
|
||||||
title: 'Connected with 5 new members',
|
title: 'Portal Invitation Sent',
|
||||||
description: 'Expanded network in the Tech sector',
|
description: 'Member accepted invitation and logged in',
|
||||||
time: '2 weeks ago'
|
time: '2 months ago'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'post',
|
type: 'status',
|
||||||
icon: '📝',
|
icon: '✅',
|
||||||
title: 'Published article on Monaco innovation',
|
title: 'Status Changed to Active',
|
||||||
description: 'Shared insights on emerging markets',
|
description: 'Member activated after payment verification',
|
||||||
time: '3 weeks ago'
|
time: '3 months ago'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
const achievements = ref([
|
const memberNotes = ref([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
icon: '🌟',
|
author: 'Admin Jane Doe',
|
||||||
title: 'Founding Member',
|
date: 'Dec 1, 2024',
|
||||||
description: 'One of the first 100 members',
|
content: 'VIP member - ensure priority support and invitations to exclusive events.'
|
||||||
date: 'March 2021'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
icon: '🎯',
|
author: 'Admin John Smith',
|
||||||
title: 'Event Champion',
|
date: 'Nov 15, 2024',
|
||||||
description: 'Hosted 10+ successful events',
|
content: 'Requested paper invoices instead of electronic billing. Updated preferences.'
|
||||||
date: 'June 2023'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
icon: '💎',
|
author: 'System',
|
||||||
title: 'Diamond Contributor',
|
date: 'Oct 10, 2024',
|
||||||
description: 'Top 1% activity level',
|
content: 'Automatic renewal scheduled for January 2025.'
|
||||||
date: 'December 2023'
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
@ -408,21 +454,32 @@ const eventsAttended = ref([
|
||||||
{ id: 3, day: '20', month: 'OCT', title: 'Tech Innovation Day', role: 'Panelist' }
|
{ id: 3, day: '20', month: 'OCT', title: 'Tech Innovation Day', role: 'Panelist' }
|
||||||
])
|
])
|
||||||
|
|
||||||
const connections = ref([
|
const paymentHistory = ref([
|
||||||
{ id: 1, name: 'John Doe', title: 'CFO at TechCorp', avatar: '/api/placeholder/40/40' },
|
{ id: 1, date: 'Dec 2024', type: 'Annual Dues', amount: 2500, status: 'paid' },
|
||||||
{ id: 2, name: 'Sarah Smith', title: 'Director at Monaco Bank', avatar: '' },
|
{ id: 2, date: 'Nov 2024', type: 'Event Ticket', amount: 150, status: 'paid' },
|
||||||
{ id: 3, name: 'Marc Blanc', title: 'Partner at Law Firm', avatar: '/api/placeholder/40/40' },
|
{ id: 3, date: 'Oct 2024', type: 'Special Assessment', amount: 500, status: 'paid' },
|
||||||
{ id: 4, name: 'Lisa Chen', title: 'VP Marketing', avatar: '/api/placeholder/40/40' }
|
{ id: 4, date: 'Jan 2024', type: 'Annual Dues', amount: 2500, status: 'paid' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const memberWarnings = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
severity: 'info',
|
||||||
|
icon: 'ℹ',
|
||||||
|
title: 'VIP Member',
|
||||||
|
description: 'Priority support required',
|
||||||
|
date: 'Active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
severity: 'warning',
|
||||||
|
icon: '⚠',
|
||||||
|
title: 'Paper Invoices',
|
||||||
|
description: 'Requires printed billing',
|
||||||
|
date: 'Nov 2024'
|
||||||
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
const getSkillLevel = (skill: string) => {
|
|
||||||
const expertSkills = ['Leadership', 'Strategy', 'Investment']
|
|
||||||
const advancedSkills = ['International Business', 'Networking']
|
|
||||||
|
|
||||||
if (expertSkills.includes(skill)) return 'expert'
|
|
||||||
if (advancedSkills.includes(skill)) return 'advanced'
|
|
||||||
return 'intermediate'
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|
@ -540,7 +597,7 @@ const getSkillLevel = (skill: string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-status {
|
.member-status-badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
@ -548,20 +605,36 @@ const getSkillLevel = (skill: string) => {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
&__indicator {
|
&--active {
|
||||||
width: 0.5rem;
|
|
||||||
height: 0.5rem;
|
|
||||||
background: #10b981;
|
background: #10b981;
|
||||||
border-radius: 50%;
|
|
||||||
animation: pulse 2s infinite;
|
animation: pulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__text {
|
&--inactive {
|
||||||
font-size: 0.875rem;
|
background: #6b7280;
|
||||||
font-weight: 500;
|
|
||||||
color: #10b981;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--suspended {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pending {
|
||||||
|
background: #fb923c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: #27272a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-name {
|
.profile-name {
|
||||||
|
|
@ -578,11 +651,11 @@ const getSkillLevel = (skill: string) => {
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-bio {
|
.profile-member-id {
|
||||||
margin: 0 0 1.5rem;
|
margin: 0 0 1.5rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: #27272a;
|
color: #6b7280;
|
||||||
max-width: 600px;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-badges {
|
.profile-badges {
|
||||||
|
|
@ -597,14 +670,39 @@ const getSkillLevel = (skill: string) => {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
&--verified {
|
&--active {
|
||||||
background: rgba(16, 185, 129, 0.1);
|
background: rgba(16, 185, 129, 0.1);
|
||||||
color: #10b981;
|
color: #10b981;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--board {
|
&--inactive {
|
||||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
background: rgba(107, 114, 128, 0.1);
|
||||||
color: white;
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--suspended {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pending {
|
||||||
|
background: rgba(251, 146, 60, 0.1);
|
||||||
|
color: #fb923c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--current {
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--overdue {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--exempt {
|
||||||
|
background: rgba(107, 114, 128, 0.1);
|
||||||
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--year {
|
&--year {
|
||||||
|
|
@ -710,38 +808,99 @@ const getSkillLevel = (skill: string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.skills-cloud {
|
.account-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skill-tag {
|
.account-item {
|
||||||
padding: 0.5rem 1rem;
|
display: flex;
|
||||||
border-radius: 20px;
|
justify-content: space-between;
|
||||||
font-size: 0.875rem;
|
align-items: center;
|
||||||
font-weight: 500;
|
padding: 0.75rem;
|
||||||
transition: all 0.2s;
|
background: rgba(255, 255, 255, 0.5);
|
||||||
cursor: pointer;
|
border-radius: 8px;
|
||||||
|
|
||||||
&--expert {
|
&__label {
|
||||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
font-size: 0.875rem;
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--advanced {
|
|
||||||
background: rgba(220, 38, 38, 0.15);
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--intermediate {
|
|
||||||
background: rgba(107, 114, 128, 0.1);
|
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&__value {
|
||||||
transform: translateY(-2px);
|
font-size: 0.875rem;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
font-weight: 500;
|
||||||
|
color: #27272a;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-history {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&__date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.125rem;
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__type {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #27272a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__amount {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__status {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: capitalize;
|
||||||
|
|
||||||
|
&--paid {
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pending {
|
||||||
|
background: rgba(251, 146, 60, 0.1);
|
||||||
|
color: #fb923c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--overdue {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -870,51 +1029,63 @@ const getSkillLevel = (skill: string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.achievements-grid {
|
.notes-section {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.achievement-card {
|
.note-item {
|
||||||
display: flex;
|
padding: 1rem;
|
||||||
flex-direction: column;
|
background: rgba(255, 255, 255, 0.5);
|
||||||
align-items: center;
|
border-radius: 8px;
|
||||||
padding: 1.5rem;
|
border-left: 3px solid #dc2626;
|
||||||
background: linear-gradient(135deg,
|
|
||||||
rgba(220, 38, 38, 0.05) 0%,
|
|
||||||
rgba(220, 38, 38, 0.02) 100%);
|
|
||||||
border-radius: 12px;
|
|
||||||
text-align: center;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
&__header {
|
||||||
transform: translateY(-4px);
|
display: flex;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
justify-content: space-between;
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__author {
|
||||||
margin: 0 0 0.25rem;
|
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #27272a;
|
color: #dc2626;
|
||||||
}
|
|
||||||
|
|
||||||
&__description {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__date {
|
&__date {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #dc2626;
|
color: #6b7280;
|
||||||
font-weight: 500;
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #27272a;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-note {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 2px solid rgba(220, 38, 38, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #dc2626;
|
||||||
|
background: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -987,36 +1158,78 @@ const getSkillLevel = (skill: string) => {
|
||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-stats {
|
.admin-actions {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-stat {
|
.warnings-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 0.75rem;
|
||||||
padding: 1rem 0.5rem;
|
margin-bottom: 1rem;
|
||||||
background: rgba(255, 255, 255, 0.5);
|
}
|
||||||
|
|
||||||
|
.warning-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
border-left: 3px solid;
|
||||||
|
|
||||||
|
&--info {
|
||||||
|
background: rgba(59, 130, 246, 0.05);
|
||||||
|
border-color: #3b82f6;
|
||||||
|
|
||||||
|
.warning-item__icon {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--warning {
|
||||||
|
background: rgba(251, 146, 60, 0.05);
|
||||||
|
border-color: #fb923c;
|
||||||
|
|
||||||
|
.warning-item__icon {
|
||||||
|
color: #fb923c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
background: rgba(239, 68, 68, 0.05);
|
||||||
|
border-color: #ef4444;
|
||||||
|
|
||||||
|
.warning-item__icon {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__value {
|
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 700;
|
flex-shrink: 0;
|
||||||
color: #dc2626;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__label {
|
&__content {
|
||||||
font-size: 0.625rem;
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #27272a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
text-align: center;
|
}
|
||||||
|
|
||||||
|
&__date {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
color: #a3a3a3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue