monacousa-portal/pages/dashboard/member-list.vue

467 lines
13 KiB
Vue

<template>
<v-container fluid>
<!-- Header -->
<v-row class="mb-4">
<v-col>
<h1 class="text-h4 font-weight-bold mb-4">
<v-icon left>mdi-account-multiple</v-icon>
Welcome Back, {{ firstName }}
</h1>
<p class="text-body-1 mb-4">
Manage MonacoUSA association members and their information.
</p>
</v-col>
</v-row>
<!-- Search and Filter Controls -->
<v-row class="mb-4">
<v-col cols="12" md="4">
<v-text-field
v-model="searchTerm"
label="Search members..."
prepend-inner-icon="mdi-magnify"
variant="outlined"
clearable
@input="debouncedSearch"
/>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="nationalityFilter"
:items="nationalityOptions"
label="Nationality"
variant="outlined"
clearable
@update:model-value="filterMembers"
>
<template #selection="{ item }">
<CountryFlag :country-code="item.raw.code" :show-name="true" size="small" />
</template>
<template #item="{ props, item }">
<v-list-item v-bind="props">
<template #prepend>
<CountryFlag :country-code="item.raw.code" :show-name="false" size="small" />
</template>
<v-list-item-title>{{ item.raw.name }}</v-list-item-title>
</v-list-item>
</template>
</v-select>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="statusFilter"
:items="statusOptions"
label="Membership Status"
variant="outlined"
clearable
@update:model-value="filterMembers"
/>
</v-col>
<v-col cols="12" md="2">
<v-btn
color="primary"
block
size="large"
@click="showAddDialog = true"
:disabled="!canCreateMembers"
>
<v-icon start>mdi-plus</v-icon>
Add Member
</v-btn>
</v-col>
</v-row>
<!-- Member Statistics -->
<v-row class="mb-6">
<v-col cols="12" md="3">
<v-card elevation="2">
<v-card-text>
<div class="text-h6 text-primary font-weight-bold">{{ totalMembers }}</div>
<div class="text-body-2 text-medium-emphasis">Total Members</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card elevation="2">
<v-card-text>
<div class="text-h6 text-success font-weight-bold">{{ activeMembers }}</div>
<div class="text-body-2 text-medium-emphasis">Active Members</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card elevation="2">
<v-card-text>
<div class="text-h6 text-success font-weight-bold">{{ paidDuesMembers }}</div>
<div class="text-body-2 text-medium-emphasis">Paid Dues</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card elevation="2">
<v-card-text>
<div class="text-h6 font-weight-bold">{{ uniqueNationalities }}</div>
<div class="text-body-2 text-medium-emphasis">Countries</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Loading State -->
<v-row v-if="loading" justify="center" class="my-12">
<v-col cols="auto" class="text-center">
<v-progress-circular indeterminate color="primary" size="64" />
<p class="mt-4 text-h6">Loading members...</p>
</v-col>
</v-row>
<!-- Error State -->
<v-alert
v-else-if="error"
type="error"
variant="tonal"
class="mb-4"
closable
@click:close="error = ''"
>
<template #title>Failed to load members</template>
{{ error }}
<template #append>
<v-btn color="error" variant="text" @click="loadMembers">
Try Again
</v-btn>
</template>
</v-alert>
<!-- Members Grid -->
<v-row v-else>
<v-col
v-for="member in filteredMembers"
:key="member.Id"
cols="12"
sm="6"
md="4"
lg="3"
>
<MemberCard
:member="member"
@edit="editMember"
@delete="confirmDeleteMember"
@view="viewMember"
:can-edit="canEditMembers"
:can-delete="canDeleteMembers"
/>
</v-col>
<!-- No Results State -->
<v-col v-if="filteredMembers.length === 0 && !loading && !error" cols="12" class="text-center">
<v-card elevation="0" class="pa-8">
<v-icon size="64" color="grey-lighten-1" class="mb-4">mdi-account-search</v-icon>
<h3 class="text-h5 mb-2">No members found</h3>
<p class="text-body-1 mb-4">
{{ searchTerm || nationalityFilter || statusFilter
? 'Try adjusting your filters to find members.'
: 'No members have been added yet.' }}
</p>
<v-btn
v-if="canCreateMembers && !searchTerm && !nationalityFilter && !statusFilter"
color="primary"
@click="showAddDialog = true"
>
<v-icon start>mdi-plus</v-icon>
Add First Member
</v-btn>
</v-card>
</v-col>
</v-row>
<!-- Add Member Dialog -->
<AddMemberDialog
v-model="showAddDialog"
@member-created="handleMemberCreated"
/>
<!-- Edit Member Dialog -->
<EditMemberDialog
v-model="showEditDialog"
:member="selectedMember"
@member-updated="handleMemberUpdated"
/>
<!-- View Member Dialog -->
<ViewMemberDialog
v-model="showViewDialog"
:member="selectedMember"
/>
<!-- Delete Confirmation Dialog -->
<v-dialog v-model="showDeleteDialog" max-width="400">
<v-card>
<v-card-title class="text-h6">
<v-icon color="error" class="mr-2">mdi-delete-alert</v-icon>
Confirm Delete
</v-card-title>
<v-card-text>
Are you sure you want to delete <strong>{{ selectedMember?.FullName }}</strong>?
<br><br>
<v-alert type="warning" variant="tonal" class="mt-2">
This action cannot be undone.
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showDeleteDialog = false" variant="text">
Cancel
</v-btn>
<v-btn
color="error"
@click="deleteMember"
:loading="deleteLoading"
>
<v-icon start>mdi-delete</v-icon>
Delete
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Success Snackbar -->
<v-snackbar
v-model="showSuccess"
:timeout="4000"
color="success"
location="top"
>
{{ successMessage }}
<template #actions>
<v-btn variant="text" @click="showSuccess = false">
Close
</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
<script setup lang="ts">
import type { Member, MembershipStatus } from '~/utils/types';
import { getAllCountries, searchCountries } from '~/utils/countries';
definePageMeta({
layout: 'dashboard',
middleware: 'auth-board'
});
// Auth and permissions
const { firstName, isBoard, isAdmin } = useAuth();
const canCreateMembers = computed(() => isBoard.value || isAdmin.value);
const canEditMembers = computed(() => isBoard.value || isAdmin.value);
const canDeleteMembers = computed(() => isAdmin.value);
// Reactive data
const members = ref<Member[]>([]);
const loading = ref(true);
const error = ref('');
// Search and filtering
const searchTerm = ref('');
const nationalityFilter = ref('');
const statusFilter = ref('');
// Dialogs
const showAddDialog = ref(false);
const showEditDialog = ref(false);
const showViewDialog = ref(false);
const showDeleteDialog = ref(false);
const selectedMember = ref<Member | null>(null);
const deleteLoading = ref(false);
// Success handling
const showSuccess = ref(false);
const successMessage = ref('');
// Filter options
const statusOptions = [
{ title: 'Active', value: 'Active' },
{ title: 'Inactive', value: 'Inactive' },
{ title: 'Pending', value: 'Pending' },
{ title: 'Expired', value: 'Expired' }
];
const nationalityOptions = computed(() => {
const countries = getAllCountries();
return countries.map(country => ({
title: country.name,
value: country.code,
code: country.code,
name: country.name
}));
});
// Computed properties
const filteredMembers = computed(() => {
let filtered = [...members.value];
// Search filter
if (searchTerm.value) {
const search = searchTerm.value.toLowerCase();
filtered = filtered.filter(member =>
member.FullName?.toLowerCase().includes(search) ||
member.Email?.toLowerCase().includes(search) ||
member.Phone?.includes(search)
);
}
// Nationality filter
if (nationalityFilter.value) {
filtered = filtered.filter(member =>
member.Nationality === nationalityFilter.value
);
}
// Status filter
if (statusFilter.value) {
filtered = filtered.filter(member =>
member['Membership Status'] === statusFilter.value
);
}
return filtered;
});
const totalMembers = computed(() => members.value.length);
const activeMembers = computed(() =>
members.value.filter(m => m['Membership Status'] === 'Active').length
);
const paidDuesMembers = computed(() =>
members.value.filter(m => m['Current Year Dues Paid'] === 'true').length
);
const uniqueNationalities = computed(() => {
const nationalities = new Set(members.value.map(m => m.Nationality).filter(Boolean));
return nationalities.size;
});
// Methods
const loadMembers = async () => {
loading.value = true;
error.value = '';
try {
const response = await $fetch<{ success: boolean; data: { list: Member[] } }>('/api/members');
if (response.success) {
members.value = response.data.list || [];
} else {
throw new Error('Failed to load members');
}
} catch (err: any) {
console.error('Error loading members:', err);
error.value = err.message || 'Failed to load members. Please try again.';
} finally {
loading.value = false;
}
};
// Simple debounce function
const debouncedSearch = (() => {
let timeout: NodeJS.Timeout;
return () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
// Search happens automatically via computed
}, 300);
};
})();
const filterMembers = () => {
// Filtering happens automatically via computed
};
const viewMember = (member: Member) => {
selectedMember.value = member;
showViewDialog.value = true;
};
const editMember = (member: Member) => {
selectedMember.value = member;
showEditDialog.value = true;
};
const confirmDeleteMember = (member: Member) => {
selectedMember.value = member;
showDeleteDialog.value = true;
};
const deleteMember = async () => {
if (!selectedMember.value) return;
deleteLoading.value = true;
try {
const response = await $fetch<{ success: boolean; message?: string }>(`/api/members/${selectedMember.value.Id}`, {
method: 'DELETE'
});
if (response.success) {
// Remove from local array
const index = members.value.findIndex(m => m.Id === selectedMember.value?.Id);
if (index !== -1) {
members.value.splice(index, 1);
}
showSuccess.value = true;
successMessage.value = `${selectedMember.value.FullName} has been deleted successfully.`;
showDeleteDialog.value = false;
selectedMember.value = null;
}
} catch (err: any) {
console.error('Error deleting member:', err);
error.value = err.message || 'Failed to delete member. Please try again.';
} finally {
deleteLoading.value = false;
}
};
const handleMemberCreated = (newMember: Member) => {
members.value.unshift(newMember);
showSuccess.value = true;
successMessage.value = `${newMember.FullName} has been added successfully.`;
};
const handleMemberUpdated = (updatedMember: Member) => {
const index = members.value.findIndex(m => m.Id === updatedMember.Id);
if (index !== -1) {
members.value[index] = updatedMember;
}
showSuccess.value = true;
successMessage.value = `${updatedMember.FullName} has been updated successfully.`;
};
// Load members on mount
onMounted(() => {
loadMembers();
});
</script>
<style scoped>
.v-card {
border-radius: 12px !important;
transition: all 0.3s ease;
}
.v-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important;
}
.member-grid {
min-height: 400px;
}
.text-primary {
color: #a31515 !important;
}
</style>