229 lines
6.3 KiB
Vue
229 lines
6.3 KiB
Vue
<template>
|
|
<div
|
|
:class="[
|
|
'glass rounded-glass p-5 transition-all duration-300',
|
|
'hover:shadow-glass-lg hover:-translate-y-0.5 group'
|
|
]"
|
|
>
|
|
<!-- Header Section -->
|
|
<div class="flex items-start justify-between mb-4">
|
|
<!-- Member Info -->
|
|
<div class="flex items-center gap-3">
|
|
<div class="relative">
|
|
<img
|
|
:src="member.avatar || '/default-avatar.png'"
|
|
:alt="member.name"
|
|
class="w-12 h-12 rounded-full object-cover ring-2 ring-white/60"
|
|
>
|
|
<!-- Country Flag Badge -->
|
|
<div
|
|
v-if="member.countryCode"
|
|
class="absolute -bottom-1 -right-1 w-6 h-6 rounded-full overflow-hidden ring-2 ring-white"
|
|
>
|
|
<CountryFlag :country="member.countryCode" size="small" />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 class="font-semibold text-gray-800">{{ member.name }}</h4>
|
|
<p class="text-sm text-gray-500">Member #{{ member.id }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Badge -->
|
|
<span
|
|
:class="[
|
|
'px-3 py-1 rounded-full text-xs font-medium',
|
|
status === 'overdue'
|
|
? 'bg-red-50 text-red-700 border border-red-200'
|
|
: status === 'upcoming'
|
|
? 'bg-amber-50 text-amber-700 border border-amber-200'
|
|
: 'bg-green-50 text-green-700 border border-green-200'
|
|
]"
|
|
>
|
|
{{ statusLabel }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Dues Information -->
|
|
<div class="space-y-3 mb-4">
|
|
<!-- Amount -->
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm text-gray-600">Amount Due</span>
|
|
<span class="text-lg font-bold text-monaco-600">
|
|
${{ member.dueAmount }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Due Date -->
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm text-gray-600">Due Date</span>
|
|
<span class="text-sm font-medium text-gray-800">
|
|
{{ formatDate(member.dueDate) }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Days Status -->
|
|
<div
|
|
v-if="daysUntilDue !== null"
|
|
class="flex items-center justify-between"
|
|
>
|
|
<span class="text-sm text-gray-600">
|
|
{{ daysUntilDue > 0 ? 'Days Until Due' : 'Days Overdue' }}
|
|
</span>
|
|
<span
|
|
:class="[
|
|
'text-sm font-medium',
|
|
daysUntilDue > 7
|
|
? 'text-gray-800'
|
|
: daysUntilDue > 0
|
|
? 'text-amber-600'
|
|
: 'text-red-600'
|
|
]"
|
|
>
|
|
{{ Math.abs(daysUntilDue) }} {{ Math.abs(daysUntilDue) === 1 ? 'day' : 'days' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions Section -->
|
|
<div class="flex gap-2">
|
|
<!-- Mark as Paid - Subtle Design -->
|
|
<button
|
|
@click="$emit('mark-paid', member)"
|
|
class="flex-1 px-3 py-2 rounded-lg bg-white/50 border border-monaco-200
|
|
text-monaco-600 text-sm font-medium hover:bg-glass-monaco-soft
|
|
hover:border-monaco-300 transition-all duration-200
|
|
flex items-center justify-center gap-2"
|
|
>
|
|
<Check class="w-4 h-4" />
|
|
Mark Paid
|
|
</button>
|
|
|
|
<!-- More Options Dropdown -->
|
|
<div class="relative">
|
|
<button
|
|
@click="showDropdown = !showDropdown"
|
|
class="p-2 rounded-lg bg-white/50 border border-gray-200
|
|
text-gray-600 hover:bg-gray-50 hover:border-gray-300
|
|
transition-all duration-200"
|
|
>
|
|
<MoreVertical class="w-4 h-4" />
|
|
</button>
|
|
|
|
<!-- Dropdown Menu -->
|
|
<transition name="dropdown">
|
|
<div
|
|
v-if="showDropdown"
|
|
class="absolute right-0 mt-2 w-48 glass-ultra rounded-xl shadow-lg
|
|
border border-white/60 overflow-hidden z-50"
|
|
>
|
|
<button
|
|
v-for="action in dropdownActions"
|
|
:key="action.label"
|
|
@click="handleAction(action)"
|
|
class="w-full px-4 py-3 text-left text-sm text-gray-700
|
|
hover:bg-glass-monaco-soft hover:text-monaco-600
|
|
transition-colors flex items-center gap-3"
|
|
>
|
|
<component :is="action.icon" class="w-4 h-4" />
|
|
{{ action.label }}
|
|
</button>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
import {
|
|
Check,
|
|
MoreVertical,
|
|
Mail,
|
|
Phone,
|
|
FileText,
|
|
Calendar,
|
|
AlertCircle
|
|
} from 'lucide-vue-next'
|
|
|
|
const props = defineProps({
|
|
member: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
status: {
|
|
type: String,
|
|
default: 'upcoming',
|
|
validator: (value) => ['overdue', 'upcoming', 'paid'].includes(value)
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['mark-paid', 'send-reminder', 'view-details', 'schedule-payment'])
|
|
|
|
const showDropdown = ref(false)
|
|
|
|
const statusLabel = computed(() => {
|
|
const labels = {
|
|
overdue: 'Overdue',
|
|
upcoming: 'Upcoming',
|
|
paid: 'Paid'
|
|
}
|
|
return labels[props.status] || 'Upcoming'
|
|
})
|
|
|
|
const daysUntilDue = computed(() => {
|
|
if (!props.member.dueDate) return null
|
|
|
|
const today = new Date()
|
|
const dueDate = new Date(props.member.dueDate)
|
|
const diffTime = dueDate - today
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
|
|
|
return diffDays
|
|
})
|
|
|
|
const dropdownActions = [
|
|
{ icon: Mail, label: 'Send Reminder', action: 'send-reminder' },
|
|
{ icon: Calendar, label: 'Schedule Payment', action: 'schedule-payment' },
|
|
{ icon: FileText, label: 'View Details', action: 'view-details' },
|
|
{ icon: Phone, label: 'Contact Member', action: 'contact' },
|
|
{ icon: AlertCircle, label: 'Report Issue', action: 'report' }
|
|
]
|
|
|
|
const handleAction = (action) => {
|
|
showDropdown.value = false
|
|
emit(action.action, props.member)
|
|
}
|
|
|
|
const formatDate = (date) => {
|
|
if (!date) return 'Not set'
|
|
return new Date(date).toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric'
|
|
})
|
|
}
|
|
|
|
// Placeholder for CountryFlag component
|
|
const CountryFlag = {
|
|
name: 'CountryFlag',
|
|
props: ['country', 'size'],
|
|
template: '<span></span>'
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Dropdown animation */
|
|
.dropdown-enter-active,
|
|
.dropdown-leave-active {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.dropdown-enter-from,
|
|
.dropdown-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
</style> |