From f6bc81cb014a2f87fe0e853e12898167cfae7fa2 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 Aug 2025 22:53:45 +0200 Subject: [PATCH] implemented comprehensive member card enhancements with complete multiple nationality support and dues management features. --- components/MemberCard.vue | 232 ++++++++++++++++++++++++-------- pages/dashboard/member-list.vue | 16 ++- 2 files changed, 188 insertions(+), 60 deletions(-) diff --git a/components/MemberCard.vue b/components/MemberCard.vue index cf81501..a92f478 100644 --- a/components/MemberCard.vue +++ b/components/MemberCard.vue @@ -11,13 +11,42 @@ :color="statusColor" size="small" variant="flat" + class="font-weight-bold" > - {{ member.membership_status }} + mdi-account-off + mdi-account-check + {{ member.membership_status || 'Inactive' }} - - + +
+ + mdi-pencil + + + + mdi-delete + +
+ + +
{{ member.FullName || `${member.first_name} ${member.last_name}` }} -
- - - {{ getCountryName(member.nationality) || 'Unknown' }} - +
+ +
@@ -78,8 +123,20 @@ {{ duesText }} + + mdi-clock-alert + Due {{ formatDate(nextDuesDate) }} + + +
- - - - - - mdi-pencil - - - - mdi-delete - - -
@@ -148,11 +177,11 @@ const props = withDefaults(defineProps(), { defineEmits(); // 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 */ diff --git a/pages/dashboard/member-list.vue b/pages/dashboard/member-list.vue index 3f4afe7..d488cf5 100644 --- a/pages/dashboard/member-list.vue +++ b/pages/dashboard/member-list.vue @@ -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 );