monacousa-portal/components/BoardDuesManagement.vue

239 lines
6.8 KiB
Vue
Raw Normal View History

<template>
<v-card elevation="4" class="dues-management-card" style="border: 2px solid #dc2626; border-radius: 16px;">
<v-card-title class="pa-4 bg-warning-lighten-5">
<v-icon class="mr-3" color="warning" size="28">mdi-cash-multiple</v-icon>
<span class="text-h6 font-weight-bold">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) {
// Sort members alphabetically by last name, then first name
const sortByName = (a: Member, b: Member) => {
const aLastName = (a.last_name || '').toLowerCase();
const bLastName = (b.last_name || '').toLowerCase();
const aFirstName = (a.first_name || '').toLowerCase();
const bFirstName = (b.first_name || '').toLowerCase();
const lastNameCompare = aLastName.localeCompare(bLastName);
if (lastNameCompare !== 0) return lastNameCompare;
return aFirstName.localeCompare(bFirstName);
};
overdueMembers.value = (response.data.overdue || []).sort(sortByName);
upcomingMembers.value = (response.data.upcoming || []).sort(sortByName);
}
} catch (error) {
console.error('Error loading dues data:', error);
// Show error notification
} finally {
refreshLoading.value = false;
}
};
2025-08-15 16:10:12 +02:00
// Handle mark as paid - let DuesActionCard handle the date picker and API call
const handleMarkPaid = async (member: Member) => {
2025-08-15 16:10:12 +02:00
// Remove member from current lists since they've been marked as paid
overdueMembers.value = overdueMembers.value.filter(m => m.Id !== member.Id);
upcomingMembers.value = upcomingMembers.value.filter(m => m.Id !== member.Id);
2025-08-15 16:10:12 +02:00
// Emit update event
emit('member-updated', member);
// Show success message
console.log('Dues marked as paid successfully');
};
// 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>