From d9ef5bbdeba2c9df6df485dc7079ef5f1ce9f830 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 10 Aug 2025 23:42:17 +0200 Subject: [PATCH] feat: Enhanced dues overdue system with detailed time tracking - Enhanced update-overdue-statuses API to calculate and return specific overdue durations (years/months) - Updated overdue-count API to provide detailed member information with overdue durations - Enhanced DuesOverdueBanner component to display expandable list of overdue members with their specific overdue time - Added automatic marking of members as inactive when dues are over 1 year overdue - Improved UI to show 'Dues Overdue - X Members Affected' with detailed breakdown - Members with overdue dues now display exact time overdue (e.g., '2 years 3 months overdue') - Added proper TypeScript interfaces for overdue member data - Enhanced banner shows inactive status and overdue duration for each affected member --- components/DuesOverdueBanner.vue | 111 +++++++++++++++++- pages/dashboard/member-list.vue | 8 +- server/api/members/overdue-count.get.ts | 64 ++++++++-- .../members/update-overdue-statuses.post.ts | 94 ++++++++++++++- 4 files changed, 262 insertions(+), 15 deletions(-) diff --git a/components/DuesOverdueBanner.vue b/components/DuesOverdueBanner.vue index 5e1474a..ef9ec72 100644 --- a/components/DuesOverdueBanner.vue +++ b/components/DuesOverdueBanner.vue @@ -1,6 +1,6 @@ @@ -23,6 +23,70 @@ These accounts have been automatically marked as inactive.

