318 lines
7.5 KiB
Vue
318 lines
7.5 KiB
Vue
<template>
|
|
<v-card
|
|
class="member-card"
|
|
:class="{ 'member-card--inactive': !isActive }"
|
|
elevation="2"
|
|
@click="$emit('view', member)"
|
|
>
|
|
<!-- Member Status Badge -->
|
|
<div class="member-status-badge">
|
|
<v-chip
|
|
:color="statusColor"
|
|
size="small"
|
|
variant="flat"
|
|
>
|
|
{{ member['Membership Status'] }}
|
|
</v-chip>
|
|
</div>
|
|
|
|
<!-- Card Header -->
|
|
<v-card-text class="pb-2">
|
|
<div class="d-flex align-center mb-3">
|
|
<v-avatar
|
|
:color="avatarColor"
|
|
size="48"
|
|
class="mr-3"
|
|
>
|
|
<span class="text-white font-weight-bold text-h6">
|
|
{{ memberInitials }}
|
|
</span>
|
|
</v-avatar>
|
|
|
|
<div class="flex-grow-1">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Member Info -->
|
|
<div class="member-info">
|
|
<div class="info-row mb-2">
|
|
<v-icon size="16" class="mr-2 text-medium-emphasis">mdi-email</v-icon>
|
|
<span class="text-body-2">{{ member.Email || 'No email' }}</span>
|
|
</div>
|
|
|
|
<div class="info-row mb-2" v-if="member.Phone">
|
|
<v-icon size="16" class="mr-2 text-medium-emphasis">mdi-phone</v-icon>
|
|
<span class="text-body-2">{{ member.FormattedPhone || member.Phone }}</span>
|
|
</div>
|
|
|
|
<div class="info-row mb-2" v-if="member['Member Since']">
|
|
<v-icon size="16" class="mr-2 text-medium-emphasis">mdi-calendar</v-icon>
|
|
<span class="text-body-2">Member since {{ formatDate(member['Member Since']) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dues Status -->
|
|
<div class="dues-status mt-3">
|
|
<v-chip
|
|
:color="duesColor"
|
|
:variant="duesVariant"
|
|
size="small"
|
|
class="mr-2"
|
|
>
|
|
<v-icon start size="14">{{ duesIcon }}</v-icon>
|
|
{{ duesText }}
|
|
</v-chip>
|
|
|
|
<v-chip
|
|
v-if="member['Payment Due Date']"
|
|
color="warning"
|
|
variant="tonal"
|
|
size="small"
|
|
:class="{ 'text-error': isOverdue }"
|
|
>
|
|
<v-icon start size="14">mdi-calendar-alert</v-icon>
|
|
{{ isOverdue ? 'Overdue' : `Due ${formatDate(member['Payment Due Date'])}` }}
|
|
</v-chip>
|
|
</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>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Member } from '~/utils/types';
|
|
import { getCountryName } from '~/utils/countries';
|
|
|
|
interface Props {
|
|
member: Member;
|
|
canEdit?: boolean;
|
|
canDelete?: boolean;
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'view', member: Member): void;
|
|
(e: 'edit', member: Member): void;
|
|
(e: 'delete', member: Member): void;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
canEdit: false,
|
|
canDelete: false
|
|
});
|
|
|
|
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 avatarColor = computed(() => {
|
|
// Generate consistent color based on member ID
|
|
const colors = ['primary', 'secondary', 'accent', 'info', 'warning', 'success'];
|
|
const idNumber = parseInt(props.member.Id) || 0;
|
|
return colors[idNumber % colors.length];
|
|
});
|
|
|
|
const isActive = computed(() => {
|
|
return props.member['Membership Status'] === 'Active';
|
|
});
|
|
|
|
const statusColor = computed(() => {
|
|
const status = props.member['Membership Status'];
|
|
switch (status) {
|
|
case 'Active': return 'success';
|
|
case 'Inactive': return 'grey';
|
|
case 'Pending': return 'warning';
|
|
case 'Expired': return 'error';
|
|
default: return 'grey';
|
|
}
|
|
});
|
|
|
|
const duesColor = computed(() => {
|
|
return props.member['Current Year Dues Paid'] === 'true' ? 'success' : 'error';
|
|
});
|
|
|
|
const duesVariant = computed(() => {
|
|
return props.member['Current Year Dues Paid'] === 'true' ? 'tonal' : 'flat';
|
|
});
|
|
|
|
const duesIcon = computed(() => {
|
|
return props.member['Current Year Dues Paid'] === 'true' ? 'mdi-check-circle' : 'mdi-alert-circle';
|
|
});
|
|
|
|
const duesText = computed(() => {
|
|
return props.member['Current Year Dues Paid'] === 'true' ? 'Dues Paid' : 'Dues Outstanding';
|
|
});
|
|
|
|
const isOverdue = computed(() => {
|
|
if (!props.member['Payment Due Date']) return false;
|
|
const dueDate = new Date(props.member['Payment Due Date']);
|
|
const today = new Date();
|
|
return dueDate < today && props.member['Current Year Dues Paid'] !== 'true';
|
|
});
|
|
|
|
// Methods
|
|
const formatDate = (dateString: string): string => {
|
|
if (!dateString) return '';
|
|
|
|
try {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
} catch {
|
|
return dateString;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.member-card {
|
|
cursor: pointer;
|
|
border-radius: 12px !important;
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
height: 100%;
|
|
}
|
|
|
|
.member-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 8px 25px rgba(163, 21, 21, 0.15) !important;
|
|
}
|
|
|
|
.member-card--inactive {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.member-card--inactive .v-card-text {
|
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
}
|
|
|
|
.member-status-badge {
|
|
position: absolute;
|
|
top: 12px;
|
|
right: 12px;
|
|
z-index: 2;
|
|
}
|
|
|
|
.member-info {
|
|
min-height: 80px;
|
|
}
|
|
|
|
.info-row {
|
|
display: flex;
|
|
align-items: center;
|
|
min-height: 24px;
|
|
}
|
|
|
|
.dues-status {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.member-card-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: 1;
|
|
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 {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.dues-status {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
}
|
|
|
|
/* Animation for status changes */
|
|
.v-chip {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
/* Custom scrollbar for long content */
|
|
.member-info::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
.member-info::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.member-info::-webkit-scrollbar-thumb {
|
|
background-color: rgba(163, 21, 21, 0.3);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.text-error {
|
|
color: rgb(var(--v-theme-error)) !important;
|
|
}
|
|
</style>
|