2025-08-07 19:20:29 +02:00
|
|
|
<template>
|
2025-08-13 14:02:29 +02:00
|
|
|
<v-container fluid class="pa-4">
|
2025-08-11 15:29:42 +02:00
|
|
|
<!-- Dues Payment Banner -->
|
|
|
|
|
<DuesPaymentBanner />
|
|
|
|
|
|
2025-08-07 19:20:29 +02:00
|
|
|
<!-- 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>
|
|
|
|
|
|
2025-08-10 23:29:48 +02:00
|
|
|
|
2025-08-07 19:20:29 +02:00
|
|
|
<!-- Search and Filter Controls -->
|
|
|
|
|
<v-row class="mb-4">
|
2025-08-07 22:34:51 +02:00
|
|
|
<v-col cols="12" md="2">
|
2025-08-07 19:20:29 +02:00
|
|
|
<v-text-field
|
|
|
|
|
v-model="searchTerm"
|
|
|
|
|
label="Search members..."
|
|
|
|
|
prepend-inner-icon="mdi-magnify"
|
|
|
|
|
variant="outlined"
|
|
|
|
|
clearable
|
|
|
|
|
@input="debouncedSearch"
|
|
|
|
|
/>
|
|
|
|
|
</v-col>
|
|
|
|
|
|
2025-08-07 22:34:51 +02:00
|
|
|
<v-col cols="12" md="2">
|
|
|
|
|
<v-select
|
|
|
|
|
v-model="activeFilter"
|
|
|
|
|
:items="activeFilterOptions"
|
|
|
|
|
label="Member Status"
|
|
|
|
|
variant="outlined"
|
|
|
|
|
clearable
|
|
|
|
|
prepend-inner-icon="mdi-account-check"
|
|
|
|
|
/>
|
|
|
|
|
</v-col>
|
|
|
|
|
|
2025-08-07 19:20:29 +02:00
|
|
|
|
2025-08-07 22:34:51 +02:00
|
|
|
<v-col cols="12" md="2">
|
|
|
|
|
<v-select
|
|
|
|
|
v-model="duesFilter"
|
|
|
|
|
:items="duesFilterOptions"
|
|
|
|
|
label="Dues Status"
|
|
|
|
|
variant="outlined"
|
|
|
|
|
clearable
|
|
|
|
|
prepend-inner-icon="mdi-cash"
|
|
|
|
|
/>
|
|
|
|
|
</v-col>
|
|
|
|
|
|
|
|
|
|
<v-col cols="12" md="2">
|
2025-08-07 19:20:29 +02:00
|
|
|
<v-select
|
2025-08-07 21:09:00 +02:00
|
|
|
v-model="sortOption"
|
|
|
|
|
:items="sortOptions"
|
|
|
|
|
label="Sort By"
|
2025-08-07 19:20:29 +02:00
|
|
|
variant="outlined"
|
2025-08-07 21:09:00 +02:00
|
|
|
prepend-inner-icon="mdi-sort"
|
2025-08-07 19:20:29 +02:00
|
|
|
/>
|
|
|
|
|
</v-col>
|
|
|
|
|
|
2025-08-07 22:34:51 +02:00
|
|
|
<v-col cols="12" md="2">
|
2025-08-07 19:20:29 +02:00
|
|
|
<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"
|
2025-08-13 14:02:29 +02:00
|
|
|
md="6"
|
|
|
|
|
lg="4"
|
|
|
|
|
xl="3"
|
2025-08-07 19:20:29 +02:00
|
|
|
>
|
|
|
|
|
<MemberCard
|
|
|
|
|
:member="member"
|
|
|
|
|
@edit="editMember"
|
|
|
|
|
@delete="confirmDeleteMember"
|
|
|
|
|
@view="viewMember"
|
2025-08-08 20:07:47 +02:00
|
|
|
@create-portal-account="createPortalAccount"
|
2025-08-07 19:20:29 +02:00
|
|
|
:can-edit="canEditMembers"
|
|
|
|
|
:can-delete="canDeleteMembers"
|
2025-08-08 20:07:47 +02:00
|
|
|
:can-create-portal-account="canCreatePortalAccounts"
|
|
|
|
|
:creating-portal-account="creatingPortalAccountIds.includes(member.Id)"
|
2025-08-07 19:20:29 +02:00
|
|
|
/>
|
|
|
|
|
</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">
|
2025-08-08 00:25:44 +02:00
|
|
|
{{ searchTerm
|
2025-08-07 19:20:29 +02:00
|
|
|
? 'Try adjusting your filters to find members.'
|
|
|
|
|
: 'No members have been added yet.' }}
|
|
|
|
|
</p>
|
|
|
|
|
<v-btn
|
2025-08-08 00:25:44 +02:00
|
|
|
v-if="canCreateMembers && !searchTerm"
|
2025-08-07 19:20:29 +02:00
|
|
|
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"
|
2025-08-10 23:19:48 +02:00
|
|
|
@edit="editMember"
|
2025-08-07 19:20:29 +02:00
|
|
|
/>
|
|
|
|
|
|
2025-08-13 18:55:49 +02:00
|
|
|
<!-- Create Portal Account Dialog -->
|
|
|
|
|
<CreatePortalAccountDialog
|
|
|
|
|
v-model="showCreatePortalAccountDialog"
|
|
|
|
|
:member="selectedMemberForPortalAccount"
|
|
|
|
|
@account-created="handlePortalAccountCreated"
|
|
|
|
|
/>
|
|
|
|
|
|
2025-08-07 19:20:29 +02:00
|
|
|
<!-- 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);
|
2025-08-08 20:07:47 +02:00
|
|
|
const canCreatePortalAccounts = computed(() => isAdmin.value); // Only admins can create portal accounts
|
2025-08-07 19:20:29 +02:00
|
|
|
|
|
|
|
|
// Reactive data
|
|
|
|
|
const members = ref<Member[]>([]);
|
|
|
|
|
const loading = ref(true);
|
|
|
|
|
const error = ref('');
|
|
|
|
|
|
|
|
|
|
// Search and filtering
|
|
|
|
|
const searchTerm = ref('');
|
2025-08-07 22:34:51 +02:00
|
|
|
const activeFilter = ref('');
|
|
|
|
|
const duesFilter = ref('');
|
2025-08-07 21:09:00 +02:00
|
|
|
const sortOption = ref('name-asc');
|
2025-08-07 19:20:29 +02:00
|
|
|
|
|
|
|
|
// Dialogs
|
|
|
|
|
const showAddDialog = ref(false);
|
|
|
|
|
const showEditDialog = ref(false);
|
|
|
|
|
const showViewDialog = ref(false);
|
|
|
|
|
const showDeleteDialog = ref(false);
|
2025-08-13 18:55:49 +02:00
|
|
|
const showCreatePortalAccountDialog = ref(false);
|
2025-08-07 19:20:29 +02:00
|
|
|
const selectedMember = ref<Member | null>(null);
|
2025-08-13 18:55:49 +02:00
|
|
|
const selectedMemberForPortalAccount = ref<Member | null>(null);
|
2025-08-07 19:20:29 +02:00
|
|
|
const deleteLoading = ref(false);
|
|
|
|
|
|
|
|
|
|
// Success handling
|
|
|
|
|
const showSuccess = ref(false);
|
|
|
|
|
const successMessage = ref('');
|
|
|
|
|
|
2025-08-08 20:07:47 +02:00
|
|
|
// Portal account creation
|
|
|
|
|
const creatingPortalAccountIds = ref<string[]>([]);
|
|
|
|
|
|
2025-08-10 23:29:48 +02:00
|
|
|
// Overdue dues management
|
|
|
|
|
const overdueCount = ref(0);
|
|
|
|
|
const overdueRefreshTrigger = ref(0);
|
|
|
|
|
|
2025-08-07 19:20:29 +02:00
|
|
|
// Filter options
|
2025-08-07 22:34:51 +02:00
|
|
|
const activeFilterOptions = [
|
|
|
|
|
{ title: 'Active Members', value: 'active' },
|
|
|
|
|
{ title: 'Inactive Members', value: 'inactive' }
|
|
|
|
|
];
|
|
|
|
|
|
2025-08-07 23:13:31 +02:00
|
|
|
const membershipLevelOptions = [
|
|
|
|
|
{ title: 'Regular Member', value: 'regular' },
|
|
|
|
|
{ title: 'Board Member', value: 'board' },
|
|
|
|
|
{ title: 'Honorary Member', value: 'honorary' },
|
|
|
|
|
{ title: 'New Member', value: 'new' },
|
|
|
|
|
{ title: 'Delinquent Member', value: 'delinquent' }
|
2025-08-07 19:20:29 +02:00
|
|
|
];
|
|
|
|
|
|
2025-08-07 22:34:51 +02:00
|
|
|
const duesFilterOptions = [
|
|
|
|
|
{ title: 'Dues Paid', value: 'paid' },
|
|
|
|
|
{ title: 'Dues Outstanding', value: 'unpaid' }
|
|
|
|
|
];
|
|
|
|
|
|
2025-08-07 21:09:00 +02:00
|
|
|
// Sort options
|
|
|
|
|
const sortOptions = [
|
|
|
|
|
{ title: 'Name (A-Z)', value: 'name-asc' },
|
|
|
|
|
{ title: 'Name (Z-A)', value: 'name-desc' },
|
|
|
|
|
{ title: 'Nationality (A-Z)', value: 'nationality-asc' },
|
|
|
|
|
{ title: 'Nationality (Z-A)', value: 'nationality-desc' }
|
|
|
|
|
];
|
2025-08-07 19:20:29 +02:00
|
|
|
|
|
|
|
|
// 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) ||
|
2025-08-07 22:34:51 +02:00
|
|
|
member.email?.toLowerCase().includes(search) ||
|
2025-08-10 23:19:48 +02:00
|
|
|
member.phone?.includes(search) ||
|
|
|
|
|
member.member_id?.toLowerCase().includes(search) ||
|
|
|
|
|
`MUSA-${member.Id}`.toLowerCase().includes(search) // Search by generated member ID format
|
2025-08-07 19:20:29 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-07 22:34:51 +02:00
|
|
|
// Active/Inactive filter
|
|
|
|
|
if (activeFilter.value) {
|
|
|
|
|
if (activeFilter.value === 'active') {
|
|
|
|
|
filtered = filtered.filter(member => member.membership_status === 'Active');
|
|
|
|
|
} else if (activeFilter.value === 'inactive') {
|
|
|
|
|
filtered = filtered.filter(member => member.membership_status !== 'Active');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-07 19:20:29 +02:00
|
|
|
|
2025-08-07 22:34:51 +02:00
|
|
|
// Dues filter
|
|
|
|
|
if (duesFilter.value) {
|
|
|
|
|
if (duesFilter.value === 'paid') {
|
|
|
|
|
filtered = filtered.filter(member => member.current_year_dues_paid === 'true');
|
|
|
|
|
} else if (duesFilter.value === 'unpaid') {
|
|
|
|
|
filtered = filtered.filter(member => member.current_year_dues_paid !== 'true');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-07 21:09:00 +02:00
|
|
|
// Sorting
|
|
|
|
|
if (sortOption.value) {
|
|
|
|
|
filtered.sort((a, b) => {
|
|
|
|
|
switch (sortOption.value) {
|
|
|
|
|
case 'name-asc':
|
|
|
|
|
return (a.FullName || '').localeCompare(b.FullName || '');
|
|
|
|
|
case 'name-desc':
|
|
|
|
|
return (b.FullName || '').localeCompare(a.FullName || '');
|
|
|
|
|
case 'nationality-asc':
|
2025-08-07 22:34:51 +02:00
|
|
|
return (a.nationality || '').localeCompare(b.nationality || '');
|
2025-08-07 21:09:00 +02:00
|
|
|
case 'nationality-desc':
|
2025-08-07 22:34:51 +02:00
|
|
|
return (b.nationality || '').localeCompare(a.nationality || '');
|
2025-08-07 21:09:00 +02:00
|
|
|
default:
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-07 19:20:29 +02:00
|
|
|
return filtered;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const totalMembers = computed(() => members.value.length);
|
2025-08-07 22:53:45 +02:00
|
|
|
const activeMembers = computed(() => {
|
|
|
|
|
// Temporary debug logging
|
|
|
|
|
console.log('Members data for active count:');
|
|
|
|
|
members.value.forEach((m, i) => {
|
|
|
|
|
if (i < 5) { // Only log first 5 to avoid spam
|
|
|
|
|
console.log(`${m.FullName}: status="${m.membership_status}", type=${typeof m.membership_status}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const activeCount = members.value.filter(m => m.membership_status === 'Active').length;
|
|
|
|
|
console.log(`Active members count: ${activeCount} out of ${members.value.length} total`);
|
|
|
|
|
return activeCount;
|
|
|
|
|
});
|
2025-08-07 19:20:29 +02:00
|
|
|
const paidDuesMembers = computed(() =>
|
2025-08-07 22:34:51 +02:00
|
|
|
members.value.filter(m => m.current_year_dues_paid === 'true').length
|
2025-08-07 19:20:29 +02:00
|
|
|
);
|
|
|
|
|
const uniqueNationalities = computed(() => {
|
2025-08-07 22:34:51 +02:00
|
|
|
const nationalities = new Set(members.value.map(m => m.nationality).filter(Boolean));
|
2025-08-07 19:20:29 +02:00
|
|
|
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 || [];
|
2025-08-07 23:57:18 +02:00
|
|
|
|
|
|
|
|
// DIAGNOSTIC: Log what we received from API
|
|
|
|
|
console.log('[member-list] Received response from API:', response);
|
|
|
|
|
console.log('[member-list] Members count:', members.value.length);
|
|
|
|
|
if (members.value.length > 0) {
|
|
|
|
|
const sampleMember = members.value[0];
|
|
|
|
|
console.log('[member-list] DIAGNOSTIC - Sample member from API:', JSON.stringify(sampleMember, null, 2));
|
|
|
|
|
console.log('[member-list] DIAGNOSTIC - Sample member fields:', Object.keys(sampleMember));
|
|
|
|
|
console.log('[member-list] DIAGNOSTIC - Sample FullName:', `"${sampleMember.FullName}"`);
|
|
|
|
|
console.log('[member-list] DIAGNOSTIC - Sample first_name:', `"${sampleMember.first_name}"`);
|
|
|
|
|
console.log('[member-list] DIAGNOSTIC - Sample last_name:', `"${sampleMember.last_name}"`);
|
|
|
|
|
}
|
2025-08-07 19:20:29 +02:00
|
|
|
} 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) => {
|
2025-08-14 15:08:40 +02:00
|
|
|
console.log('[member-list] =====================================');
|
2025-08-13 22:43:40 +02:00
|
|
|
console.log('[member-list] handleMemberCreated called with:', JSON.stringify(newMember, null, 2));
|
|
|
|
|
console.log('[member-list] newMember fields:', Object.keys(newMember));
|
|
|
|
|
console.log('[member-list] FullName value:', `"${newMember.FullName}"`);
|
|
|
|
|
console.log('[member-list] first_name value:', `"${newMember.first_name}"`);
|
|
|
|
|
console.log('[member-list] last_name value:', `"${newMember.last_name}"`);
|
2025-08-14 15:08:40 +02:00
|
|
|
console.log('[member-list] nationality value:', `"${newMember.nationality}"`);
|
|
|
|
|
console.log('[member-list] email value:', `"${newMember.email}"`);
|
|
|
|
|
console.log('[member-list] member_id value:', `"${newMember.member_id}"`);
|
|
|
|
|
console.log('[member-list] membership_status value:', `"${newMember.membership_status}"`);
|
2025-08-13 22:43:40 +02:00
|
|
|
|
2025-08-14 15:08:40 +02:00
|
|
|
// ADVANCED DEBUGGING: Check if data is actually missing
|
|
|
|
|
const hasFirstName = !!(newMember.first_name && newMember.first_name.trim());
|
|
|
|
|
const hasLastName = !!(newMember.last_name && newMember.last_name.trim());
|
|
|
|
|
const hasFullName = !!(newMember.FullName && newMember.FullName.trim());
|
|
|
|
|
|
|
|
|
|
console.log('[member-list] Data validation:');
|
|
|
|
|
console.log(' - hasFirstName:', hasFirstName);
|
|
|
|
|
console.log(' - hasLastName:', hasLastName);
|
|
|
|
|
console.log(' - hasFullName:', hasFullName);
|
|
|
|
|
|
|
|
|
|
// If the API response is missing data, refresh the entire member list instead
|
|
|
|
|
if (!hasFirstName || !hasLastName || !hasFullName) {
|
|
|
|
|
console.error('[member-list] ❌ API response missing critical member data, refreshing member list...');
|
|
|
|
|
loadMembers();
|
|
|
|
|
showSuccess.value = true;
|
|
|
|
|
successMessage.value = 'Member created successfully. Refreshing member list...';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate FullName with robust fallback
|
2025-08-13 22:43:40 +02:00
|
|
|
const fullName = newMember.FullName ||
|
|
|
|
|
`${newMember.first_name || ''} ${newMember.last_name || ''}`.trim() ||
|
|
|
|
|
'New Member';
|
|
|
|
|
|
|
|
|
|
console.log('[member-list] Calculated FullName:', `"${fullName}"`);
|
|
|
|
|
|
2025-08-14 15:08:40 +02:00
|
|
|
// Ensure the member has complete data for display
|
|
|
|
|
const memberWithCompleteData = {
|
2025-08-13 22:43:40 +02:00
|
|
|
...newMember,
|
2025-08-14 15:08:40 +02:00
|
|
|
FullName: fullName,
|
|
|
|
|
// Ensure all required fields are present
|
|
|
|
|
first_name: newMember.first_name || '',
|
|
|
|
|
last_name: newMember.last_name || '',
|
|
|
|
|
nationality: newMember.nationality || '',
|
|
|
|
|
email: newMember.email || '',
|
|
|
|
|
membership_status: newMember.membership_status || 'Active'
|
2025-08-13 22:43:40 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-14 15:08:40 +02:00
|
|
|
console.log('[member-list] Final member data:', JSON.stringify(memberWithCompleteData, null, 2));
|
|
|
|
|
console.log('[member-list] Adding member to beginning of list...');
|
|
|
|
|
|
|
|
|
|
members.value.unshift(memberWithCompleteData);
|
2025-08-07 19:20:29 +02:00
|
|
|
showSuccess.value = true;
|
2025-08-13 22:43:40 +02:00
|
|
|
successMessage.value = `${fullName} has been added successfully.`;
|
2025-08-14 15:08:40 +02:00
|
|
|
|
|
|
|
|
console.log('[member-list] ✅ Member added to local list, total count:', members.value.length);
|
|
|
|
|
console.log('[member-list] =====================================');
|
2025-08-07 19:20:29 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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.`;
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-13 18:55:49 +02:00
|
|
|
const createPortalAccount = (member: Member) => {
|
|
|
|
|
selectedMemberForPortalAccount.value = member;
|
|
|
|
|
showCreatePortalAccountDialog.value = true;
|
|
|
|
|
};
|
2025-08-08 20:07:47 +02:00
|
|
|
|
2025-08-13 18:55:49 +02:00
|
|
|
const handlePortalAccountCreated = (updatedMember: Member) => {
|
|
|
|
|
// Update the member in the local array to reflect the new keycloak_id
|
|
|
|
|
const index = members.value.findIndex(m => m.Id === updatedMember.Id);
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
members.value[index] = updatedMember;
|
2025-08-08 20:07:47 +02:00
|
|
|
}
|
2025-08-13 18:55:49 +02:00
|
|
|
|
|
|
|
|
showSuccess.value = true;
|
|
|
|
|
successMessage.value = `Portal account created successfully for ${updatedMember.FullName}.`;
|
2025-08-08 20:07:47 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-10 23:29:48 +02:00
|
|
|
// Overdue dues handlers
|
|
|
|
|
const loadOverdueCount = async () => {
|
|
|
|
|
try {
|
2025-08-10 23:42:17 +02:00
|
|
|
const response = await $fetch<{
|
|
|
|
|
success: boolean;
|
|
|
|
|
data: {
|
|
|
|
|
count: number;
|
|
|
|
|
overdueMembers: any[];
|
|
|
|
|
}
|
|
|
|
|
}>('/api/members/overdue-count');
|
2025-08-10 23:29:48 +02:00
|
|
|
if (response.success) {
|
|
|
|
|
overdueCount.value = response.data.count;
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error('Error loading overdue count:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const viewOverdueMembers = () => {
|
|
|
|
|
// Filter to show only inactive members (who were marked inactive due to overdue dues)
|
|
|
|
|
activeFilter.value = 'inactive';
|
|
|
|
|
duesFilter.value = 'unpaid';
|
|
|
|
|
|
|
|
|
|
showSuccess.value = true;
|
|
|
|
|
successMessage.value = 'Showing members with overdue dues (marked as inactive)';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const sendDuesReminders = () => {
|
|
|
|
|
// Placeholder for dues reminder functionality
|
|
|
|
|
console.log('Send dues reminders - feature to be implemented');
|
|
|
|
|
showSuccess.value = true;
|
|
|
|
|
successMessage.value = 'Dues reminder feature coming soon!';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleStatusesUpdated = async (updatedCount: number) => {
|
|
|
|
|
showSuccess.value = true;
|
|
|
|
|
successMessage.value = `Successfully updated ${updatedCount} member${updatedCount !== 1 ? 's' : ''} to inactive status`;
|
|
|
|
|
|
|
|
|
|
// Refresh members list and overdue count
|
|
|
|
|
await loadMembers();
|
|
|
|
|
await loadOverdueCount();
|
|
|
|
|
|
|
|
|
|
// Trigger banner refresh
|
|
|
|
|
overdueRefreshTrigger.value += 1;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Load members and overdue count on mount
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await loadMembers();
|
|
|
|
|
await loadOverdueCount();
|
2025-08-07 19:20:29 +02:00
|
|
|
});
|
|
|
|
|
</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>
|