diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index c82c334..17a7edd 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -81,7 +81,19 @@
"Read(/Z:\\Repos\\monacousa-portal\\layouts/**)",
"Read(/Z:\\Repos\\monacousa-portal\\layouts/**)",
"Read(/Z:\\Repos\\monacousa-portal\\layouts/**)",
- "Read(/Z:\\Repos\\monacousa-portal\\utils/**)"
+ "Read(/Z:\\Repos\\monacousa-portal\\utils/**)",
+ "Read(/Z:\\Repos\\monacousa-portal\\server\\api\\members\\[id]/**)",
+ "Read(/Z:\\Repos\\monacousa-portal\\components/**)",
+ "Read(/Z:\\Repos\\monacousa-portal\\server\\utils/**)",
+ "Read(/Z:\\Repos\\monacousa-portal\\pages\\admin\\payments/**)",
+ "Read(/Z:\\Repos\\monacousa-portal\\server\\utils/**)",
+ "Read(/Z:\\Repos\\monacousa-portal\\pages\\admin\\members/**)",
+ "Read(/Z:\\Repos\\monacousa-portal\\pages\\admin\\members/**)",
+ "Read(/Z:\\Repos\\monacousa-portal\\components/**)",
+ "Read(/Z:\\Repos\\monacousa-portal\\components/**)",
+ "Read(/Z:\\Repos\\monacousa-portal\\server\\api/**)",
+ "Read(/Z:\\Repos\\monacousa-portal\\pages\\admin\\members/**)",
+ "Bash(git pull:*)"
],
"deny": [],
"ask": []
diff --git a/pages/admin/members/index.vue b/pages/admin/members/index.vue
index ec37071..4956347 100644
--- a/pages/admin/members/index.vue
+++ b/pages/admin/members/index.vue
@@ -177,7 +177,7 @@
/>
{{ item.first_name }} {{ item.last_name }}
-
Member ID: {{ item.member_id }}
+
ID: {{ item.member_id || `Pending (DB ID: ${item.Id})` }}
@@ -710,7 +710,8 @@ const loadMembers = async () => {
const duesPaidThisYear = lastPaid && lastPaid.getFullYear() === currentYear;
return {
- member_id: member.Id || member.id,
+ ...member, // Keep all original fields including Id for API calls
+ member_id: member.member_id || '', // Use the actual member_id field
first_name: member.first_name,
last_name: member.last_name,
name: `${member.last_name || ''}, ${member.first_name || ''}`.trim(),
@@ -721,6 +722,8 @@ const loadMembers = async () => {
dues_status: member.dues_status || (duesPaidThisYear ? 'Paid' : 'Due'),
dues_paid_this_year: duesPaidThisYear,
last_dues_paid: member.last_dues_paid,
+ membership_date_paid: member.membership_date_paid,
+ payment_due_date: member.payment_due_date,
join_date: member.member_since || member.created_at,
phone: member.phone_number || member.phone || ''
};
diff --git a/pages/admin/payments/index.vue b/pages/admin/payments/index.vue
index 450b691..cadd449 100644
--- a/pages/admin/payments/index.vue
+++ b/pages/admin/payments/index.vue
@@ -509,7 +509,7 @@ const loadPayments = async () => {
// If member has dues due/overdue, create a pending payment record
if (member.dues_status === 'Due' || member.dues_status === 'Overdue') {
- const dueDate = member.dues_paid_until ? new Date(member.dues_paid_until) : null;
+ const dueDate = member.payment_due_date ? new Date(member.payment_due_date) : null;
if (dueDate) {
paymentRecords.push({
id: transactionCounter++,
diff --git a/server/api/admin/fix-payment-dates.post.ts b/server/api/admin/fix-payment-dates.post.ts
new file mode 100644
index 0000000..fb3d3b3
--- /dev/null
+++ b/server/api/admin/fix-payment-dates.post.ts
@@ -0,0 +1,132 @@
+// server/api/admin/fix-payment-dates.post.ts
+export default defineEventHandler(async (event) => {
+ try {
+ const { getMembers, updateMember } = await import('~/server/utils/nocodb');
+
+ console.log('[api/admin/fix-payment-dates.post] Starting payment date migration...');
+
+ // Get all members
+ const membersResponse = await getMembers();
+ const members = membersResponse?.list || [];
+
+ if (members.length === 0) {
+ return {
+ success: true,
+ message: 'No members found to process',
+ stats: {
+ total: 0,
+ fixed: 0,
+ skipped: 0,
+ failed: 0
+ }
+ };
+ }
+
+ const results = {
+ total: members.length,
+ fixed: 0,
+ skipped: 0,
+ failed: 0,
+ errors: [] as any[]
+ };
+
+ // Process each member
+ for (const member of members) {
+ try {
+ // Check if member has membership_date_paid but no payment_due_date
+ if (member.membership_date_paid && !member.payment_due_date) {
+ const paymentDate = new Date(member.membership_date_paid);
+
+ // Calculate payment_due_date as 1 year from payment date
+ const dueDate = new Date(paymentDate);
+ dueDate.setFullYear(dueDate.getFullYear() + 1);
+ const dueDateStr = dueDate.toISOString().split('T')[0];
+
+ console.log(`[api/admin/fix-payment-dates.post] Fixing dates for ${member.first_name} ${member.last_name} (ID: ${member.Id})`);
+ console.log(` Payment Date: ${member.membership_date_paid}`);
+ console.log(` New Due Date: ${dueDateStr}`);
+
+ // Update the member
+ await updateMember(member.Id, {
+ payment_due_date: dueDateStr
+ });
+
+ results.fixed++;
+ console.log(`[api/admin/fix-payment-dates.post] ✅ Fixed payment dates for member ${member.Id}`);
+
+ } else if (member.membership_date_paid && member.payment_due_date) {
+ // Member already has both dates, skip
+ results.skipped++;
+ console.log(`[api/admin/fix-payment-dates.post] Skipped member ${member.Id} - already has payment_due_date`);
+
+ } else if (!member.membership_date_paid && !member.payment_due_date) {
+ // Member hasn't paid yet, check if they're new (within grace period)
+ if (member.member_since) {
+ const joinDate = new Date(member.member_since);
+ const gracePeriodEnd = new Date(joinDate);
+ gracePeriodEnd.setMonth(gracePeriodEnd.getMonth() + 1); // 1 month grace period
+
+ const now = new Date();
+ if (now < gracePeriodEnd) {
+ // Still in grace period, set payment_due_date to end of grace period
+ const dueDateStr = gracePeriodEnd.toISOString().split('T')[0];
+
+ await updateMember(member.Id, {
+ payment_due_date: dueDateStr
+ });
+
+ results.fixed++;
+ console.log(`[api/admin/fix-payment-dates.post] Set grace period due date for new member ${member.Id}`);
+ } else {
+ // Past grace period, set due date to 1 year from join date
+ const dueDate = new Date(joinDate);
+ dueDate.setFullYear(dueDate.getFullYear() + 1);
+ const dueDateStr = dueDate.toISOString().split('T')[0];
+
+ await updateMember(member.Id, {
+ payment_due_date: dueDateStr
+ });
+
+ results.fixed++;
+ console.log(`[api/admin/fix-payment-dates.post] Set overdue date for member ${member.Id}`);
+ }
+ } else {
+ results.skipped++;
+ console.log(`[api/admin/fix-payment-dates.post] Skipped member ${member.Id} - no payment or join date`);
+ }
+ } else {
+ results.skipped++;
+ }
+
+ } catch (error: any) {
+ console.error(`[api/admin/fix-payment-dates.post] ❌ Failed to fix dates for member ${member.Id}:`, error);
+ results.failed++;
+ results.errors.push({
+ memberId: member.Id,
+ name: `${member.first_name} ${member.last_name}`,
+ error: error.message || 'Unknown error'
+ });
+ }
+ }
+
+ const message = `Payment date migration complete!\n` +
+ `Fixed: ${results.fixed} members\n` +
+ `Skipped: ${results.skipped} members\n` +
+ `Failed: ${results.failed} members`;
+
+ console.log(`[api/admin/fix-payment-dates.post] ${message}`);
+
+ return {
+ success: results.failed === 0,
+ message,
+ stats: results
+ };
+
+ } catch (error: any) {
+ console.error('[api/admin/fix-payment-dates.post] Error:', error);
+ throw createError({
+ statusCode: error.statusCode || 500,
+ statusMessage: error.message || 'Failed to fix payment dates'
+ });
+ }
+});
\ No newline at end of file
diff --git a/server/api/admin/generate-member-ids.post.ts b/server/api/admin/generate-member-ids.post.ts
new file mode 100644
index 0000000..0d4bc7f
--- /dev/null
+++ b/server/api/admin/generate-member-ids.post.ts
@@ -0,0 +1,119 @@
+// server/api/admin/generate-member-ids.post.ts
+export default defineEventHandler(async (event) => {
+ try {
+ const body = await readBody(event).catch(() => ({}));
+ const { forceRegenerate = false } = body;
+
+ const { getMembers, updateMember } = await import('~/server/utils/nocodb');
+ const { generateUniqueMemberId } = await import('~/server/utils/member-id');
+
+ console.log('[api/admin/generate-member-ids.post] Starting member ID generation...');
+
+ // Get all members
+ const membersResponse = await getMembers();
+ const members = membersResponse?.list || [];
+
+ if (members.length === 0) {
+ return {
+ success: true,
+ message: 'No members found to process',
+ stats: {
+ total: 0,
+ generated: 0,
+ skipped: 0,
+ failed: 0
+ }
+ };
+ }
+
+ // Filter members that need IDs (unless force regenerate)
+ const membersToProcess = forceRegenerate
+ ? members
+ : members.filter((member: any) => !member.member_id || member.member_id.trim() === '');
+
+ console.log(`[api/admin/generate-member-ids.post] Found ${membersToProcess.length} members to process (out of ${members.length} total)`);
+
+ const results = {
+ total: members.length,
+ generated: 0,
+ skipped: members.length - membersToProcess.length,
+ failed: 0,
+ errors: [] as any[]
+ };
+
+ // Process each member
+ for (const member of membersToProcess) {
+ try {
+ // Generate unique member ID
+ const memberID = await generateUniqueMemberId();
+
+ console.log(`[api/admin/generate-member-ids.post] Generated ID ${memberID} for ${member.first_name} ${member.last_name} (ID: ${member.Id})`);
+
+ // Update the member with the new ID
+ await updateMember(member.Id, {
+ member_id: memberID
+ });
+
+ results.generated++;
+
+ console.log(`[api/admin/generate-member-ids.post] ✅ Successfully assigned ID ${memberID} to member ${member.Id}`);
+
+ } catch (error: any) {
+ console.error(`[api/admin/generate-member-ids.post] ❌ Failed to generate ID for member ${member.Id}:`, error);
+ results.failed++;
+ results.errors.push({
+ memberId: member.Id,
+ name: `${member.first_name} ${member.last_name}`,
+ error: error.message || 'Unknown error'
+ });
+ }
+ }
+
+ // Calculate and update payment_due_date for all members with membership_date_paid
+ console.log('[api/admin/generate-member-ids.post] Updating payment due dates for paid members...');
+
+ let duesDatesFixed = 0;
+ for (const member of members) {
+ if (member.membership_date_paid && !member.payment_due_date) {
+ try {
+ const paymentDate = new Date(member.membership_date_paid);
+ const dueDate = new Date(paymentDate);
+ dueDate.setFullYear(dueDate.getFullYear() + 1);
+
+ await updateMember(member.Id, {
+ payment_due_date: dueDate.toISOString().split('T')[0]
+ });
+
+ duesDatesFixed++;
+ console.log(`[api/admin/generate-member-ids.post] Fixed payment_due_date for member ${member.Id}`);
+ } catch (error) {
+ console.error(`[api/admin/generate-member-ids.post] Failed to fix payment_due_date for member ${member.Id}:`, error);
+ }
+ }
+ }
+
+ const message = `Member ID generation complete!\n` +
+ `Generated: ${results.generated} IDs\n` +
+ `Skipped: ${results.skipped} (already have IDs)\n` +
+ `Failed: ${results.failed}\n` +
+ `Payment Due Dates Fixed: ${duesDatesFixed}`;
+
+ console.log(`[api/admin/generate-member-ids.post] ${message}`);
+
+ return {
+ success: results.failed === 0,
+ message,
+ stats: {
+ ...results,
+ duesDatesFixed
+ }
+ };
+
+ } catch (error: any) {
+ console.error('[api/admin/generate-member-ids.post] Error:', error);
+ throw createError({
+ statusCode: error.statusCode || 500,
+ statusMessage: error.message || 'Failed to generate member IDs'
+ });
+ }
+});
\ No newline at end of file
diff --git a/server/api/members/[id]/mark-dues-paid.post.ts b/server/api/members/[id]/mark-dues-paid.post.ts
index 5f1b1dc..b4e9024 100644
--- a/server/api/members/[id]/mark-dues-paid.post.ts
+++ b/server/api/members/[id]/mark-dues-paid.post.ts
@@ -28,14 +28,16 @@ export default defineEventHandler(async (event) => {
// Determine payment date - use custom date if provided, otherwise today
let paymentDate: string;
+ let paymentDateObj: Date;
+
if (customPaymentDate) {
try {
// Validate the custom date
- const parsedDate = new Date(customPaymentDate);
- if (isNaN(parsedDate.getTime())) {
+ paymentDateObj = new Date(customPaymentDate);
+ if (isNaN(paymentDateObj.getTime())) {
throw new Error('Invalid date format');
}
- paymentDate = parsedDate.toISOString().split('T')[0]; // YYYY-MM-DD format
+ paymentDate = paymentDateObj.toISOString().split('T')[0]; // YYYY-MM-DD format
} catch (error) {
throw createError({
statusCode: 400,
@@ -44,15 +46,21 @@ export default defineEventHandler(async (event) => {
}
} else {
// Default to today if no custom date provided
- paymentDate = new Date().toISOString().split('T')[0];
+ paymentDateObj = new Date();
+ paymentDate = paymentDateObj.toISOString().split('T')[0];
}
+ // Calculate next payment due date (1 year from payment date)
+ const nextDueDate = new Date(paymentDateObj);
+ nextDueDate.setFullYear(nextDueDate.getFullYear() + 1);
+ const nextDueDateStr = nextDueDate.toISOString().split('T')[0];
+
// Prepare update data
const updateData = {
current_year_dues_paid: 'true',
membership_date_paid: paymentDate,
- membership_status: 'Active', // Ensure member is marked as active when dues are paid
- payment_due_date: undefined // Clear the due date since it's now paid
+ payment_due_date: nextDueDateStr, // Set to 1 year from payment date
+ membership_status: 'Active' // Ensure member is marked as active when dues are paid
};
// Update the member
diff --git a/server/utils/dues-calculator.ts b/server/utils/dues-calculator.ts
index c761794..82d2786 100644
--- a/server/utils/dues-calculator.ts
+++ b/server/utils/dues-calculator.ts
@@ -31,14 +31,14 @@ export interface DuesPayment {
/**
* Calculate dues status for a member
- * Uses actual dues_paid_until field from database when available
+ * Uses actual payment_due_date field from database when available
*/
export async function calculateDuesStatus(member: any): Promise {
const now = new Date();
- // First check if member has dues_paid_until field
- if (member.dues_paid_until) {
- const paidUntil = new Date(member.dues_paid_until);
+ // First check if member has payment_due_date field
+ if (member.payment_due_date) {
+ const paidUntil = new Date(member.payment_due_date);
const isDue = paidUntil < now;
const daysUntilDue = isDue ? null : Math.floor((paidUntil.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
const daysOverdue = isDue ? Math.floor((now.getTime() - paidUntil.getTime()) / (1000 * 60 * 60 * 24)) : null;
@@ -174,7 +174,7 @@ export async function recordDuesPayment(payment: DuesPayment): Promise<{ success
// Update member record with payment information
await updateMember(payment.memberId, {
last_dues_paid: payment.paymentDate.toISOString(),
- dues_paid_until: payment.paidUntil.toISOString(),
+ payment_due_date: payment.paidUntil.toISOString(),
dues_amount: payment.amount,
last_payment_method: payment.paymentMethod,
last_transaction_id: payment.transactionId
@@ -309,7 +309,7 @@ export async function bulkUpdateDuesDates(
for (const memberId of memberIds) {
try {
await updateMember(memberId, {
- dues_paid_until: paidUntil.toISOString()
+ payment_due_date: paidUntil.toISOString()
});
results.success++;
} catch (error: any) {
diff --git a/server/utils/member-id.ts b/server/utils/member-id.ts
index 5615464..46cc60a 100644
--- a/server/utils/member-id.ts
+++ b/server/utils/member-id.ts
@@ -2,24 +2,44 @@ import { getMembers, updateMember } from './nocodb';
import type { Member } from '~/utils/types';
/**
- * Generates a unique member ID in the format MUSA-{unique 6-digit number}
+ * Generates a unique member ID in the format MUSA-YYYY-XXXX
+ * where YYYY is the current year and XXXX is a sequential number
* Checks against existing member IDs to ensure uniqueness
* @returns Promise - The unique member ID
*/
export async function generateMemberID(): Promise {
console.log('[member-id] Generating new member ID...');
+ const currentYear = new Date().getFullYear();
let memberID: string;
let isUnique = false;
let attempts = 0;
- const maxAttempts = 100; // Prevent infinite loops
+ const maxAttempts = 1000; // Prevent infinite loops
+
+ // Get all existing member IDs for this year to find the highest number
+ const members = await getMembers();
+ const memberList = Array.isArray(members) ? members : members?.list || [];
+
+ // Filter member IDs that match the current year format
+ const yearPrefix = `MUSA-${currentYear}-`;
+ const existingYearIds = memberList
+ .filter((member: Member) => member.member_id?.startsWith(yearPrefix))
+ .map((member: Member) => {
+ const parts = member.member_id!.split('-');
+ return parts.length === 3 ? parseInt(parts[2], 10) : 0;
+ })
+ .filter((num: number) => !isNaN(num));
+
+ // Find the highest number used this year
+ const highestNumber = existingYearIds.length > 0 ? Math.max(...existingYearIds) : 0;
+ let nextNumber = highestNumber + 1;
while (!isUnique && attempts < maxAttempts) {
attempts++;
- // Generate a 6-digit number (100000 to 999999)
- const uniqueNumber = Math.floor(Math.random() * 900000) + 100000;
- memberID = `MUSA-${uniqueNumber}`;
+ // Format with leading zeros (4 digits)
+ const formattedNumber = String(nextNumber).padStart(4, '0');
+ memberID = `MUSA-${currentYear}-${formattedNumber}`;
console.log(`[member-id] Attempt ${attempts}: Checking uniqueness of ${memberID}`);
@@ -28,7 +48,8 @@ export async function generateMemberID(): Promise {
isUnique = !existingMember;
if (!isUnique) {
- console.log(`[member-id] ID ${memberID} already exists, generating new one...`);
+ console.log(`[member-id] ID ${memberID} already exists, trying next number...`);
+ nextNumber++;
}
}
@@ -41,6 +62,13 @@ export async function generateMemberID(): Promise {
return memberID!;
}
+/**
+ * Alias for generateMemberID to match the import in other files
+ */
+export async function generateUniqueMemberId(): Promise {
+ return generateMemberID();
+}
+
/**
* Checks if a member ID already exists in the database
* @param memberID - The member ID to check
@@ -103,8 +131,8 @@ export function isValidMemberIDFormat(memberID: string): boolean {
return false;
}
- // Check format: MUSA-{6 digits}
- const memberIDRegex = /^MUSA-\d{6}$/;
+ // Check format: MUSA-YYYY-XXXX (year and 4 digits)
+ const memberIDRegex = /^MUSA-\d{4}-\d{4}$/;
return memberIDRegex.test(memberID);
}