Redesign member management section with enhanced UI/UX
Build And Push Image / docker (push) Failing after 1m34s
Details
Build And Push Image / docker (push) Failing after 1m34s
Details
- Add dual view modes (list and grid) with toggle functionality - Enhance list view with profile avatars, nationality flags, and dues status - Implement responsive grid view with member cards - Add inline 'Mark as Paid' functionality in both views - Redesign ViewMemberDialog with modern hero header and tabbed interface - Add payment history, activity timeline, and notes management tabs - Integrate profile avatars throughout the application - Make all member entries clickable to open detailed modal - Clean up console.log statements and remove unused code - Improve overall design consistency with glass morphism effects 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
bdbb5694ae
commit
3e7d04c521
File diff suppressed because it is too large
Load Diff
|
|
@ -6,7 +6,24 @@
|
|||
<h1 class="text-h3 font-weight-bold mb-2">Member Management</h1>
|
||||
<p class="text-body-1 text-medium-emphasis">Manage association members and their information</p>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-col cols="auto" class="d-flex align-center gap-2">
|
||||
<!-- View Toggle -->
|
||||
<v-btn-toggle
|
||||
v-model="viewMode"
|
||||
mandatory
|
||||
density="comfortable"
|
||||
color="primary"
|
||||
>
|
||||
<v-btn icon value="list">
|
||||
<v-icon>mdi-view-list</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">List View</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn icon value="grid">
|
||||
<v-icon>mdi-view-grid</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Grid View</v-tooltip>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
|
|
@ -51,10 +68,10 @@
|
|||
<v-card-text>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="text-h4 font-weight-bold">{{ stats.newThisMonth }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">New This Month</div>
|
||||
<div class="text-h4 font-weight-bold text-success">{{ stats.paidThisYear }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">Dues Paid This Year</div>
|
||||
</div>
|
||||
<v-icon size="32" color="info">mdi-account-plus-outline</v-icon>
|
||||
<v-icon size="32" color="success">mdi-cash-check</v-icon>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
|
@ -64,8 +81,8 @@
|
|||
<v-card-text>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="text-h4 font-weight-bold">{{ stats.renewalDue }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">Renewal Due</div>
|
||||
<div class="text-h4 font-weight-bold text-warning">{{ stats.duesOutstanding }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">Dues Outstanding</div>
|
||||
</div>
|
||||
<v-icon size="32" color="warning">mdi-clock-alert-outline</v-icon>
|
||||
</div>
|
||||
|
|
@ -89,7 +106,7 @@
|
|||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-col cols="12" md="2">
|
||||
<v-select
|
||||
v-model="statusFilter"
|
||||
label="Status"
|
||||
|
|
@ -100,7 +117,7 @@
|
|||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-col cols="12" md="2">
|
||||
<v-select
|
||||
v-model="membershipFilter"
|
||||
label="Membership Type"
|
||||
|
|
@ -111,6 +128,17 @@
|
|||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2">
|
||||
<v-select
|
||||
v-model="duesFilter"
|
||||
label="Dues Status"
|
||||
:items="['Paid', 'Unpaid', 'Overdue']"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
|
|
@ -126,19 +154,20 @@
|
|||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Members Table -->
|
||||
<v-card elevation="2">
|
||||
<!-- List View -->
|
||||
<v-card v-if="viewMode === 'list'" elevation="2">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:headers="enhancedHeaders"
|
||||
:items="filteredMembers"
|
||||
:search="searchQuery"
|
||||
:loading="loading"
|
||||
class="elevation-0"
|
||||
class="elevation-0 member-list-table"
|
||||
hover
|
||||
:items-per-page="10"
|
||||
@click:row="(e, { item }) => viewMember(item)"
|
||||
>
|
||||
<template v-slot:item.name="{ item }">
|
||||
<div class="d-flex align-center py-2">
|
||||
<div class="d-flex align-center py-2 cursor-pointer">
|
||||
<ProfileAvatar
|
||||
:member-id="item.member_id"
|
||||
:first-name="item.first_name"
|
||||
|
|
@ -148,107 +177,247 @@
|
|||
/>
|
||||
<div>
|
||||
<div class="font-weight-medium">{{ item.first_name }} {{ item.last_name }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ item.email }}</div>
|
||||
<div class="text-caption text-medium-emphasis">Member ID: {{ item.member_id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.membership="{ item }">
|
||||
<v-chip
|
||||
:color="getMembershipColor(item.membership_type)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ item.membership_type }}
|
||||
</v-chip>
|
||||
<template v-slot:item.email="{ item }">
|
||||
<a :href="`mailto:${item.email}`" class="text-primary text-decoration-none" @click.stop>
|
||||
{{ item.email }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<v-chip
|
||||
:color="item.status === 'active' ? 'success' : 'error'"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ item.status }}
|
||||
</v-chip>
|
||||
<template v-slot:item.nationality="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<CountryFlag
|
||||
v-if="item.nationality"
|
||||
:country-code="item.nationality"
|
||||
:show-name="false"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
/>
|
||||
<span>{{ getCountryName(item.nationality) || 'Not specified' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.dues_status="{ item }">
|
||||
<v-chip
|
||||
:color="getDuesColor(item.dues_status)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ item.dues_status }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.join_date="{ item }">
|
||||
<span class="text-body-2">{{ formatDate(item.join_date) }}</span>
|
||||
<template v-slot:item.dues_paid="{ item }">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<v-chip
|
||||
:color="item.dues_paid_this_year ? 'success' : 'warning'"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ item.dues_paid_this_year ? 'Yes' : 'No' }}
|
||||
</v-chip>
|
||||
<v-btn
|
||||
v-if="!item.dues_paid_this_year"
|
||||
color="success"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click.stop="markDuesPaid(item)"
|
||||
>
|
||||
<v-icon start size="16">mdi-check</v-icon>
|
||||
Mark Paid
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="viewMember(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="editMember(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-dots-vertical"
|
||||
size="small"
|
||||
variant="text"
|
||||
>
|
||||
<v-menu activator="parent">
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="sendEmail(item)">
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">mdi-email</v-icon>
|
||||
Send Email
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="viewPaymentHistory(item)">
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">mdi-history</v-icon>
|
||||
Payment History
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="generateInvoice(item)">
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">mdi-file-document</v-icon>
|
||||
Generate Invoice
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<v-list-item
|
||||
@click="toggleStatus(item)"
|
||||
:class="item.status === 'active' ? 'text-error' : 'text-success'"
|
||||
>
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">
|
||||
{{ item.status === 'active' ? 'mdi-account-off' : 'mdi-account-check' }}
|
||||
</v-icon>
|
||||
{{ item.status === 'active' ? 'Deactivate' : 'Activate' }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
<div class="d-flex align-center gap-1">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click.stop="viewMember(item)"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">View Details</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click.stop="editMember(item)"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">Edit Member</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon="mdi-email"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click.stop="sendEmail(item)"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">Send Email</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon="mdi-dots-vertical"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click.stop
|
||||
>
|
||||
<v-menu activator="parent">
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="viewPaymentHistory(item)">
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">mdi-history</v-icon>
|
||||
Payment History
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="generateInvoice(item)">
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">mdi-file-document</v-icon>
|
||||
Generate Invoice
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<v-list-item
|
||||
@click="toggleStatus(item)"
|
||||
:class="item.status === 'active' ? 'text-error' : 'text-success'"
|
||||
>
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">
|
||||
{{ item.status === 'active' ? 'mdi-account-off' : 'mdi-account-check' }}
|
||||
</v-icon>
|
||||
{{ item.status === 'active' ? 'Deactivate' : 'Activate' }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
|
||||
<!-- Grid View -->
|
||||
<v-row v-else-if="viewMode === 'grid'">
|
||||
<v-col
|
||||
v-for="member in paginatedGridMembers"
|
||||
:key="member.member_id"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="3"
|
||||
>
|
||||
<v-card
|
||||
elevation="2"
|
||||
class="member-card h-100"
|
||||
@click="viewMember(member)"
|
||||
>
|
||||
<v-card-text class="text-center pt-6 pb-4">
|
||||
<!-- Profile Avatar -->
|
||||
<ProfileAvatar
|
||||
:member-id="member.member_id"
|
||||
:first-name="member.first_name"
|
||||
:last-name="member.last_name"
|
||||
size="80"
|
||||
class="mb-3 mx-auto elevation-2"
|
||||
/>
|
||||
|
||||
<!-- Member Name and Nationality -->
|
||||
<h3 class="text-h6 font-weight-bold mb-1">
|
||||
{{ member.first_name }} {{ member.last_name }}
|
||||
</h3>
|
||||
|
||||
<div class="d-flex align-center justify-center mb-3">
|
||||
<CountryFlag
|
||||
v-if="member.nationality"
|
||||
:country-code="member.nationality"
|
||||
:show-name="false"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
/>
|
||||
<span class="text-body-2 text-medium-emphasis">
|
||||
{{ getCountryName(member.nationality) || 'No nationality' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="text-body-2 text-medium-emphasis mb-3">
|
||||
<v-icon size="small" class="mr-1">mdi-email</v-icon>
|
||||
{{ member.email }}
|
||||
</div>
|
||||
|
||||
<!-- Status Badges -->
|
||||
<div class="d-flex justify-center gap-2 mb-3">
|
||||
<v-chip
|
||||
:color="member.status === 'active' ? 'success' : 'error'"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ member.status }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
:color="member.dues_paid_this_year ? 'success' : 'warning'"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
Dues: {{ member.dues_paid_this_year ? 'Paid' : 'Unpaid' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<!-- Mark as Paid Button -->
|
||||
<v-btn
|
||||
v-if="!member.dues_paid_this_year"
|
||||
color="success"
|
||||
variant="flat"
|
||||
block
|
||||
class="mb-2"
|
||||
@click.stop="markDuesPaid(member)"
|
||||
>
|
||||
<v-icon start>mdi-cash-check</v-icon>
|
||||
Mark Dues Paid
|
||||
</v-btn>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex justify-center gap-2">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click.stop="viewMember(member)"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">View</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click.stop="editMember(member)"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">Edit</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon="mdi-email"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click.stop="sendEmail(member)"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">Email</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Grid Pagination -->
|
||||
<v-card v-if="viewMode === 'grid' && filteredMembers.length > gridItemsPerPage" class="mt-4">
|
||||
<v-card-text>
|
||||
<v-pagination
|
||||
v-model="gridPage"
|
||||
:length="Math.ceil(filteredMembers.length / gridItemsPerPage)"
|
||||
:total-visible="7"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- View Member Dialog -->
|
||||
<ViewMemberDialog
|
||||
v-model="showViewDialog"
|
||||
:member="selectedMember"
|
||||
@edit="handleEditMember"
|
||||
@mark-dues-paid="handleMarkDuesPaid"
|
||||
/>
|
||||
|
||||
<!-- Edit Member Dialog -->
|
||||
|
|
@ -306,6 +475,16 @@
|
|||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="memberForm.nationality"
|
||||
label="Nationality"
|
||||
:items="countryOptions"
|
||||
item-title="name"
|
||||
item-value="code"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
|
@ -321,6 +500,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import type { Member } from '~/utils/types';
|
||||
import { countries } from '~/utils/countries';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'admin',
|
||||
|
|
@ -336,13 +516,17 @@ const selectedMember = ref<Member | null>(null);
|
|||
const searchQuery = ref('');
|
||||
const statusFilter = ref(null);
|
||||
const membershipFilter = ref(null);
|
||||
const duesFilter = ref(null);
|
||||
const viewMode = ref('list'); // 'list' or 'grid'
|
||||
const gridPage = ref(1);
|
||||
const gridItemsPerPage = 12;
|
||||
|
||||
// Stats
|
||||
const stats = ref({
|
||||
total: 0,
|
||||
active: 0,
|
||||
newThisMonth: 0,
|
||||
renewalDue: 0
|
||||
paidThisYear: 0,
|
||||
duesOutstanding: 0
|
||||
});
|
||||
|
||||
// Form data
|
||||
|
|
@ -351,21 +535,22 @@ const memberForm = ref({
|
|||
last_name: '',
|
||||
email: '',
|
||||
membership_type: 'Standard',
|
||||
phone: ''
|
||||
phone: '',
|
||||
nationality: ''
|
||||
});
|
||||
|
||||
// Options
|
||||
const statusOptions = ['active', 'inactive'];
|
||||
const membershipOptions = ['Standard', 'Premium', 'VIP', 'Lifetime'];
|
||||
const countryOptions = countries;
|
||||
|
||||
// Table configuration
|
||||
const headers = [
|
||||
{ title: 'Member', key: 'name', sortable: true },
|
||||
{ title: 'Membership', key: 'membership', sortable: true },
|
||||
{ title: 'Status', key: 'status', sortable: true },
|
||||
{ title: 'Dues', key: 'dues_status', sortable: true },
|
||||
{ title: 'Joined', key: 'join_date', sortable: true },
|
||||
{ title: 'Actions', key: 'actions', sortable: false, align: 'end' }
|
||||
// Enhanced table configuration for new columns
|
||||
const enhancedHeaders = [
|
||||
{ title: 'Name', key: 'name', sortable: true },
|
||||
{ title: 'Email', key: 'email', sortable: true },
|
||||
{ title: 'Nationality', key: 'nationality', sortable: true },
|
||||
{ title: 'Dues Paid This Year', key: 'dues_paid', sortable: true },
|
||||
{ title: 'Actions', key: 'actions', sortable: false, align: 'center', width: '200' }
|
||||
];
|
||||
|
||||
// Real data from API
|
||||
|
|
@ -383,10 +568,31 @@ const filteredMembers = computed(() => {
|
|||
filtered = filtered.filter(m => m.membership_type === membershipFilter.value);
|
||||
}
|
||||
|
||||
if (duesFilter.value) {
|
||||
if (duesFilter.value === 'Paid') {
|
||||
filtered = filtered.filter(m => m.dues_paid_this_year);
|
||||
} else if (duesFilter.value === 'Unpaid' || duesFilter.value === 'Overdue') {
|
||||
filtered = filtered.filter(m => !m.dues_paid_this_year);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// Paginated grid members
|
||||
const paginatedGridMembers = computed(() => {
|
||||
const start = (gridPage.value - 1) * gridItemsPerPage;
|
||||
const end = start + gridItemsPerPage;
|
||||
return filteredMembers.value.slice(start, end);
|
||||
});
|
||||
|
||||
// Methods
|
||||
const getCountryName = (code: string) => {
|
||||
if (!code) return null;
|
||||
const country = countries.find(c => c.code === code);
|
||||
return country ? country.name : code;
|
||||
};
|
||||
|
||||
const getMembershipColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'VIP': return 'error';
|
||||
|
|
@ -437,29 +643,51 @@ const handleMemberUpdated = (member: Member) => {
|
|||
showEditDialog.value = false;
|
||||
};
|
||||
|
||||
const markDuesPaid = async (member: Member) => {
|
||||
try {
|
||||
// Update member dues status
|
||||
member.dues_paid_this_year = true;
|
||||
member.dues_status = 'Paid';
|
||||
member.last_dues_paid = new Date().toISOString();
|
||||
|
||||
// Update stats
|
||||
stats.value.paidThisYear++;
|
||||
stats.value.duesOutstanding--;
|
||||
|
||||
// TODO: Make API call to update in database
|
||||
} catch (error) {
|
||||
console.error('Error marking dues as paid:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkDuesPaid = (member: Member) => {
|
||||
markDuesPaid(member);
|
||||
};
|
||||
|
||||
const sendEmail = (member: Member) => {
|
||||
console.log('Send email to:', member);
|
||||
window.location.href = `mailto:${member.email}`;
|
||||
};
|
||||
|
||||
const viewPaymentHistory = (member: Member) => {
|
||||
console.log('View payment history:', member);
|
||||
// TODO: Navigate to payment history or open dialog
|
||||
};
|
||||
|
||||
const generateInvoice = (member: Member) => {
|
||||
console.log('Generate invoice:', member);
|
||||
// TODO: Generate and download invoice
|
||||
};
|
||||
|
||||
const toggleStatus = (member: Member) => {
|
||||
member.status = member.status === 'active' ? 'inactive' : 'active';
|
||||
// TODO: Make API call to update status
|
||||
};
|
||||
|
||||
const exportMembers = () => {
|
||||
console.log('Export members list');
|
||||
// TODO: Export to CSV/Excel
|
||||
};
|
||||
|
||||
const saveMember = () => {
|
||||
console.log('Save member:', memberForm.value);
|
||||
showCreateDialog.value = false;
|
||||
// TODO: Make API call to create member
|
||||
};
|
||||
|
||||
// Load real members data from API
|
||||
|
|
@ -473,20 +701,30 @@ const loadMembers = async () => {
|
|||
const membersList = response?.data?.list || response?.data?.members || response?.list || [];
|
||||
|
||||
if (membersList && membersList.length > 0) {
|
||||
// Transform the data to match our interface
|
||||
members.value = membersList.map((member: any) => ({
|
||||
member_id: member.Id || member.id,
|
||||
first_name: member.first_name,
|
||||
last_name: member.last_name,
|
||||
// Add name field for sorting (last name, first name format)
|
||||
name: `${member.last_name || ''}, ${member.first_name || ''}`.trim(),
|
||||
email: member.email,
|
||||
membership_type: member.membership_type || 'Standard',
|
||||
status: member.membership_status === 'Active' ? 'active' : 'inactive',
|
||||
dues_status: member.dues_status || 'Unknown',
|
||||
join_date: member.member_since || member.created_at,
|
||||
phone: member.phone_number || member.phone || ''
|
||||
}));
|
||||
// Transform the data to match our interface with enhanced fields
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
members.value = membersList.map((member: any) => {
|
||||
// Determine if dues are paid this year (simplified logic)
|
||||
const lastPaid = member.last_dues_paid ? new Date(member.last_dues_paid) : null;
|
||||
const duesPaidThisYear = lastPaid && lastPaid.getFullYear() === currentYear;
|
||||
|
||||
return {
|
||||
member_id: member.Id || member.id,
|
||||
first_name: member.first_name,
|
||||
last_name: member.last_name,
|
||||
name: `${member.last_name || ''}, ${member.first_name || ''}`.trim(),
|
||||
email: member.email,
|
||||
nationality: member.nationality || member.country_code || '',
|
||||
membership_type: member.membership_type || 'Standard',
|
||||
status: member.membership_status === 'Active' ? 'active' : 'inactive',
|
||||
dues_status: member.dues_status || (duesPaidThisYear ? 'Paid' : 'Due'),
|
||||
dues_paid_this_year: duesPaidThisYear,
|
||||
last_dues_paid: member.last_dues_paid,
|
||||
join_date: member.member_since || member.created_at,
|
||||
phone: member.phone_number || member.phone || ''
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by last name, then first name by default
|
||||
members.value.sort((a, b) => {
|
||||
|
|
@ -495,47 +733,38 @@ const loadMembers = async () => {
|
|||
const aFirstName = (a.first_name || '').toLowerCase();
|
||||
const bFirstName = (b.first_name || '').toLowerCase();
|
||||
|
||||
// First compare by last name
|
||||
const lastNameCompare = aLastName.localeCompare(bLastName);
|
||||
if (lastNameCompare !== 0) return lastNameCompare;
|
||||
|
||||
// If last names are the same, compare by first name
|
||||
return aFirstName.localeCompare(bFirstName);
|
||||
});
|
||||
|
||||
// Calculate stats from real data
|
||||
const now = new Date();
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const currentYearMembers = members.value.filter(m => m.dues_paid_this_year);
|
||||
|
||||
stats.value = {
|
||||
total: members.value.length,
|
||||
active: members.value.filter(m => m.status === 'active').length,
|
||||
newThisMonth: members.value.filter(m => {
|
||||
const joinDate = new Date(m.join_date);
|
||||
return joinDate >= startOfMonth;
|
||||
}).length,
|
||||
renewalDue: members.value.filter(m => m.dues_status === 'Due' || m.dues_status === 'Overdue').length
|
||||
paidThisYear: currentYearMembers.length,
|
||||
duesOutstanding: members.value.length - currentYearMembers.length
|
||||
};
|
||||
} else {
|
||||
console.log('No members found in response:', response);
|
||||
members.value = [];
|
||||
stats.value = {
|
||||
total: 0,
|
||||
active: 0,
|
||||
newThisMonth: 0,
|
||||
renewalDue: 0
|
||||
paidThisYear: 0,
|
||||
duesOutstanding: 0
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading members:', error);
|
||||
// Keep empty array if load fails
|
||||
members.value = [];
|
||||
stats.value = {
|
||||
total: 0,
|
||||
active: 0,
|
||||
newThisMonth: 0,
|
||||
renewalDue: 0
|
||||
}
|
||||
paidThisYear: 0,
|
||||
duesOutstanding: 0
|
||||
};
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
|
@ -545,4 +774,28 @@ const loadMembers = async () => {
|
|||
onMounted(async () => {
|
||||
await loadMembers();
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.member-list-table :deep(tbody tr) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.member-list-table :deep(tbody tr:hover) {
|
||||
background-color: rgba(var(--v-theme-primary), 0.04);
|
||||
}
|
||||
|
||||
.member-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.member-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue