diff --git a/components/DuesOverdueBanner.vue b/components/DuesOverdueBanner.vue new file mode 100644 index 0000000..5e1474a --- /dev/null +++ b/components/DuesOverdueBanner.vue @@ -0,0 +1,170 @@ + + + + mdi-alert-circle + + + + + {{ overdueCount }} Member{{ overdueCount > 1 ? 's' : '' }} with Overdue Dues + + + + + + {{ overdueCount }} member{{ overdueCount > 1 ? 's have' : ' has' }} dues that are more than 1 year overdue. + These accounts have been automatically marked as inactive. + + + + + mdi-eye + View Overdue Members + + + + mdi-refresh + Update Member Statuses + + + + mdi-email-multiple + Send Reminders + + + + + + mdi-close + + + + + + + + + diff --git a/pages/dashboard/admin.vue b/pages/dashboard/admin.vue index 0114436..21d9c36 100644 --- a/pages/dashboard/admin.vue +++ b/pages/dashboard/admin.vue @@ -112,6 +112,29 @@ + + + + + + + + + + @@ -390,6 +413,11 @@ const showRecaptchaConfig = ref(false); const showMembershipConfig = ref(false); const showEmailConfig = ref(false); +// Dues management +const overdueCount = ref(0); +const overdueRefreshTrigger = ref(0); +const duesRefreshTrigger = ref(0); + // Create user dialog data const createUserValid = ref(false); const creatingUser = ref(false); @@ -622,9 +650,60 @@ const systemMaintenance = () => { // TODO: Implement maintenance mode }; -// Load stats on component mount +// Dues management handlers +const loadOverdueCount = async () => { + try { + const response = await $fetch<{ success: boolean; data: { count: number } }>('/api/members/overdue-count'); + if (response.success) { + overdueCount.value = response.data.count; + } + } catch (error: any) { + console.error('Error loading overdue count:', error); + } +}; + +const viewOverdueMembers = () => { + // Navigate to member list with overdue filter applied + navigateTo('/dashboard/member-list'); +}; + +const sendDuesReminders = () => { + // Placeholder for dues reminder functionality + console.log('Send dues reminders - feature to be implemented'); +}; + +const handleStatusesUpdated = async (updatedCount: number) => { + console.log(`Successfully updated ${updatedCount} member${updatedCount !== 1 ? 's' : ''} to inactive status`); + + // Refresh overdue count + await loadOverdueCount(); + + // Trigger banner refresh + overdueRefreshTrigger.value += 1; +}; + +const handleViewMember = (member: any) => { + // Navigate to member details or open modal + console.log('View member:', member.FullName || `${member.first_name} ${member.last_name}`); + navigateTo('/dashboard/member-list'); +}; + +const navigateToMembers = () => { + // Navigate to member list page + navigateTo('/dashboard/member-list'); +}; + +const handleMemberUpdated = (member: any) => { + console.log('Member updated:', member.FullName || `${member.first_name} ${member.last_name}`); + + // Trigger dues refresh + duesRefreshTrigger.value += 1; +}; + +// Load stats and overdue count on component mount onMounted(async () => { await loadStats(); + await loadOverdueCount(); }); diff --git a/pages/dashboard/member-list.vue b/pages/dashboard/member-list.vue index 450c96e..63facd4 100644 --- a/pages/dashboard/member-list.vue +++ b/pages/dashboard/member-list.vue @@ -13,6 +13,17 @@ + + + @@ -290,6 +301,10 @@ const successMessage = ref(''); // Portal account creation const creatingPortalAccountIds = ref([]); +// Overdue dues management +const overdueCount = ref(0); +const overdueRefreshTrigger = ref(0); + // Filter options const activeFilterOptions = [ { title: 'Active Members', value: 'active' }, @@ -554,9 +569,50 @@ const createPortalAccount = async (member: Member) => { } }; -// Load members on mount -onMounted(() => { - loadMembers(); +// Overdue dues handlers +const loadOverdueCount = async () => { + try { + const response = await $fetch<{ success: boolean; data: { count: number } }>('/api/members/overdue-count'); + if (response.success) { + overdueCount.value = response.data.count; + } + } catch (error: any) { + console.error('Error loading overdue count:', error); + } +}; + +const viewOverdueMembers = () => { + // Filter to show only inactive members (who were marked inactive due to overdue dues) + activeFilter.value = 'inactive'; + duesFilter.value = 'unpaid'; + + showSuccess.value = true; + successMessage.value = 'Showing members with overdue dues (marked as inactive)'; +}; + +const sendDuesReminders = () => { + // Placeholder for dues reminder functionality + console.log('Send dues reminders - feature to be implemented'); + showSuccess.value = true; + successMessage.value = 'Dues reminder feature coming soon!'; +}; + +const handleStatusesUpdated = async (updatedCount: number) => { + showSuccess.value = true; + successMessage.value = `Successfully updated ${updatedCount} member${updatedCount !== 1 ? 's' : ''} to inactive status`; + + // Refresh members list and overdue count + await loadMembers(); + await loadOverdueCount(); + + // Trigger banner refresh + overdueRefreshTrigger.value += 1; +}; + +// Load members and overdue count on mount +onMounted(async () => { + await loadMembers(); + await loadOverdueCount(); }); diff --git a/server/api/members/dues-status.get.ts b/server/api/members/dues-status.get.ts index eeda9e9..f43e440 100644 --- a/server/api/members/dues-status.get.ts +++ b/server/api/members/dues-status.get.ts @@ -22,9 +22,51 @@ export default defineEventHandler(async (event) => { const overdueMembers: any[] = []; const upcomingMembers: any[] = []; + const severelyOverdueMembers: any[] = []; for (const member of allMembers.list) { - // Skip if dues are already paid + // Check for severely overdue members (more than 1 year past due date) + let isSeverelyOverdue = false; + + if (member.current_year_dues_paid === 'true' && member.membership_date_paid) { + // If dues are marked as paid, check if it's been more than 1 year since payment + const lastPaidDate = new Date(member.membership_date_paid); + const oneYearFromPayment = new Date(lastPaidDate); + oneYearFromPayment.setFullYear(oneYearFromPayment.getFullYear() + 1); + + if (today > oneYearFromPayment) { + isSeverelyOverdue = true; + } + } else if (member.current_year_dues_paid !== 'true') { + // If dues are not paid, check payment due date or member since date + let dueDate: Date; + + if (member.payment_due_date) { + dueDate = new Date(member.payment_due_date); + } else if (member.member_since) { + // Fallback: 1 year from member since date + dueDate = new Date(member.member_since); + dueDate.setFullYear(dueDate.getFullYear() + 1); + } else { + // Skip if we can't determine due date + continue; + } + + // Check if more than 1 year overdue + const oneYearOverdue = new Date(dueDate); + oneYearOverdue.setFullYear(oneYearOverdue.getFullYear() + 1); + + if (today > oneYearOverdue) { + isSeverelyOverdue = true; + } + } + + if (isSeverelyOverdue) { + severelyOverdueMembers.push(member); + continue; // Don't add to other lists + } + + // Skip if dues are already paid (and not severely overdue) if (member.current_year_dues_paid === 'true') { continue; } @@ -34,7 +76,7 @@ export default defineEventHandler(async (event) => { const dueDate = new Date(member.payment_due_date); if (dueDate < today) { - // Overdue + // Overdue (but not severely overdue) overdueMembers.push(member); } else if (dueDate <= thirtyDaysFromNow) { // Due within 30 days diff --git a/server/api/members/overdue-count.get.ts b/server/api/members/overdue-count.get.ts new file mode 100644 index 0000000..cce3cf2 --- /dev/null +++ b/server/api/members/overdue-count.get.ts @@ -0,0 +1,74 @@ +// server/api/members/overdue-count.get.ts +export default defineEventHandler(async (event) => { + try { + const { getMembers } = await import('~/server/utils/nocodb'); + + // Get all members + const allMembers = await getMembers(); + + if (!allMembers?.list) { + return { + success: true, + data: { count: 0 } + }; + } + + const today = new Date(); + let severelyOverdueCount = 0; + + for (const member of allMembers.list) { + // Check for severely overdue members (more than 1 year past due date) + let isSeverelyOverdue = false; + + if (member.current_year_dues_paid === 'true' && member.membership_date_paid) { + // If dues are marked as paid, check if it's been more than 1 year since payment + const lastPaidDate = new Date(member.membership_date_paid); + const oneYearFromPayment = new Date(lastPaidDate); + oneYearFromPayment.setFullYear(oneYearFromPayment.getFullYear() + 1); + + if (today > oneYearFromPayment) { + isSeverelyOverdue = true; + } + } else if (member.current_year_dues_paid !== 'true') { + // If dues are not paid, check payment due date or member since date + let dueDate: Date; + + if (member.payment_due_date) { + dueDate = new Date(member.payment_due_date); + } else if (member.member_since) { + // Fallback: 1 year from member since date + dueDate = new Date(member.member_since); + dueDate.setFullYear(dueDate.getFullYear() + 1); + } else { + // Skip if we can't determine due date + continue; + } + + // Check if more than 1 year overdue + const oneYearOverdue = new Date(dueDate); + oneYearOverdue.setFullYear(oneYearOverdue.getFullYear() + 1); + + if (today > oneYearOverdue) { + isSeverelyOverdue = true; + } + } + + if (isSeverelyOverdue) { + severelyOverdueCount++; + } + } + + return { + success: true, + data: { count: severelyOverdueCount } + }; + + } catch (error: any) { + console.error('[API] Error fetching overdue count:', error); + + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.message || 'Failed to fetch overdue count' + }); + } +}); diff --git a/server/api/members/update-overdue-statuses.post.ts b/server/api/members/update-overdue-statuses.post.ts new file mode 100644 index 0000000..423489c --- /dev/null +++ b/server/api/members/update-overdue-statuses.post.ts @@ -0,0 +1,108 @@ +// server/api/members/update-overdue-statuses.post.ts +export default defineEventHandler(async (event) => { + try { + const { getMembers, updateMember } = await import('~/server/utils/nocodb'); + + // Get all members + const allMembers = await getMembers(); + + if (!allMembers?.list) { + return { + success: true, + data: { updatedCount: 0 }, + message: 'No members found' + }; + } + + const today = new Date(); + const membersToUpdate: any[] = []; + + for (const member of allMembers.list) { + // Skip if already inactive + if (member.membership_status === 'Inactive') { + continue; + } + + // Check if member has overdue dues (more than 1 year) + let isOverdue = false; + + if (member.current_year_dues_paid === 'true' && member.membership_date_paid) { + // If dues are marked as paid, check if it's been more than 1 year since payment + const lastPaidDate = new Date(member.membership_date_paid); + const oneYearFromPayment = new Date(lastPaidDate); + oneYearFromPayment.setFullYear(oneYearFromPayment.getFullYear() + 1); + + if (today > oneYearFromPayment) { + isOverdue = true; + } + } else if (member.current_year_dues_paid !== 'true') { + // If dues are not paid, check payment due date or member since date + let dueDate: Date; + + if (member.payment_due_date) { + dueDate = new Date(member.payment_due_date); + } else if (member.member_since) { + // Fallback: 1 year from member since date + dueDate = new Date(member.member_since); + dueDate.setFullYear(dueDate.getFullYear() + 1); + } else { + // Skip if we can't determine due date + continue; + } + + // Check if more than 1 year overdue + const oneYearOverdue = new Date(dueDate); + oneYearOverdue.setFullYear(oneYearOverdue.getFullYear() + 1); + + if (today > oneYearOverdue) { + isOverdue = true; + } + } + + if (isOverdue) { + membersToUpdate.push({ + id: member.Id, + name: member.FullName || `${member.first_name} ${member.last_name}`, + currentStatus: member.membership_status + }); + } + } + + // Update all overdue members to inactive + let updatedCount = 0; + const errors: string[] = []; + + for (const memberInfo of membersToUpdate) { + try { + await updateMember(memberInfo.id, { + membership_status: 'Inactive' + }); + updatedCount++; + console.log(`[API] Marked member ${memberInfo.name} (${memberInfo.id}) as inactive due to overdue dues`); + } catch (error: any) { + console.error(`[API] Failed to update member ${memberInfo.name} (${memberInfo.id}):`, error); + errors.push(`Failed to update ${memberInfo.name}: ${error.message}`); + } + } + + console.log(`[API] Updated ${updatedCount} of ${membersToUpdate.length} overdue members to inactive status`); + + return { + success: true, + data: { + updatedCount, + totalOverdue: membersToUpdate.length, + errors: errors.length > 0 ? errors : undefined + }, + message: `Successfully updated ${updatedCount} overdue member${updatedCount !== 1 ? 's' : ''} to inactive status` + }; + + } catch (error: any) { + console.error('[API] Error updating overdue member statuses:', error); + + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.message || 'Failed to update overdue member statuses' + }); + } +});
+ {{ overdueCount }} member{{ overdueCount > 1 ? 's have' : ' has' }} dues that are more than 1 year overdue. + These accounts have been automatically marked as inactive. +