From ecae3795eeb4704c7eb4a85fbcf66edf2acd27df Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 10 Aug 2025 16:49:23 +0200 Subject: [PATCH] Add global branding and implement member ID system - Add MonacoUSA logo component with global header placement - Implement member ID generation and migration system - Create profile page and improve dashboard navigation - Add member ID as payment reference in dues banner - Enable support contact functionality with pre-filled email --- app.vue | 75 ++- components/DuesPaymentBanner.vue | 15 + components/MonacoUSALogo.vue | 159 +++++++ pages/dashboard/profile.vue | 477 ++++++++++++++++++++ pages/dashboard/user.vue | 18 +- server/api/admin/migrate-member-ids.post.ts | 117 +++++ server/api/members/index.post.ts | 10 +- server/utils/member-id.ts | 123 +++++ utils/types.ts | 1 + 9 files changed, 986 insertions(+), 9 deletions(-) create mode 100644 components/MonacoUSALogo.vue create mode 100644 pages/dashboard/profile.vue create mode 100644 server/api/admin/migrate-member-ids.post.ts create mode 100644 server/utils/member-id.ts 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 @@ + + + + + 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;