574 lines
16 KiB
Vue
574 lines
16 KiB
Vue
<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' }
|
|
];
|
|
|
|
// Real members data from API
|
|
const members = ref([]);
|
|
|
|
// 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]
|
|
};
|
|
};
|
|
|
|
// Load real members data from API
|
|
const loadMembers = async () => {
|
|
loading.value = true;
|
|
try {
|
|
// Fetch members from API
|
|
const { data } = await $fetch('/api/members');
|
|
|
|
if (data?.members) {
|
|
// Transform the data to match our interface
|
|
members.value = data.members.map((member: any) => ({
|
|
id: member.Id || member.id,
|
|
memberId: member.member_id || `MUSA-${String(member.Id).padStart(4, '0')}`,
|
|
firstName: member.first_name,
|
|
lastName: member.last_name,
|
|
email: member.email,
|
|
phone: member.phone_number || member.phone || '',
|
|
status: member.membership_status === 'Active' ? 'Active' : 'Inactive',
|
|
duesStatus: member.dues_status || 'Unknown',
|
|
memberType: member.membership_type || 'Regular',
|
|
joinDate: member.member_since || member.created_at,
|
|
nationality: member.nationality || member.country || ''
|
|
}));
|
|
|
|
console.log(`[board-members] Loaded ${members.value.length} members from API`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading members:', error);
|
|
// Keep empty array if load fails
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Load data on mount
|
|
onMounted(async () => {
|
|
await loadMembers();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.gap-1 {
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.gap-2 {
|
|
gap: 0.5rem;
|
|
}
|
|
</style> |