implemented comprehensive member card enhancements with complete multiple nationality support and dues management features.
Build And Push Image / docker (push) Successful in 2m54s Details

This commit is contained in:
Matt 2025-08-07 22:53:45 +02:00
parent 9202509c9c
commit f6bc81cb01
2 changed files with 188 additions and 60 deletions

View File

@ -11,13 +11,42 @@
:color="statusColor"
size="small"
variant="flat"
class="font-weight-bold"
>
{{ member.membership_status }}
<v-icon v-if="!isActive" start size="12">mdi-account-off</v-icon>
<v-icon v-else start size="12">mdi-account-check</v-icon>
{{ member.membership_status || 'Inactive' }}
</v-chip>
</div>
<!-- Card Header -->
<v-card-text class="pb-2">
<!-- Action Buttons -->
<div v-if="canEdit || canDelete" class="member-action-buttons">
<v-btn
v-if="canEdit"
icon
size="small"
variant="text"
@click.stop="$emit('edit', member)"
:title="'Edit ' + member.FullName"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn
v-if="canDelete"
icon
size="small"
variant="text"
color="error"
@click.stop="$emit('delete', member)"
:title="'Delete ' + member.FullName"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
<!-- Card Content -->
<v-card-text class="pb-4">
<div class="d-flex align-center mb-3">
<v-avatar
:color="avatarColor"
@ -33,17 +62,33 @@
<h3 class="text-h6 font-weight-bold mb-1">
{{ member.FullName || `${member.first_name} ${member.last_name}` }}
</h3>
<div class="d-flex align-center">
<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) || 'Unknown' }}
</span>
<div class="nationality-display">
<template v-if="nationalitiesArray.length > 0">
<div class="d-flex align-center flex-wrap">
<!-- Display all flags together -->
<div class="flags-container d-flex align-center me-2">
<CountryFlag
v-for="nationality in nationalitiesArray"
:key="nationality"
:country-code="nationality"
:show-name="false"
size="small"
class="flag-item"
/>
</div>
<!-- Display country names -->
<div class="country-names">
<span class="text-body-2 text-medium-emphasis">
{{ nationalitiesArray.map(n => getCountryName(n)).join(', ') }}
</span>
</div>
</div>
</template>
<template v-else>
<span class="text-body-2 text-medium-emphasis">
Unknown
</span>
</template>
</div>
</div>
</div>
@ -78,8 +123,20 @@
{{ duesText }}
</v-chip>
<!-- Dues Coming Due Warning -->
<v-chip
v-if="member.payment_due_date"
v-if="isDuesComingDue"
color="orange"
variant="flat"
size="small"
class="mr-2"
>
<v-icon start size="14">mdi-clock-alert</v-icon>
Due {{ formatDate(nextDuesDate) }}
</v-chip>
<v-chip
v-if="member.payment_due_date && !isDuesComingDue"
color="warning"
variant="tonal"
size="small"
@ -91,34 +148,6 @@
</div>
</v-card-text>
<!-- Card Actions -->
<v-card-actions v-if="canEdit || canDelete" class="pt-0">
<v-spacer />
<v-btn
v-if="canEdit"
icon
size="small"
variant="text"
@click.stop="$emit('edit', member)"
:title="'Edit ' + member.FullName"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn
v-if="canDelete"
icon
size="small"
variant="text"
color="error"
@click.stop="$emit('delete', member)"
:title="'Delete ' + member.FullName"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-card-actions>
<!-- Click overlay for better UX -->
<div class="member-card-overlay" @click="$emit('view', member)"></div>
</v-card>
@ -148,11 +177,11 @@ const props = withDefaults(defineProps<Props>(), {
defineEmits<Emits>();
// Computed properties
const memberInitials = computed(() => {
const firstName = props.member.first_name || '';
const lastName = props.member.last_name || '';
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
});
const memberInitials = computed(() => {
const firstName = props.member.first_name || '';
const lastName = props.member.last_name || '';
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
});
const avatarColor = computed(() => {
// Generate consistent color based on member ID using high-contrast colors
@ -161,6 +190,18 @@ const avatarColor = computed(() => {
return colors[idNumber % colors.length];
});
const nationalitiesArray = computed(() => {
if (!props.member.nationality) return [];
// Handle multiple nationalities separated by comma, semicolon, or pipe
const nationalities = props.member.nationality
.split(/[,;|]/)
.map(n => n.trim().toUpperCase())
.filter(n => n.length > 0);
return nationalities;
});
const isActive = computed(() => {
return props.member.membership_status === 'Active';
});
@ -199,6 +240,47 @@ const isOverdue = computed(() => {
return dueDate < today && props.member.current_year_dues_paid !== 'true';
});
// Calculate next dues date (1 year from when they last paid)
const nextDuesDate = computed(() => {
// If dues are paid, calculate 1 year from payment date
if (props.member.current_year_dues_paid === 'true' && props.member.membership_date_paid) {
const lastPaidDate = new Date(props.member.membership_date_paid);
const nextDue = new Date(lastPaidDate);
nextDue.setFullYear(nextDue.getFullYear() + 1);
return nextDue.toISOString().split('T')[0]; // Return as date string
}
// If not paid but has a due date, use that
if (props.member.payment_due_date) {
return props.member.payment_due_date;
}
// Fallback: 1 year from member since date
if (props.member.member_since) {
const memberSince = new Date(props.member.member_since);
const nextDue = new Date(memberSince);
nextDue.setFullYear(nextDue.getFullYear() + 1);
return nextDue.toISOString().split('T')[0];
}
return '';
});
// Check if dues are coming due within 2 months
const isDuesComingDue = computed(() => {
// Only show warning if dues are currently paid
if (props.member.current_year_dues_paid !== 'true') return false;
if (!nextDuesDate.value) return false;
const today = new Date();
const dueDate = new Date(nextDuesDate.value);
const twoMonthsFromNow = new Date();
twoMonthsFromNow.setMonth(twoMonthsFromNow.getMonth() + 2);
// Show warning if due date is within the next 2 months
return dueDate <= twoMonthsFromNow && dueDate > today;
});
// Methods
const formatDate = (dateString: string): string => {
if (!dateString) return '';
@ -223,6 +305,7 @@ const formatDate = (dateString: string): string => {
transition: all 0.3s ease;
position: relative;
height: 100%;
overflow: hidden;
}
.member-card:hover {
@ -245,6 +328,45 @@ const formatDate = (dateString: string): string => {
z-index: 2;
}
.member-action-buttons {
position: absolute;
bottom: 12px;
right: 12px;
z-index: 3;
display: flex;
gap: 4px;
}
.member-action-buttons .v-btn {
pointer-events: all;
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
}
.nationality-display {
min-height: 20px;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.flags-container {
display: flex;
align-items: center;
}
.flag-item {
margin-right: 4px;
}
.flag-item:last-child {
margin-right: 0;
}
.country-names {
flex: 1;
}
.member-info {
min-height: 80px;
}
@ -271,15 +393,6 @@ const formatDate = (dateString: string): string => {
pointer-events: none;
}
.v-card-actions {
position: relative;
z-index: 3;
}
.v-card-actions .v-btn {
pointer-events: all;
}
/* Responsive adjustments */
@media (max-width: 600px) {
.member-card {
@ -290,6 +403,11 @@ const formatDate = (dateString: string): string => {
flex-direction: column;
align-items: flex-start;
}
.member-action-buttons {
bottom: 8px;
right: 8px;
}
}
/* Animation for status changes */

View File

@ -380,9 +380,19 @@ const filteredMembers = computed(() => {
});
const totalMembers = computed(() => members.value.length);
const activeMembers = computed(() =>
members.value.filter(m => m.membership_status === 'Active').length
);
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;
});
const paidDuesMembers = computed(() =>
members.value.filter(m => m.current_year_dues_paid === 'true').length
);