725 lines
23 KiB
Vue
725 lines
23 KiB
Vue
<template>
|
|
<v-dialog
|
|
:model-value="modelValue"
|
|
@update:model-value="$emit('update:model-value', $event)"
|
|
max-width="900"
|
|
persistent
|
|
scrollable
|
|
>
|
|
<v-card v-if="member" class="member-modal">
|
|
<!-- Hero Header with Profile -->
|
|
<div class="member-hero-header">
|
|
<v-btn
|
|
icon
|
|
variant="text"
|
|
color="white"
|
|
class="close-btn"
|
|
@click="$emit('update:model-value', false)"
|
|
>
|
|
<v-icon>mdi-close</v-icon>
|
|
</v-btn>
|
|
|
|
<div class="hero-content">
|
|
<ProfileAvatar
|
|
:member-id="member.member_id"
|
|
:member-name="member.FullName || `${member.first_name} ${member.last_name}`"
|
|
:first-name="member.first_name"
|
|
:last-name="member.last_name"
|
|
size="120"
|
|
class="mb-4 elevation-4"
|
|
clickable
|
|
show-border
|
|
@click="openImageLightbox"
|
|
/>
|
|
|
|
<h1 class="text-h4 font-weight-bold text-white mb-2">
|
|
{{ member.FullName || `${member.first_name} ${member.last_name}` }}
|
|
</h1>
|
|
|
|
<div class="d-flex align-center justify-center gap-3 mb-3">
|
|
<div class="d-flex align-center">
|
|
<CountryFlag
|
|
v-if="member.nationality"
|
|
:country-code="member.nationality"
|
|
:show-name="false"
|
|
size="medium"
|
|
class="mr-2"
|
|
/>
|
|
<span class="text-white">
|
|
{{ getCountryName(member.nationality) || 'No nationality' }}
|
|
</span>
|
|
</div>
|
|
<v-divider vertical color="white" opacity="0.5" class="mx-2" />
|
|
<span class="text-white">
|
|
Member since {{ formatDate(member.member_since) || 'Unknown' }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Status Badges -->
|
|
<div class="d-flex justify-center gap-2">
|
|
<v-chip
|
|
:color="statusColor"
|
|
variant="flat"
|
|
size="small"
|
|
class="font-weight-bold"
|
|
>
|
|
<v-icon start size="16">{{ statusIcon }}</v-icon>
|
|
{{ member.membership_status }}
|
|
</v-chip>
|
|
|
|
<v-chip
|
|
:color="duesColor"
|
|
:variant="duesVariant"
|
|
size="small"
|
|
class="font-weight-bold"
|
|
>
|
|
<v-icon start size="16">{{ duesIcon }}</v-icon>
|
|
{{ duesText }}
|
|
</v-chip>
|
|
|
|
<v-chip
|
|
v-if="member.membership_type"
|
|
color="purple"
|
|
variant="tonal"
|
|
size="small"
|
|
class="font-weight-bold"
|
|
>
|
|
{{ member.membership_type }}
|
|
</v-chip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions Bar -->
|
|
<div class="quick-actions-bar">
|
|
<v-btn
|
|
v-if="!member.dues_paid_this_year"
|
|
color="success"
|
|
variant="flat"
|
|
prepend-icon="mdi-cash-check"
|
|
@click="markDuesPaid"
|
|
>
|
|
Mark Dues Paid
|
|
</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
variant="tonal"
|
|
prepend-icon="mdi-pencil"
|
|
@click="$emit('edit', member)"
|
|
>
|
|
Edit Profile
|
|
</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
variant="tonal"
|
|
prepend-icon="mdi-email"
|
|
@click="sendEmail"
|
|
>
|
|
Send Email
|
|
</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
variant="tonal"
|
|
prepend-icon="mdi-phone"
|
|
:disabled="!member.phone"
|
|
@click="callPhone"
|
|
>
|
|
Call
|
|
</v-btn>
|
|
<v-menu>
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn
|
|
color="primary"
|
|
variant="tonal"
|
|
icon="mdi-dots-vertical"
|
|
v-bind="props"
|
|
/>
|
|
</template>
|
|
<v-list>
|
|
<v-list-item @click="viewPaymentHistory">
|
|
<v-list-item-title>
|
|
<v-icon start>mdi-history</v-icon>
|
|
Payment History
|
|
</v-list-item-title>
|
|
</v-list-item>
|
|
<v-list-item @click="generateInvoice">
|
|
<v-list-item-title>
|
|
<v-icon start>mdi-file-document</v-icon>
|
|
Generate Invoice
|
|
</v-list-item-title>
|
|
</v-list-item>
|
|
<v-list-item @click="exportMemberData">
|
|
<v-list-item-title>
|
|
<v-icon start>mdi-download</v-icon>
|
|
Export Data
|
|
</v-list-item-title>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
</div>
|
|
|
|
<!-- Content Tabs -->
|
|
<v-card-text class="pa-0">
|
|
<v-tabs
|
|
v-model="activeTab"
|
|
bg-color="grey-lighten-4"
|
|
slider-color="primary"
|
|
>
|
|
<v-tab value="overview">
|
|
<v-icon start>mdi-account-details</v-icon>
|
|
Overview
|
|
</v-tab>
|
|
<v-tab value="payments">
|
|
<v-icon start>mdi-cash-multiple</v-icon>
|
|
Payments
|
|
</v-tab>
|
|
<v-tab value="activity">
|
|
<v-icon start>mdi-history</v-icon>
|
|
Activity
|
|
</v-tab>
|
|
<v-tab value="notes">
|
|
<v-icon start>mdi-note-text</v-icon>
|
|
Notes
|
|
</v-tab>
|
|
</v-tabs>
|
|
|
|
<v-tabs-window v-model="activeTab">
|
|
<!-- Overview Tab -->
|
|
<v-tabs-window-item value="overview">
|
|
<v-container>
|
|
<v-row>
|
|
<!-- Personal Information -->
|
|
<v-col cols="12" md="6">
|
|
<v-card elevation="0" class="info-card">
|
|
<v-card-title class="d-flex align-center">
|
|
<v-icon start color="primary">mdi-account</v-icon>
|
|
Personal Information
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<label>Full Name</label>
|
|
<p>{{ member.first_name }} {{ member.last_name }}</p>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label>Email</label>
|
|
<p>
|
|
<a :href="`mailto:${member.email}`" class="text-primary">
|
|
{{ member.email }}
|
|
</a>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="info-item" v-if="member.phone">
|
|
<label>Phone</label>
|
|
<p>
|
|
<a :href="`tel:${member.phone}`" class="text-primary">
|
|
{{ member.FormattedPhone || member.phone }}
|
|
</a>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="info-item" v-if="member.date_of_birth">
|
|
<label>Date of Birth</label>
|
|
<p>{{ formatDate(member.date_of_birth) }}</p>
|
|
</div>
|
|
|
|
<div class="info-item" v-if="member.address">
|
|
<label>Address</label>
|
|
<p>{{ member.address }}</p>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label>Nationality</label>
|
|
<div class="d-flex align-center">
|
|
<CountryFlag
|
|
v-if="member.nationality"
|
|
:country-code="member.nationality"
|
|
:show-name="false"
|
|
size="small"
|
|
class="mr-2"
|
|
/>
|
|
<span>{{ getCountryName(member.nationality) || 'Not specified' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Membership Information -->
|
|
<v-col cols="12" md="6">
|
|
<v-card elevation="0" class="info-card">
|
|
<v-card-title class="d-flex align-center">
|
|
<v-icon start color="primary">mdi-card-account-details</v-icon>
|
|
Membership Details
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<label>Member ID</label>
|
|
<p>{{ member.member_id }}</p>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label>Membership Type</label>
|
|
<v-chip :color="getMembershipColor(member.membership_type)" size="small" variant="tonal">
|
|
{{ member.membership_type }}
|
|
</v-chip>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label>Status</label>
|
|
<v-chip :color="statusColor" size="small" variant="flat">
|
|
{{ member.membership_status }}
|
|
</v-chip>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label>Member Since</label>
|
|
<p>{{ formatDate(member.member_since) || 'Not specified' }}</p>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label>Last Renewal</label>
|
|
<p>{{ member.last_renewal ? formatDate(member.last_renewal) : 'Never' }}</p>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label>Dues Status</label>
|
|
<v-chip :color="duesColor" size="small" :variant="duesVariant">
|
|
{{ duesText }}
|
|
</v-chip>
|
|
</div>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Emergency Contact -->
|
|
<v-col cols="12" v-if="member.emergency_contact">
|
|
<v-card elevation="0" class="info-card">
|
|
<v-card-title class="d-flex align-center">
|
|
<v-icon start color="error">mdi-phone-alert</v-icon>
|
|
Emergency Contact
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<v-row>
|
|
<v-col cols="12" md="4">
|
|
<div class="info-item">
|
|
<label>Name</label>
|
|
<p>{{ member.emergency_contact.name || 'Not provided' }}</p>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="4">
|
|
<div class="info-item">
|
|
<label>Relationship</label>
|
|
<p>{{ member.emergency_contact.relationship || 'Not provided' }}</p>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="4">
|
|
<div class="info-item">
|
|
<label>Phone</label>
|
|
<p>{{ member.emergency_contact.phone || 'Not provided' }}</p>
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
</v-container>
|
|
</v-tabs-window-item>
|
|
|
|
<!-- Payments Tab -->
|
|
<v-tabs-window-item value="payments">
|
|
<v-container>
|
|
<v-card elevation="0" class="info-card">
|
|
<v-card-title class="d-flex align-center justify-space-between">
|
|
<div class="d-flex align-center">
|
|
<v-icon start color="primary">mdi-cash-multiple</v-icon>
|
|
Payment History
|
|
</div>
|
|
<v-btn
|
|
color="primary"
|
|
variant="tonal"
|
|
size="small"
|
|
prepend-icon="mdi-plus"
|
|
@click="recordPayment"
|
|
>
|
|
Record Payment
|
|
</v-btn>
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<v-list lines="two" class="pa-0">
|
|
<v-list-item
|
|
v-for="payment in recentPayments"
|
|
:key="payment.id"
|
|
class="px-0"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="payment.status === 'Completed' ? 'success' : 'warning'">
|
|
{{ payment.status === 'Completed' ? 'mdi-check-circle' : 'mdi-clock-outline' }}
|
|
</v-icon>
|
|
</template>
|
|
<v-list-item-title>
|
|
${{ payment.amount }} - {{ payment.type }}
|
|
</v-list-item-title>
|
|
<v-list-item-subtitle>
|
|
{{ formatDate(payment.date) }} • {{ payment.method }}
|
|
</v-list-item-subtitle>
|
|
<template v-slot:append>
|
|
<v-chip
|
|
:color="payment.status === 'Completed' ? 'success' : 'warning'"
|
|
size="small"
|
|
variant="tonal"
|
|
>
|
|
{{ payment.status }}
|
|
</v-chip>
|
|
</template>
|
|
</v-list-item>
|
|
</v-list>
|
|
<div v-if="!recentPayments || recentPayments.length === 0" class="text-center py-8 text-medium-emphasis">
|
|
No payment history available
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-container>
|
|
</v-tabs-window-item>
|
|
|
|
<!-- Activity Tab -->
|
|
<v-tabs-window-item value="activity">
|
|
<v-container>
|
|
<v-card elevation="0" class="info-card">
|
|
<v-card-title class="d-flex align-center">
|
|
<v-icon start color="primary">mdi-history</v-icon>
|
|
Recent Activity
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<v-timeline side="end" density="compact">
|
|
<v-timeline-item
|
|
v-for="activity in recentActivities"
|
|
:key="activity.id"
|
|
:dot-color="activity.color"
|
|
size="small"
|
|
>
|
|
<template v-slot:opposite>
|
|
<div class="text-caption">
|
|
{{ formatRelativeTime(activity.date) }}
|
|
</div>
|
|
</template>
|
|
<div>
|
|
<div class="font-weight-medium">{{ activity.title }}</div>
|
|
<div class="text-caption text-medium-emphasis">{{ activity.description }}</div>
|
|
</div>
|
|
</v-timeline-item>
|
|
</v-timeline>
|
|
<div v-if="!recentActivities || recentActivities.length === 0" class="text-center py-8 text-medium-emphasis">
|
|
No recent activity
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-container>
|
|
</v-tabs-window-item>
|
|
|
|
<!-- Notes Tab -->
|
|
<v-tabs-window-item value="notes">
|
|
<v-container>
|
|
<v-card elevation="0" class="info-card">
|
|
<v-card-title class="d-flex align-center justify-space-between">
|
|
<div class="d-flex align-center">
|
|
<v-icon start color="primary">mdi-note-text</v-icon>
|
|
Member Notes
|
|
</div>
|
|
<v-btn
|
|
color="primary"
|
|
variant="tonal"
|
|
size="small"
|
|
prepend-icon="mdi-plus"
|
|
@click="addNote"
|
|
>
|
|
Add Note
|
|
</v-btn>
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<v-textarea
|
|
v-model="memberNotes"
|
|
label="Notes about this member"
|
|
rows="6"
|
|
variant="outlined"
|
|
placeholder="Add notes about this member..."
|
|
/>
|
|
<v-btn
|
|
color="primary"
|
|
variant="flat"
|
|
@click="saveNotes"
|
|
:disabled="!memberNotes"
|
|
>
|
|
Save Notes
|
|
</v-btn>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-container>
|
|
</v-tabs-window-item>
|
|
</v-tabs-window>
|
|
</v-card-text>
|
|
|
|
<!-- Footer Actions -->
|
|
<v-card-actions class="pa-4 bg-grey-lighten-5">
|
|
<v-spacer />
|
|
<v-btn
|
|
variant="text"
|
|
@click="$emit('update:model-value', false)"
|
|
>
|
|
Close
|
|
</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
variant="flat"
|
|
prepend-icon="mdi-pencil"
|
|
@click="$emit('edit', member)"
|
|
>
|
|
Edit Member
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Member } from '~/utils/types';
|
|
import { countries } from '~/utils/countries';
|
|
|
|
interface Props {
|
|
modelValue: boolean;
|
|
member: Member | null;
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'update:model-value', value: boolean): void;
|
|
(e: 'edit', member: Member): void;
|
|
(e: 'mark-dues-paid', member: Member): void;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
const emit = defineEmits<Emits>();
|
|
|
|
// State
|
|
const activeTab = ref('overview');
|
|
const memberNotes = ref('');
|
|
const recentPayments = ref([]);
|
|
const recentActivities = ref([]);
|
|
|
|
// Computed properties
|
|
const statusColor = computed(() => {
|
|
if (!props.member) return 'default';
|
|
return props.member.membership_status === 'Active' ? 'success' : 'error';
|
|
});
|
|
|
|
const statusIcon = computed(() => {
|
|
if (!props.member) return 'mdi-account';
|
|
return props.member.membership_status === 'Active' ? 'mdi-check-circle' : 'mdi-close-circle';
|
|
});
|
|
|
|
const duesColor = computed(() => {
|
|
if (!props.member) return 'default';
|
|
if (props.member.dues_paid_this_year) return 'success';
|
|
if (props.member.dues_status === 'Overdue') return 'error';
|
|
return 'warning';
|
|
});
|
|
|
|
const duesVariant = computed(() => {
|
|
if (!props.member) return 'tonal';
|
|
return props.member.dues_paid_this_year ? 'flat' : 'tonal';
|
|
});
|
|
|
|
const duesIcon = computed(() => {
|
|
if (!props.member) return 'mdi-cash';
|
|
if (props.member.dues_paid_this_year) return 'mdi-check-circle';
|
|
if (props.member.dues_status === 'Overdue') return 'mdi-alert-circle';
|
|
return 'mdi-clock-outline';
|
|
});
|
|
|
|
const duesText = computed(() => {
|
|
if (!props.member) return 'Unknown';
|
|
if (props.member.dues_paid_this_year) return 'Dues Paid';
|
|
if (props.member.dues_status === 'Overdue') return 'Dues Overdue';
|
|
return 'Dues Due';
|
|
});
|
|
|
|
const isOverdue = computed(() => {
|
|
if (!props.member || !props.member.payment_due_date) return false;
|
|
return new Date(props.member.payment_due_date) < new Date();
|
|
});
|
|
|
|
// 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';
|
|
case 'Premium': return 'warning';
|
|
case 'Lifetime': return 'purple';
|
|
default: return 'info';
|
|
}
|
|
};
|
|
|
|
const formatDate = (date: string) => {
|
|
if (!date) return 'N/A';
|
|
const parsedDate = new Date(date);
|
|
if (isNaN(parsedDate.getTime())) return 'N/A';
|
|
return parsedDate.toLocaleDateString('en-US', {
|
|
month: 'long',
|
|
day: 'numeric',
|
|
year: 'numeric'
|
|
});
|
|
};
|
|
|
|
const formatRelativeTime = (date: string) => {
|
|
const now = new Date();
|
|
const then = new Date(date);
|
|
const diff = now.getTime() - then.getTime();
|
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
|
|
if (days === 0) return 'Today';
|
|
if (days === 1) return 'Yesterday';
|
|
if (days < 7) return `${days} days ago`;
|
|
if (days < 30) return `${Math.floor(days / 7)} weeks ago`;
|
|
if (days < 365) return `${Math.floor(days / 30)} months ago`;
|
|
return `${Math.floor(days / 365)} years ago`;
|
|
};
|
|
|
|
const openImageLightbox = () => {
|
|
// TODO: Implement image lightbox
|
|
};
|
|
|
|
const markDuesPaid = () => {
|
|
if (props.member) {
|
|
emit('mark-dues-paid', props.member);
|
|
}
|
|
};
|
|
|
|
const sendEmail = () => {
|
|
if (props.member) {
|
|
window.location.href = `mailto:${props.member.email}`;
|
|
}
|
|
};
|
|
|
|
const callPhone = () => {
|
|
if (props.member && props.member.phone) {
|
|
window.location.href = `tel:${props.member.phone}`;
|
|
}
|
|
};
|
|
|
|
const viewPaymentHistory = () => {
|
|
activeTab.value = 'payments';
|
|
};
|
|
|
|
const generateInvoice = () => {
|
|
// TODO: Generate invoice for member
|
|
};
|
|
|
|
const exportMemberData = () => {
|
|
// TODO: Export member data
|
|
};
|
|
|
|
const recordPayment = () => {
|
|
// TODO: Record payment for member
|
|
};
|
|
|
|
const addNote = () => {
|
|
// Focus on notes textarea
|
|
activeTab.value = 'notes';
|
|
};
|
|
|
|
const saveNotes = () => {
|
|
// TODO: Save notes to database
|
|
};
|
|
|
|
// Load member-specific data when dialog opens
|
|
watch(() => props.modelValue, (newVal) => {
|
|
if (newVal && props.member) {
|
|
// Reset to overview tab
|
|
activeTab.value = 'overview';
|
|
// Load member notes
|
|
memberNotes.value = props.member.notes || '';
|
|
// TODO: Load payment history and activities
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.member-modal {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.member-hero-header {
|
|
position: relative;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
padding: 3rem 2rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.close-btn {
|
|
position: absolute;
|
|
top: 1rem;
|
|
right: 1rem;
|
|
z-index: 1;
|
|
}
|
|
|
|
.hero-content {
|
|
position: relative;
|
|
z-index: 0;
|
|
}
|
|
|
|
.quick-actions-bar {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
padding: 1rem;
|
|
background-color: #f5f5f5;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.info-card {
|
|
border: 1px solid #e0e0e0;
|
|
}
|
|
|
|
.info-grid {
|
|
display: grid;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.info-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.info-item label {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
color: #666;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.info-item p {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
color: #333;
|
|
}
|
|
|
|
.info-item a {
|
|
text-decoration: none;
|
|
}
|
|
|
|
.info-item a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
</style> |