+ + + + + + +
diff --git a/pages/dashboard/member-list.vue b/pages/dashboard/member-list.vue index 63facd4..73b112a 100644 --- a/pages/dashboard/member-list.vue +++ b/pages/dashboard/member-list.vue @@ -572,7 +572,13 @@ const createPortalAccount = async (member: Member) => { // Overdue dues handlers const loadOverdueCount = async () => { try { - const response = await $fetch<{ success: boolean; data: { count: number } }>('/api/members/overdue-count'); + const response = await $fetch<{ + success: boolean; + data: { + count: number; + overdueMembers: any[]; + } + }>('/api/members/overdue-count'); if (response.success) { overdueCount.value = response.data.count; } diff --git a/server/api/members/overdue-count.get.ts b/server/api/members/overdue-count.get.ts index cce3cf2..5f4f701 100644 --- a/server/api/members/overdue-count.get.ts +++ b/server/api/members/overdue-count.get.ts @@ -1,4 +1,35 @@ // server/api/members/overdue-count.get.ts + +// Helper function to calculate overdue duration +function calculateOverdueDuration(dueDate: Date, today: Date): { + years: number; + months: number; + totalMonths: number; + formattedDuration: string; +} { + const diffTime = today.getTime() - dueDate.getTime(); + const diffMonths = Math.floor(diffTime / (1000 * 60 * 60 * 24 * 30.44)); // Average days per month + const years = Math.floor(diffMonths / 12); + const months = diffMonths % 12; + + let formattedDuration = ''; + if (years > 0) { + formattedDuration += `${years} year${years !== 1 ? 's' : ''}`; + if (months > 0) { + formattedDuration += ` ${months} month${months !== 1 ? 's' : ''}`; + } + } else { + formattedDuration = `${diffMonths} month${diffMonths !== 1 ? 's' : ''}`; + } + + return { + years, + months, + totalMonths: diffMonths, + formattedDuration: `${formattedDuration} overdue` + }; +} + export default defineEventHandler(async (event) => { try { const { getMembers } = await import('~/server/utils/nocodb'); @@ -9,16 +40,19 @@ export default defineEventHandler(async (event) => { if (!allMembers?.list) { return { success: true, - data: { count: 0 } + data: { + count: 0, + overdueMembers: [] + } }; } const today = new Date(); - let severelyOverdueCount = 0; + const overdueMembers: any[] = []; for (const member of allMembers.list) { // Check for severely overdue members (more than 1 year past due date) - let isSeverelyOverdue = false; + let overdueDuration = null; 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 @@ -27,7 +61,7 @@ export default defineEventHandler(async (event) => { oneYearFromPayment.setFullYear(oneYearFromPayment.getFullYear() + 1); if (today > oneYearFromPayment) { - isSeverelyOverdue = true; + overdueDuration = calculateOverdueDuration(oneYearFromPayment, today); } } else if (member.current_year_dues_paid !== 'true') { // If dues are not paid, check payment due date or member since date @@ -49,18 +83,32 @@ export default defineEventHandler(async (event) => { oneYearOverdue.setFullYear(oneYearOverdue.getFullYear() + 1); if (today > oneYearOverdue) { - isSeverelyOverdue = true; + overdueDuration = calculateOverdueDuration(oneYearOverdue, today); } } - if (isSeverelyOverdue) { - severelyOverdueCount++; + if (overdueDuration) { + overdueMembers.push({ + id: member.Id, + name: member.FullName || `${member.first_name} ${member.last_name}`, + email: member.email, + status: member.membership_status, + overdueDuration: overdueDuration.formattedDuration, + totalMonthsOverdue: overdueDuration.totalMonths, + isInactive: member.membership_status === 'Inactive' + }); } } + // Sort by most overdue first + overdueMembers.sort((a, b) => b.totalMonthsOverdue - a.totalMonthsOverdue); + return { success: true, - data: { count: severelyOverdueCount } + data: { + count: overdueMembers.length, + overdueMembers: overdueMembers + } }; } catch (error: any) { diff --git a/server/api/members/update-overdue-statuses.post.ts b/server/api/members/update-overdue-statuses.post.ts index 423489c..acb8df1 100644 --- a/server/api/members/update-overdue-statuses.post.ts +++ b/server/api/members/update-overdue-statuses.post.ts @@ -1,4 +1,35 @@ // server/api/members/update-overdue-statuses.post.ts + +// Helper function to calculate overdue duration +function calculateOverdueDuration(dueDate: Date, today: Date): { + years: number; + months: number; + totalMonths: number; + formattedDuration: string; +} { + const diffTime = today.getTime() - dueDate.getTime(); + const diffMonths = Math.floor(diffTime / (1000 * 60 * 60 * 24 * 30.44)); // Average days per month + const years = Math.floor(diffMonths / 12); + const months = diffMonths % 12; + + let formattedDuration = ''; + if (years > 0) { + formattedDuration += `${years} year${years !== 1 ? 's' : ''}`; + if (months > 0) { + formattedDuration += ` ${months} month${months !== 1 ? 's' : ''}`; + } + } else { + formattedDuration = `${diffMonths} month${diffMonths !== 1 ? 's' : ''}`; + } + + return { + years, + months, + totalMonths: diffMonths, + formattedDuration: `${formattedDuration} overdue` + }; +} + export default defineEventHandler(async (event) => { try { const { getMembers, updateMember } = await import('~/server/utils/nocodb'); @@ -16,15 +47,57 @@ export default defineEventHandler(async (event) => { const today = new Date(); const membersToUpdate: any[] = []; + const overdueDetails: any[] = []; for (const member of allMembers.list) { // Skip if already inactive if (member.membership_status === 'Inactive') { + // Still check if this inactive member is overdue to include in details + let overdueDuration = null; + + if (member.current_year_dues_paid === 'true' && member.membership_date_paid) { + const lastPaidDate = new Date(member.membership_date_paid); + const oneYearFromPayment = new Date(lastPaidDate); + oneYearFromPayment.setFullYear(oneYearFromPayment.getFullYear() + 1); + + if (today > oneYearFromPayment) { + overdueDuration = calculateOverdueDuration(oneYearFromPayment, today); + } + } else if (member.current_year_dues_paid !== 'true') { + let dueDate: Date; + + if (member.payment_due_date) { + dueDate = new Date(member.payment_due_date); + } else if (member.member_since) { + dueDate = new Date(member.member_since); + dueDate.setFullYear(dueDate.getFullYear() + 1); + } else { + continue; + } + + const oneYearOverdue = new Date(dueDate); + oneYearOverdue.setFullYear(oneYearOverdue.getFullYear() + 1); + + if (today > oneYearOverdue) { + overdueDuration = calculateOverdueDuration(oneYearOverdue, today); + } + } + + if (overdueDuration) { + overdueDetails.push({ + id: member.Id, + name: member.FullName || `${member.first_name} ${member.last_name}`, + status: 'Inactive', + overdueDuration: overdueDuration.formattedDuration, + totalMonthsOverdue: overdueDuration.totalMonths + }); + } continue; } // Check if member has overdue dues (more than 1 year) let isOverdue = false; + let overdueDuration = null; 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 @@ -34,6 +107,7 @@ export default defineEventHandler(async (event) => { if (today > oneYearFromPayment) { isOverdue = true; + overdueDuration = calculateOverdueDuration(oneYearFromPayment, today); } } else if (member.current_year_dues_paid !== 'true') { // If dues are not paid, check payment due date or member since date @@ -56,14 +130,25 @@ export default defineEventHandler(async (event) => { if (today > oneYearOverdue) { isOverdue = true; + overdueDuration = calculateOverdueDuration(oneYearOverdue, today); } } - if (isOverdue) { + if (isOverdue && overdueDuration) { membersToUpdate.push({ id: member.Id, name: member.FullName || `${member.first_name} ${member.last_name}`, - currentStatus: member.membership_status + currentStatus: member.membership_status, + overdueDuration: overdueDuration.formattedDuration, + totalMonthsOverdue: overdueDuration.totalMonths + }); + + overdueDetails.push({ + id: member.Id, + name: member.FullName || `${member.first_name} ${member.last_name}`, + status: member.membership_status, + overdueDuration: overdueDuration.formattedDuration, + totalMonthsOverdue: overdueDuration.totalMonths }); } } @@ -78,7 +163,7 @@ export default defineEventHandler(async (event) => { membership_status: 'Inactive' }); updatedCount++; - console.log(`[API] Marked member ${memberInfo.name} (${memberInfo.id}) as inactive due to overdue dues`); + console.log(`[API] Marked member ${memberInfo.name} (${memberInfo.id}) as inactive - ${memberInfo.overdueDuration}`); } catch (error: any) { console.error(`[API] Failed to update member ${memberInfo.name} (${memberInfo.id}):`, error); errors.push(`Failed to update ${memberInfo.name}: ${error.message}`); @@ -91,7 +176,8 @@ export default defineEventHandler(async (event) => { success: true, data: { updatedCount, - totalOverdue: membersToUpdate.length, + totalOverdue: overdueDetails.length, + overdueDetails: overdueDetails.sort((a, b) => b.totalMonthsOverdue - a.totalMonthsOverdue), // Sort by most overdue first errors: errors.length > 0 ? errors : undefined }, message: `Successfully updated ${updatedCount} overdue member${updatedCount !== 1 ? 's' : ''} to inactive status`