diff --git a/app.vue b/app.vue
index 8507adc..b27fd21 100644
--- a/app.vue
+++ b/app.vue
@@ -1,7 +1,19 @@
-
-
-
+
+
+
diff --git a/components/DuesPaymentBanner.vue b/components/DuesPaymentBanner.vue
index 4c5a68e..12a5acc 100644
--- a/components/DuesPaymentBanner.vue
+++ b/components/DuesPaymentBanner.vue
@@ -44,6 +44,21 @@
+
+
+
+
+ Payment Reference:
+
+ {{ memberData?.member_id || 'Member ID pending' }}
+
+
+ mdi-information-outline
+ Please include your member ID in the wire transfer reference for identification
+
+
+
+
diff --git a/components/MonacoUSALogo.vue b/components/MonacoUSALogo.vue
new file mode 100644
index 0000000..48125c9
--- /dev/null
+++ b/components/MonacoUSALogo.vue
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
diff --git a/pages/dashboard/profile.vue b/pages/dashboard/profile.vue
new file mode 100644
index 0000000..28f3e27
--- /dev/null
+++ b/pages/dashboard/profile.vue
@@ -0,0 +1,477 @@
+
+
+
+
+
+
+
+
+
+ My Profile
+
+
+ View and manage your membership information
+
+
+
+
+
+
+
+
+
+
+ Loading your profile...
+
+
+
+
+
+
+
+
+
+
+
+
mdi-badge-account
+
+
+ {{ memberData?.member_id || 'Member ID Pending' }}
+
+
+ Your unique MonacoUSA member identifier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-account-details
+ Personal Information
+
+
+
+
+
+ Full Name
+ {{ fullName || 'Not provided' }}
+
+
+
+
+
+ Email Address
+ {{ memberData?.email || user?.email || 'Not provided' }}
+
+
+
+
+
+ Phone Number
+ {{ memberData?.phone || 'Not provided' }}
+
+
+
+
+
+ Date of Birth
+ {{ formatDate(memberData?.date_of_birth) || 'Not provided' }}
+
+
+
+
+
+ Nationality
+
+
+
+
+ Not provided
+
+
+
+
+
+
+ Address
+ {{ memberData?.address || 'Not provided' }}
+
+
+
+
+
+
+
+
+
+
+
+ mdi-card-membership
+ Membership Details
+
+
+
+
+
+ Member Since
+ {{ formatDate(memberData?.member_since) || 'Not provided' }}
+
+
+
+
+
+ Membership Status
+
+
+ {{ memberData?.membership_status || 'Pending' }}
+
+
+
+
+
+
+
+ Account Tier
+
+
+ {{ userTier?.toUpperCase() || 'USER' }}
+
+
+
+
+
+
+
+ Registration Date
+ {{ formatDate(memberData?.registration_date) || 'Not available' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-credit-card-outline
+ Dues and Payment Information
+
+
+
+
+
+
+ {{ memberData?.current_year_dues_paid === 'true' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
+
+
Current Year Dues
+
+ {{ memberData?.current_year_dues_paid === 'true' ? 'PAID' : 'UNPAID' }}
+
+
+
+
+
+
+
Last Payment Date
+
{{ formatDate(memberData?.membership_date_paid) || 'No payment recorded' }}
+
+
+
+
+
+
Payment Due Date
+
{{ formatDate(memberData?.payment_due_date) || 'Not set' }}
+
+
+
+
+
+
Days Remaining
+
+ {{ daysRemaining >= 0 ? `${daysRemaining} days` : `${Math.abs(daysRemaining)} days overdue` }}
+
+
+
+
+
+
+
+
+
+
Payment Reference for Wire Transfers
+
+
+
+
+
+ Include this member ID in your wire transfer reference for payment identification.
+
+
+
+
+
+
+
+
+
+
+ {{ snackbar.message }}
+
+
+ Close
+
+
+
+
+
+
+
+
+
diff --git a/pages/dashboard/user.vue b/pages/dashboard/user.vue
index 2a37772..6072b98 100644
--- a/pages/dashboard/user.vue
+++ b/pages/dashboard/user.vue
@@ -162,8 +162,7 @@ const { firstName, user, userTier } = useAuth();
// Navigation methods (placeholder implementations)
const navigateToProfile = () => {
- // TODO: Implement profile navigation
- console.log('Navigate to profile');
+ navigateTo('/dashboard/profile');
};
const navigateToEvents = () => {
@@ -177,8 +176,19 @@ const navigateToResources = () => {
};
const contactSupport = () => {
- // TODO: Implement support contact
- console.log('Contact support');
+ const subject = encodeURIComponent('MonacoUSA Portal Support Request');
+ const body = encodeURIComponent(`Hello,
+
+I need assistance with:
+
+[Please describe your issue]
+
+Member: ${user.value?.name || 'Not provided'}
+Email: ${user.value?.email || 'Not provided'}
+
+Thank you!`);
+
+ window.open(`mailto:support@monacousa.org?subject=${subject}&body=${body}`, '_self');
};
diff --git a/server/api/admin/migrate-member-ids.post.ts b/server/api/admin/migrate-member-ids.post.ts
new file mode 100644
index 0000000..fd9b113
--- /dev/null
+++ b/server/api/admin/migrate-member-ids.post.ts
@@ -0,0 +1,117 @@
+import { createSessionManager } from '~/server/utils/session';
+import { findMembersWithoutMemberID, generateMemberID } from '~/server/utils/member-id';
+import { updateMember, handleNocoDbError } from '~/server/utils/nocodb';
+import type { Member } from '~/utils/types';
+
+export default defineEventHandler(async (event) => {
+ console.log('[api/admin/migrate-member-ids.post] =========================');
+ console.log('[api/admin/migrate-member-ids.post] POST /api/admin/migrate-member-ids - Migrate existing member IDs');
+ console.log('[api/admin/migrate-member-ids.post] Request from:', getClientIP(event));
+
+ try {
+ // Validate session and require Admin privileges
+ const sessionManager = createSessionManager();
+ const cookieHeader = getCookie(event, 'monacousa-session') ? getHeader(event, 'cookie') : undefined;
+ const session = sessionManager.getSession(cookieHeader);
+
+ if (!session?.user) {
+ throw createError({
+ statusCode: 401,
+ statusMessage: 'Authentication required'
+ });
+ }
+
+ const userTier = session.user.tier;
+ if (userTier !== 'admin') {
+ throw createError({
+ statusCode: 403,
+ statusMessage: 'Administrator privileges required for member ID migration'
+ });
+ }
+
+ console.log('[api/admin/migrate-member-ids.post] Authorized admin user:', session.user.email);
+
+ // Find all members without member IDs
+ console.log('[api/admin/migrate-member-ids.post] Finding members without member IDs...');
+ const membersWithoutId = await findMembersWithoutMemberID();
+
+ if (membersWithoutId.length === 0) {
+ console.log('[api/admin/migrate-member-ids.post] No members found without member IDs');
+ return {
+ success: true,
+ message: 'All members already have member IDs assigned',
+ migrated: 0,
+ total: 0
+ };
+ }
+
+ console.log(`[api/admin/migrate-member-ids.post] Found ${membersWithoutId.length} members without member IDs`);
+
+ const migrationResults = [];
+ let successCount = 0;
+ let errorCount = 0;
+
+ // Migrate each member
+ for (let i = 0; i < membersWithoutId.length; i++) {
+ const member = membersWithoutId[i];
+ console.log(`[api/admin/migrate-member-ids.post] Migrating member ${i + 1}/${membersWithoutId.length}: ${member.first_name} ${member.last_name} (ID: ${member.Id})`);
+
+ try {
+ // Generate unique member ID
+ const memberID = await generateMemberID();
+ console.log(`[api/admin/migrate-member-ids.post] Generated ID ${memberID} for member ${member.Id}`);
+
+ // Update member with new member ID
+ await updateMember(member.Id, { member_id: memberID });
+
+ migrationResults.push({
+ memberId: member.Id,
+ memberName: `${member.first_name} ${member.last_name}`,
+ generatedId: memberID,
+ success: true
+ });
+
+ successCount++;
+ console.log(`[api/admin/migrate-member-ids.post] ✅ Successfully migrated member ${member.Id} with ID ${memberID}`);
+
+ // Add a small delay to avoid overwhelming the database
+ if (i < membersWithoutId.length - 1) {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+
+ } catch (error: any) {
+ console.error(`[api/admin/migrate-member-ids.post] ❌ Failed to migrate member ${member.Id}:`, error);
+
+ migrationResults.push({
+ memberId: member.Id,
+ memberName: `${member.first_name} ${member.last_name}`,
+ generatedId: null,
+ success: false,
+ error: error.message || 'Unknown error'
+ });
+
+ errorCount++;
+ }
+ }
+
+ console.log('[api/admin/migrate-member-ids.post] =========================');
+ console.log('[api/admin/migrate-member-ids.post] Migration completed');
+ console.log('[api/admin/migrate-member-ids.post] Total members processed:', membersWithoutId.length);
+ console.log('[api/admin/migrate-member-ids.post] Successful migrations:', successCount);
+ console.log('[api/admin/migrate-member-ids.post] Failed migrations:', errorCount);
+ console.log('[api/admin/migrate-member-ids.post] =========================');
+
+ return {
+ success: errorCount === 0,
+ message: `Migration completed. ${successCount} members successfully migrated, ${errorCount} errors.`,
+ migrated: successCount,
+ errors: errorCount,
+ total: membersWithoutId.length,
+ results: migrationResults
+ };
+
+ } catch (error: any) {
+ console.error('[api/admin/migrate-member-ids.post] ❌ Migration failed:', error);
+ handleNocoDbError(error, 'migrateMemberIDs', 'Member ID Migration');
+ }
+});
diff --git a/server/api/members/index.post.ts b/server/api/members/index.post.ts
index d0669a6..0b5d563 100644
--- a/server/api/members/index.post.ts
+++ b/server/api/members/index.post.ts
@@ -1,5 +1,6 @@
import { createMember, handleNocoDbError } from '~/server/utils/nocodb';
import { createSessionManager } from '~/server/utils/session';
+import { generateMemberID } from '~/server/utils/member-id';
import type { Member, MembershipStatus } from '~/utils/types';
export default defineEventHandler(async (event) => {
@@ -50,7 +51,7 @@ export default defineEventHandler(async (event) => {
}
// Sanitize and prepare data
- const memberData = sanitizeMemberData(normalizedBody);
+ const memberData = await sanitizeMemberData(normalizedBody);
console.log('[api/members.post] Sanitized data fields:', Object.keys(memberData));
// Create member in NocoDB
@@ -109,9 +110,14 @@ function validateMemberData(data: any): string[] {
return errors;
}
-function sanitizeMemberData(data: any): Partial
{
+async function sanitizeMemberData(data: any): Promise> {
const sanitized: any = {};
+ // Generate unique member ID
+ console.log('[api/members.post] Generating member ID for new member...');
+ sanitized.member_id = await generateMemberID();
+ console.log('[api/members.post] Generated member ID:', sanitized.member_id);
+
// Required fields
sanitized.first_name = data.first_name.trim();
sanitized.last_name = data.last_name.trim();
diff --git a/server/utils/member-id.ts b/server/utils/member-id.ts
new file mode 100644
index 0000000..5615464
--- /dev/null
+++ b/server/utils/member-id.ts
@@ -0,0 +1,123 @@
+import { getMembers, updateMember } from './nocodb';
+import type { Member } from '~/utils/types';
+
+/**
+ * Generates a unique member ID in the format MUSA-{unique 6-digit 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...');
+
+ let memberID: string;
+ let isUnique = false;
+ let attempts = 0;
+ const maxAttempts = 100; // Prevent infinite loops
+
+ while (!isUnique && attempts < maxAttempts) {
+ attempts++;
+
+ // Generate a 6-digit number (100000 to 999999)
+ const uniqueNumber = Math.floor(Math.random() * 900000) + 100000;
+ memberID = `MUSA-${uniqueNumber}`;
+
+ console.log(`[member-id] Attempt ${attempts}: Checking uniqueness of ${memberID}`);
+
+ // Check if ID already exists in database
+ const existingMember = await checkMemberIDExists(memberID);
+ isUnique = !existingMember;
+
+ if (!isUnique) {
+ console.log(`[member-id] ID ${memberID} already exists, generating new one...`);
+ }
+ }
+
+ if (attempts >= maxAttempts) {
+ console.error('[member-id] Failed to generate unique member ID after maximum attempts');
+ throw new Error('Failed to generate unique member ID after maximum attempts');
+ }
+
+ console.log(`[member-id] ✅ Generated unique member ID: ${memberID!} (attempts: ${attempts})`);
+ return memberID!;
+}
+
+/**
+ * Checks if a member ID already exists in the database
+ * @param memberID - The member ID to check
+ * @returns Promise - True if the member ID exists, false otherwise
+ */
+export async function checkMemberIDExists(memberID: string): Promise {
+ try {
+ console.log(`[member-id] Checking if member ID exists: ${memberID}`);
+
+ // Get all members and check for duplicate member_id
+ const members = await getMembers();
+ const memberList = Array.isArray(members) ? members : members?.list || [];
+
+ const existingMember = memberList.find((member: Member) => member.member_id === memberID);
+ const exists = !!existingMember;
+
+ console.log(`[member-id] Member ID ${memberID} exists: ${exists}`);
+ return exists;
+
+ } catch (error: any) {
+ console.error('[member-id] Error checking member ID existence:', error);
+ // In case of error, assume it doesn't exist to allow generation to continue
+ // The actual creation will fail if there's a real database issue
+ return false;
+ }
+}
+
+/**
+ * Finds all members without a member_id field
+ * Used for migration purposes
+ * @returns Promise - Array of members without member IDs
+ */
+export async function findMembersWithoutMemberID(): Promise {
+ try {
+ console.log('[member-id] Finding members without member IDs for migration...');
+
+ const members = await getMembers();
+ const memberList = Array.isArray(members) ? members : members?.list || [];
+
+ const membersWithoutId = memberList.filter((member: Member) =>
+ !member.member_id || member.member_id.trim() === ''
+ );
+
+ console.log(`[member-id] Found ${membersWithoutId.length} members without member IDs`);
+ return membersWithoutId;
+
+ } catch (error: any) {
+ console.error('[member-id] Error finding members without member IDs:', error);
+ throw error;
+ }
+}
+
+/**
+ * Validates a member ID format
+ * @param memberID - The member ID to validate
+ * @returns boolean - True if valid format, false otherwise
+ */
+export function isValidMemberIDFormat(memberID: string): boolean {
+ if (!memberID || typeof memberID !== 'string') {
+ return false;
+ }
+
+ // Check format: MUSA-{6 digits}
+ const memberIDRegex = /^MUSA-\d{6}$/;
+ return memberIDRegex.test(memberID);
+}
+
+/**
+ * Extracts the numeric part from a member ID
+ * @param memberID - The member ID (e.g., "MUSA-123456")
+ * @returns number - The numeric part or null if invalid
+ */
+export function extractMemberIDNumber(memberID: string): number | null {
+ if (!isValidMemberIDFormat(memberID)) {
+ return null;
+ }
+
+ const numericPart = memberID.replace('MUSA-', '');
+ return parseInt(numericPart, 10);
+}
diff --git a/utils/types.ts b/utils/types.ts
index 9c3bb3b..e2b6bb8 100644
--- a/utils/types.ts
+++ b/utils/types.ts
@@ -118,6 +118,7 @@ export enum MembershipStatus {
export interface Member {
Id: string;
+ member_id?: string; // MUSA-{unique number} - Member identification number
first_name: string;
last_name: string;
email: string;