implemented comprehensive member card enhancements with complete multiple nationality support and dues management features.
Build And Push Image / docker (push) Successful in 2m54s
Details
Build And Push Image / docker (push) Successful in 2m54s
Details
This commit is contained in:
parent
9202509c9c
commit
f6bc81cb01
|
|
@ -11,13 +11,42 @@
|
||||||
:color="statusColor"
|
:color="statusColor"
|
||||||
size="small"
|
size="small"
|
||||||
variant="flat"
|
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>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card Header -->
|
<!-- Action Buttons -->
|
||||||
<v-card-text class="pb-2">
|
<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">
|
<div class="d-flex align-center mb-3">
|
||||||
<v-avatar
|
<v-avatar
|
||||||
:color="avatarColor"
|
:color="avatarColor"
|
||||||
|
|
@ -33,17 +62,33 @@
|
||||||
<h3 class="text-h6 font-weight-bold mb-1">
|
<h3 class="text-h6 font-weight-bold mb-1">
|
||||||
{{ member.FullName || `${member.first_name} ${member.last_name}` }}
|
{{ member.FullName || `${member.first_name} ${member.last_name}` }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="d-flex align-center">
|
<div class="nationality-display">
|
||||||
<CountryFlag
|
<template v-if="nationalitiesArray.length > 0">
|
||||||
v-if="member.nationality"
|
<div class="d-flex align-center flex-wrap">
|
||||||
:country-code="member.nationality"
|
<!-- Display all flags together -->
|
||||||
:show-name="false"
|
<div class="flags-container d-flex align-center me-2">
|
||||||
size="small"
|
<CountryFlag
|
||||||
class="mr-2"
|
v-for="nationality in nationalitiesArray"
|
||||||
/>
|
:key="nationality"
|
||||||
<span class="text-body-2 text-medium-emphasis">
|
:country-code="nationality"
|
||||||
{{ getCountryName(member.nationality) || 'Unknown' }}
|
:show-name="false"
|
||||||
</span>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -78,8 +123,20 @@
|
||||||
{{ duesText }}
|
{{ duesText }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
|
|
||||||
|
<!-- Dues Coming Due Warning -->
|
||||||
<v-chip
|
<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"
|
color="warning"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
size="small"
|
size="small"
|
||||||
|
|
@ -91,34 +148,6 @@
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</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 -->
|
<!-- Click overlay for better UX -->
|
||||||
<div class="member-card-overlay" @click="$emit('view', member)"></div>
|
<div class="member-card-overlay" @click="$emit('view', member)"></div>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
@ -148,11 +177,11 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
defineEmits<Emits>();
|
defineEmits<Emits>();
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
const memberInitials = computed(() => {
|
const memberInitials = computed(() => {
|
||||||
const firstName = props.member.first_name || '';
|
const firstName = props.member.first_name || '';
|
||||||
const lastName = props.member.last_name || '';
|
const lastName = props.member.last_name || '';
|
||||||
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
||||||
});
|
});
|
||||||
|
|
||||||
const avatarColor = computed(() => {
|
const avatarColor = computed(() => {
|
||||||
// Generate consistent color based on member ID using high-contrast colors
|
// Generate consistent color based on member ID using high-contrast colors
|
||||||
|
|
@ -161,6 +190,18 @@ const avatarColor = computed(() => {
|
||||||
return colors[idNumber % colors.length];
|
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(() => {
|
const isActive = computed(() => {
|
||||||
return props.member.membership_status === 'Active';
|
return props.member.membership_status === 'Active';
|
||||||
});
|
});
|
||||||
|
|
@ -199,6 +240,47 @@ const isOverdue = computed(() => {
|
||||||
return dueDate < today && props.member.current_year_dues_paid !== 'true';
|
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
|
// Methods
|
||||||
const formatDate = (dateString: string): string => {
|
const formatDate = (dateString: string): string => {
|
||||||
if (!dateString) return '';
|
if (!dateString) return '';
|
||||||
|
|
@ -223,6 +305,7 @@ const formatDate = (dateString: string): string => {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-card:hover {
|
.member-card:hover {
|
||||||
|
|
@ -245,6 +328,45 @@ const formatDate = (dateString: string): string => {
|
||||||
z-index: 2;
|
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 {
|
.member-info {
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
}
|
}
|
||||||
|
|
@ -271,15 +393,6 @@ const formatDate = (dateString: string): string => {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-card-actions {
|
|
||||||
position: relative;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-card-actions .v-btn {
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.member-card {
|
.member-card {
|
||||||
|
|
@ -290,6 +403,11 @@ const formatDate = (dateString: string): string => {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.member-action-buttons {
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animation for status changes */
|
/* Animation for status changes */
|
||||||
|
|
|
||||||
|
|
@ -380,9 +380,19 @@ const filteredMembers = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalMembers = computed(() => members.value.length);
|
const totalMembers = computed(() => members.value.length);
|
||||||
const activeMembers = computed(() =>
|
const activeMembers = computed(() => {
|
||||||
members.value.filter(m => m.membership_status === 'Active').length
|
// 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(() =>
|
const paidDuesMembers = computed(() =>
|
||||||
members.value.filter(m => m.current_year_dues_paid === 'true').length
|
members.value.filter(m => m.current_year_dues_paid === 'true').length
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue