247 lines
6.5 KiB
Vue
247 lines
6.5 KiB
Vue
<template>
|
|
<v-card elevation="2" class="dues-management-card">
|
|
<v-card-title class="pa-4 bg-warning-lighten-5">
|
|
<v-icon class="mr-2" color="warning">mdi-cash-clock</v-icon>
|
|
<span class="text-h6">Dues Management</span>
|
|
<v-spacer />
|
|
<v-chip color="warning" size="small">
|
|
{{ overdueMembers.length + upcomingMembers.length }} Action Items
|
|
</v-chip>
|
|
</v-card-title>
|
|
|
|
<v-card-text class="pa-4">
|
|
<v-tabs v-model="activeTab" color="primary" class="mb-4">
|
|
<v-tab value="overdue">
|
|
<v-icon start>mdi-alert-circle</v-icon>
|
|
Overdue ({{ overdueMembers.length }})
|
|
</v-tab>
|
|
<v-tab value="upcoming">
|
|
<v-icon start>mdi-clock-alert</v-icon>
|
|
Due Soon ({{ upcomingMembers.length }})
|
|
</v-tab>
|
|
</v-tabs>
|
|
|
|
<v-tabs-window v-model="activeTab">
|
|
<!-- Overdue Dues Tab -->
|
|
<v-tabs-window-item value="overdue">
|
|
<div v-if="overdueMembers.length === 0" class="text-center py-6">
|
|
<v-icon size="48" color="success" class="mb-2">mdi-check-circle</v-icon>
|
|
<p class="text-h6 text-success">All caught up!</p>
|
|
<p class="text-body-2">No members have overdue dues.</p>
|
|
</div>
|
|
|
|
<v-row v-else>
|
|
<v-col
|
|
v-for="member in overdueMembers"
|
|
:key="member.Id"
|
|
cols="12"
|
|
md="6"
|
|
lg="4"
|
|
>
|
|
<DuesActionCard
|
|
:member="member"
|
|
status="overdue"
|
|
@mark-paid="handleMarkPaid"
|
|
@view-member="handleViewMember"
|
|
:loading="loading[member.Id]"
|
|
/>
|
|
</v-col>
|
|
</v-row>
|
|
</v-tabs-window-item>
|
|
|
|
<!-- Upcoming Dues Tab -->
|
|
<v-tabs-window-item value="upcoming">
|
|
<div v-if="upcomingMembers.length === 0" class="text-center py-6">
|
|
<v-icon size="48" color="info" class="mb-2">mdi-calendar-check</v-icon>
|
|
<p class="text-h6 text-info">All up to date!</p>
|
|
<p class="text-body-2">No upcoming dues in the next 30 days.</p>
|
|
</div>
|
|
|
|
<v-row v-else>
|
|
<v-col
|
|
v-for="member in upcomingMembers"
|
|
:key="member.Id"
|
|
cols="12"
|
|
md="6"
|
|
lg="4"
|
|
>
|
|
<DuesActionCard
|
|
:member="member"
|
|
status="upcoming"
|
|
@mark-paid="handleMarkPaid"
|
|
@view-member="handleViewMember"
|
|
:loading="loading[member.Id]"
|
|
/>
|
|
</v-col>
|
|
</v-row>
|
|
</v-tabs-window-item>
|
|
</v-tabs-window>
|
|
</v-card-text>
|
|
|
|
<!-- Refresh Button -->
|
|
<v-card-actions class="pa-4">
|
|
<v-btn
|
|
color="primary"
|
|
variant="outlined"
|
|
:loading="refreshLoading"
|
|
@click="refreshData"
|
|
>
|
|
<v-icon start>mdi-refresh</v-icon>
|
|
Refresh
|
|
</v-btn>
|
|
<v-spacer />
|
|
<v-btn
|
|
color="primary"
|
|
variant="text"
|
|
@click="$emit('view-all-members')"
|
|
>
|
|
<v-icon start>mdi-account-group</v-icon>
|
|
View All Members
|
|
</v-btn>
|
|
</v-card-actions>
|
|
|
|
<!-- View Member Dialog -->
|
|
<ViewMemberDialog
|
|
v-model="showViewDialog"
|
|
:member="selectedMember"
|
|
@edit="handleEditMember"
|
|
/>
|
|
</v-card>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Member } from '~/utils/types';
|
|
|
|
interface Props {
|
|
refreshTrigger?: number;
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'view-member', member: Member): void;
|
|
(e: 'view-all-members'): void;
|
|
(e: 'member-updated', member: Member): void;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
const emit = defineEmits<Emits>();
|
|
|
|
// State
|
|
const activeTab = ref('overdue');
|
|
const overdueMembers = ref<Member[]>([]);
|
|
const upcomingMembers = ref<Member[]>([]);
|
|
const loading = ref<Record<string, boolean>>({});
|
|
const refreshLoading = ref(false);
|
|
|
|
// View member dialog state
|
|
const showViewDialog = ref(false);
|
|
const selectedMember = ref<Member | null>(null);
|
|
|
|
// Load dues data
|
|
const loadDuesData = async () => {
|
|
refreshLoading.value = true;
|
|
|
|
try {
|
|
const response = await $fetch<{
|
|
success: boolean;
|
|
data: {
|
|
overdue: Member[];
|
|
upcoming: Member[];
|
|
};
|
|
}>('/api/members/dues-status');
|
|
|
|
if (response.success) {
|
|
overdueMembers.value = response.data.overdue || [];
|
|
upcomingMembers.value = response.data.upcoming || [];
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading dues data:', error);
|
|
// Show error notification
|
|
} finally {
|
|
refreshLoading.value = false;
|
|
}
|
|
};
|
|
|
|
// Handle mark as paid
|
|
const handleMarkPaid = async (member: Member) => {
|
|
loading.value[member.Id] = true;
|
|
|
|
try {
|
|
const response = await $fetch<{
|
|
success: boolean;
|
|
data: Member;
|
|
message?: string;
|
|
}>(`/api/members/${member.Id}/mark-dues-paid`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.success) {
|
|
// Remove member from current lists
|
|
overdueMembers.value = overdueMembers.value.filter(m => m.Id !== member.Id);
|
|
upcomingMembers.value = upcomingMembers.value.filter(m => m.Id !== member.Id);
|
|
|
|
// Emit update event
|
|
emit('member-updated', response.data);
|
|
|
|
// Show success message
|
|
console.log('Dues marked as paid successfully');
|
|
} else {
|
|
throw new Error(response.message || 'Failed to mark dues as paid');
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Error marking dues as paid:', error);
|
|
// Show error notification
|
|
} finally {
|
|
loading.value[member.Id] = false;
|
|
}
|
|
};
|
|
|
|
// Handle view member
|
|
const handleViewMember = (member: Member) => {
|
|
selectedMember.value = member;
|
|
showViewDialog.value = true;
|
|
};
|
|
|
|
// Handle edit member (from the view dialog)
|
|
const handleEditMember = (member: Member) => {
|
|
// Close the view dialog first
|
|
showViewDialog.value = false;
|
|
// Emit the view-member event which should trigger the edit dialog in the parent component
|
|
emit('view-member', member);
|
|
};
|
|
|
|
// Refresh data
|
|
const refreshData = () => {
|
|
loadDuesData();
|
|
};
|
|
|
|
// Watch for refresh trigger
|
|
watch(() => props.refreshTrigger, () => {
|
|
if (props.refreshTrigger) {
|
|
loadDuesData();
|
|
}
|
|
});
|
|
|
|
// Load data on mount
|
|
onMounted(() => {
|
|
loadDuesData();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.dues-management-card {
|
|
border-radius: 12px !important;
|
|
}
|
|
|
|
.bg-warning-lighten-5 {
|
|
background-color: rgb(var(--v-theme-warning-lighten-5)) !important;
|
|
}
|
|
|
|
.v-tab {
|
|
text-transform: none !important;
|
|
}
|
|
|
|
.v-card-title {
|
|
border-bottom: 1px solid rgba(var(--v-theme-outline), 0.12);
|
|
}
|
|
</style>
|