Fix critical member management issues: dues tracking, member IDs, and profile display
All checks were successful
Build And Push Image / docker (push) Successful in 2m20s

- 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:
2025-09-04 18:43:57 +02:00
parent 67bb9e32ac
commit d34d16fda1
8 changed files with 326 additions and 24 deletions

View File

@@ -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<DuesStatus> {
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) {

View File

@@ -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<string> - The unique member ID
*/
export async function generateMemberID(): Promise<string> {
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<string> {
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<string> {
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
* @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);
}