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 @@
- {{ overdueCount }} Member{{ overdueCount > 1 ? 's' : '' }} with Overdue Dues
+ Dues Overdue - {{ overdueCount }} Member{{ overdueCount > 1 ? 's' : '' }} Affected
@@ -23,6 +23,70 @@
These accounts have been automatically marked as inactive.
+
+
+
+
+
+
+
+
+
+ {{ member.isInactive ? 'mdi-account-off' : 'mdi-account-alert' }}
+
+
+
+
+
+ {{ member.name }}
+
+
+
+ {{ member.email }}
+
+
+
+
+
+ mdi-clock-alert
+ {{ member.overdueDuration }}
+
+
+
+ {{ member.isInactive ? 'Inactive' : member.status }}
+
+
+
+
+
+
+
+
+
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`