Fix critical member management issues: dues tracking, member IDs, and profile display
Build And Push Image / docker (push) Successful in 2m20s
Details
Build And Push Image / docker (push) Successful in 2m20s
Details
- Fix dues payment logic to automatically calculate payment_due_date as 1 year from payment date - Remove redundant dues_paid_until field and replace with payment_due_date throughout - Implement member ID generation system with format MUSA-YYYY-XXXX - Create migration endpoints for generating member IDs and fixing payment dates - Update admin members page to display actual member_id from database - Ensure ProfileAvatar components use correct member_id field - Add support for profile images in list and grid views with initials fallback - Fix countries export alias for backward compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
67bb9e32ac
commit
d34d16fda1
|
|
@ -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\\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": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-weight-medium">{{ item.first_name }} {{ item.last_name }}</div>
|
<div class="font-weight-medium">{{ item.first_name }} {{ item.last_name }}</div>
|
||||||
<div class="text-caption text-medium-emphasis">Member ID: {{ item.member_id }}</div>
|
<div class="text-caption text-medium-emphasis">ID: {{ item.member_id || `Pending (DB ID: ${item.Id})` }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -710,7 +710,8 @@ const loadMembers = async () => {
|
||||||
const duesPaidThisYear = lastPaid && lastPaid.getFullYear() === currentYear;
|
const duesPaidThisYear = lastPaid && lastPaid.getFullYear() === currentYear;
|
||||||
|
|
||||||
return {
|
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,
|
first_name: member.first_name,
|
||||||
last_name: member.last_name,
|
last_name: member.last_name,
|
||||||
name: `${member.last_name || ''}, ${member.first_name || ''}`.trim(),
|
name: `${member.last_name || ''}, ${member.first_name || ''}`.trim(),
|
||||||
|
|
@ -721,6 +722,8 @@ const loadMembers = async () => {
|
||||||
dues_status: member.dues_status || (duesPaidThisYear ? 'Paid' : 'Due'),
|
dues_status: member.dues_status || (duesPaidThisYear ? 'Paid' : 'Due'),
|
||||||
dues_paid_this_year: duesPaidThisYear,
|
dues_paid_this_year: duesPaidThisYear,
|
||||||
last_dues_paid: member.last_dues_paid,
|
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,
|
join_date: member.member_since || member.created_at,
|
||||||
phone: member.phone_number || member.phone || ''
|
phone: member.phone_number || member.phone || ''
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ const loadPayments = async () => {
|
||||||
|
|
||||||
// If member has dues due/overdue, create a pending payment record
|
// If member has dues due/overdue, create a pending payment record
|
||||||
if (member.dues_status === 'Due' || member.dues_status === 'Overdue') {
|
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) {
|
if (dueDate) {
|
||||||
paymentRecords.push({
|
paymentRecords.push({
|
||||||
id: transactionCounter++,
|
id: transactionCounter++,
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -28,14 +28,16 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
// Determine payment date - use custom date if provided, otherwise today
|
// Determine payment date - use custom date if provided, otherwise today
|
||||||
let paymentDate: string;
|
let paymentDate: string;
|
||||||
|
let paymentDateObj: Date;
|
||||||
|
|
||||||
if (customPaymentDate) {
|
if (customPaymentDate) {
|
||||||
try {
|
try {
|
||||||
// Validate the custom date
|
// Validate the custom date
|
||||||
const parsedDate = new Date(customPaymentDate);
|
paymentDateObj = new Date(customPaymentDate);
|
||||||
if (isNaN(parsedDate.getTime())) {
|
if (isNaN(paymentDateObj.getTime())) {
|
||||||
throw new Error('Invalid date format');
|
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) {
|
} catch (error) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
|
|
@ -44,15 +46,21 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Default to today if no custom date provided
|
// 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
|
// Prepare update data
|
||||||
const updateData = {
|
const updateData = {
|
||||||
current_year_dues_paid: 'true',
|
current_year_dues_paid: 'true',
|
||||||
membership_date_paid: paymentDate,
|
membership_date_paid: paymentDate,
|
||||||
membership_status: 'Active', // Ensure member is marked as active when dues are paid
|
payment_due_date: nextDueDateStr, // Set to 1 year from payment date
|
||||||
payment_due_date: undefined // Clear the due date since it's now paid
|
membership_status: 'Active' // Ensure member is marked as active when dues are paid
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the member
|
// Update the member
|
||||||
|
|
|
||||||
|
|
@ -31,14 +31,14 @@ export interface DuesPayment {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate dues status for a member
|
* 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<DuesStatus> {
|
export async function calculateDuesStatus(member: any): Promise<DuesStatus> {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// First check if member has dues_paid_until field
|
// First check if member has payment_due_date field
|
||||||
if (member.dues_paid_until) {
|
if (member.payment_due_date) {
|
||||||
const paidUntil = new Date(member.dues_paid_until);
|
const paidUntil = new Date(member.payment_due_date);
|
||||||
const isDue = paidUntil < now;
|
const isDue = paidUntil < now;
|
||||||
const daysUntilDue = isDue ? null : Math.floor((paidUntil.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
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;
|
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
|
// Update member record with payment information
|
||||||
await updateMember(payment.memberId, {
|
await updateMember(payment.memberId, {
|
||||||
last_dues_paid: payment.paymentDate.toISOString(),
|
last_dues_paid: payment.paymentDate.toISOString(),
|
||||||
dues_paid_until: payment.paidUntil.toISOString(),
|
payment_due_date: payment.paidUntil.toISOString(),
|
||||||
dues_amount: payment.amount,
|
dues_amount: payment.amount,
|
||||||
last_payment_method: payment.paymentMethod,
|
last_payment_method: payment.paymentMethod,
|
||||||
last_transaction_id: payment.transactionId
|
last_transaction_id: payment.transactionId
|
||||||
|
|
@ -309,7 +309,7 @@ export async function bulkUpdateDuesDates(
|
||||||
for (const memberId of memberIds) {
|
for (const memberId of memberIds) {
|
||||||
try {
|
try {
|
||||||
await updateMember(memberId, {
|
await updateMember(memberId, {
|
||||||
dues_paid_until: paidUntil.toISOString()
|
payment_due_date: paidUntil.toISOString()
|
||||||
});
|
});
|
||||||
results.success++;
|
results.success++;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,44 @@ import { getMembers, updateMember } from './nocodb';
|
||||||
import type { Member } from '~/utils/types';
|
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
|
* Checks against existing member IDs to ensure uniqueness
|
||||||
* @returns Promise<string> - The unique member ID
|
* @returns Promise<string> - The unique member ID
|
||||||
*/
|
*/
|
||||||
export async function generateMemberID(): Promise<string> {
|
export async function generateMemberID(): Promise<string> {
|
||||||
console.log('[member-id] Generating new member ID...');
|
console.log('[member-id] Generating new member ID...');
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
let memberID: string;
|
let memberID: string;
|
||||||
let isUnique = false;
|
let isUnique = false;
|
||||||
let attempts = 0;
|
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) {
|
while (!isUnique && attempts < maxAttempts) {
|
||||||
attempts++;
|
attempts++;
|
||||||
|
|
||||||
// Generate a 6-digit number (100000 to 999999)
|
// Format with leading zeros (4 digits)
|
||||||
const uniqueNumber = Math.floor(Math.random() * 900000) + 100000;
|
const formattedNumber = String(nextNumber).padStart(4, '0');
|
||||||
memberID = `MUSA-${uniqueNumber}`;
|
memberID = `MUSA-${currentYear}-${formattedNumber}`;
|
||||||
|
|
||||||
console.log(`[member-id] Attempt ${attempts}: Checking uniqueness of ${memberID}`);
|
console.log(`[member-id] Attempt ${attempts}: Checking uniqueness of ${memberID}`);
|
||||||
|
|
||||||
|
|
@ -28,7 +48,8 @@ export async function generateMemberID(): Promise<string> {
|
||||||
isUnique = !existingMember;
|
isUnique = !existingMember;
|
||||||
|
|
||||||
if (!isUnique) {
|
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<string> {
|
||||||
return memberID!;
|
return memberID!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for generateMemberID to match the import in other files
|
||||||
|
*/
|
||||||
|
export async function generateUniqueMemberId(): Promise<string> {
|
||||||
|
return generateMemberID();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a member ID already exists in the database
|
* Checks if a member ID already exists in the database
|
||||||
* @param memberID - The member ID to check
|
* @param memberID - The member ID to check
|
||||||
|
|
@ -103,8 +131,8 @@ export function isValidMemberIDFormat(memberID: string): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check format: MUSA-{6 digits}
|
// Check format: MUSA-YYYY-XXXX (year and 4 digits)
|
||||||
const memberIDRegex = /^MUSA-\d{6}$/;
|
const memberIDRegex = /^MUSA-\d{4}-\d{4}$/;
|
||||||
return memberIDRegex.test(memberID);
|
return memberIDRegex.test(memberID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue