feat: Reorganize platform into member, board, and admin sections
Some checks failed
Build And Push Image / docker (push) Failing after 55s

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:
2025-08-30 22:00:59 +02:00
parent 27e38d98e5
commit d9d8627e97
15 changed files with 6039 additions and 449 deletions

View File

@@ -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>

View File

@@ -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>