diff --git a/components/MemberCard.vue b/components/MemberCard.vue index 408c38c..1b19dd6 100644 --- a/components/MemberCard.vue +++ b/components/MemberCard.vue @@ -261,27 +261,81 @@ const statusColor = computed(() => { } }); +// Helper to check if dues are actually current (paid within last 12 months) +const isDuesActuallyCurrent = computed(() => { + if (props.member.current_year_dues_paid !== 'true') return false; + + if (!props.member.membership_date_paid) { + // If marked as paid but no payment date, consider it invalid/overdue + return false; + } + + const paymentDate = new Date(props.member.membership_date_paid); + const oneYearAgo = new Date(); + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); + + return paymentDate > oneYearAgo; +}); + +// Helper to check if member is in grace period (new members get 1 month) +const isInGracePeriod = computed(() => { + // For existing members, check if they have member_since and it's within 1 month + if (props.member.member_since) { + const memberSince = new Date(props.member.member_since); + const oneMonthLater = new Date(memberSince); + oneMonthLater.setMonth(oneMonthLater.getMonth() + 1); + return new Date() < oneMonthLater && props.member.current_year_dues_paid !== 'true'; + } + + // If no member_since but has payment_due_date in the future, assume in grace period + if (props.member.payment_due_date) { + const dueDate = new Date(props.member.payment_due_date); + return new Date() < dueDate && props.member.current_year_dues_paid !== 'true'; + } + + return false; +}); + const duesColor = computed(() => { - return props.member.current_year_dues_paid === 'true' ? 'success' : 'error'; + if (isDuesActuallyCurrent.value) return 'success'; + if (isInGracePeriod.value) return 'warning'; + return 'error'; }); const duesVariant = computed(() => { - return props.member.current_year_dues_paid === 'true' ? 'tonal' : 'flat'; + if (isDuesActuallyCurrent.value) return 'tonal'; + if (isInGracePeriod.value) return 'tonal'; + return 'flat'; }); const duesIcon = computed(() => { - return props.member.current_year_dues_paid === 'true' ? 'mdi-check-circle' : 'mdi-alert-circle'; + if (isDuesActuallyCurrent.value) return 'mdi-check-circle'; + if (isInGracePeriod.value) return 'mdi-clock-alert'; + return 'mdi-alert-circle'; }); const duesText = computed(() => { - return props.member.current_year_dues_paid === 'true' ? 'Dues Paid' : 'Dues Outstanding'; + if (isDuesActuallyCurrent.value) return 'Dues Paid'; + if (isInGracePeriod.value) return 'Grace Period'; + return 'Dues Outstanding'; }); const isOverdue = computed(() => { - if (!props.member.payment_due_date) return false; - const dueDate = new Date(props.member.payment_due_date); - const today = new Date(); - return dueDate < today && props.member.current_year_dues_paid !== 'true'; + // If dues are current, not overdue + if (isDuesActuallyCurrent.value) return false; + + // If in grace period, not yet overdue + if (isInGracePeriod.value) return false; + + // Check if payment_due_date has passed + if (props.member.payment_due_date) { + const dueDate = new Date(props.member.payment_due_date); + const today = new Date(); + return dueDate < today; + } + + // If no due date but not paid and not in grace period, consider overdue + return props.member.current_year_dues_paid !== 'true'; }); // Calculate next dues date (1 year from when they last paid) diff --git a/pages/dashboard/member-list.vue b/pages/dashboard/member-list.vue index 73b112a..48d6b0f 100644 --- a/pages/dashboard/member-list.vue +++ b/pages/dashboard/member-list.vue @@ -13,16 +13,6 @@ - - diff --git a/server/api/admin/assign-member-ids.post.ts b/server/api/admin/assign-member-ids.post.ts new file mode 100644 index 0000000..d76fac9 --- /dev/null +++ b/server/api/admin/assign-member-ids.post.ts @@ -0,0 +1,101 @@ +import { getMembers, updateMember } from '~/server/utils/nocodb'; +import { findMembersWithoutMemberID, generateMemberID, extractMemberIDNumber } from '~/server/utils/member-id'; +import type { Member } from '~/utils/types'; + +export default defineEventHandler(async (event) => { + console.log('[api/admin/assign-member-ids.post] ========================='); + console.log('[api/admin/assign-member-ids.post] POST /api/admin/assign-member-ids - Assign member IDs to existing members'); + + try { + // Check if user is admin + const sessionManager = createSessionManager(); + const cookieHeader = getHeader(event, 'cookie'); + const session = sessionManager.getSession(cookieHeader); + + if (!session || session.user.tier !== 'admin') { + throw createError({ + statusCode: 403, + statusMessage: 'Admin access required' + }); + } + + // Get all members and find those without member IDs + const allMembersResult = await getMembers(); + const allMembers = allMembersResult.list || []; + console.log(`[api/admin/assign-member-ids.post] Found ${allMembers.length} total members`); + + // Find members without member_id + const membersWithoutIds = await findMembersWithoutMemberID(); + console.log(`[api/admin/assign-member-ids.post] Found ${membersWithoutIds.length} members without IDs`); + + if (membersWithoutIds.length === 0) { + return { + success: true, + message: 'All members already have member IDs assigned', + data: { + totalMembers: allMembers.length, + membersUpdated: 0 + } + }; + } + + // Get the highest existing member ID number to continue sequence + const existingMemberIds = allMembers + .filter((member: Member) => member.member_id) + .map((member: Member) => extractMemberIDNumber(member.member_id!)) + .filter(num => num !== null) as number[]; + + // Start from next available number, or 1 if no existing IDs + let nextIdNumber = existingMemberIds.length > 0 ? Math.max(...existingMemberIds) + 1 : 1; + console.log(`[api/admin/assign-member-ids.post] Starting with member ID number: ${nextIdNumber}`); + + // Assign IDs to members without them + const updatedMembers = []; + + for (const member of membersWithoutIds) { + // Use 4-digit padding for member IDs (MUSA-0001, MUSA-0002, etc.) + const memberId = `MUSA-${nextIdNumber.toString().padStart(4, '0')}`; + + try { + await updateMember(member.Id, { + member_id: memberId + }); + + updatedMembers.push({ + id: member.Id, + name: `${member.first_name || ''} ${member.last_name || ''}`.trim() || 'Unknown', + email: member.email || 'No email', + memberId + }); + + console.log(`[api/admin/assign-member-ids.post] Assigned ${memberId} to ${member.first_name} ${member.last_name}`); + nextIdNumber++; + + } catch (updateError: any) { + console.error(`[api/admin/assign-member-ids.post] Failed to assign ID to member ${member.Id}:`, updateError); + // Continue with next member rather than failing the entire operation + } + } + + console.log(`[api/admin/assign-member-ids.post] ✅ Successfully assigned IDs to ${updatedMembers.length} members`); + + return { + success: true, + message: `Successfully assigned member IDs to ${updatedMembers.length} members`, + data: { + totalMembers: allMembers.length, + membersUpdated: updatedMembers.length, + updatedMembers: updatedMembers.slice(0, 10), // Return first 10 for display + startingId: updatedMembers.length > 0 ? `MUSA-${(nextIdNumber - updatedMembers.length).toString().padStart(4, '0')}` : null, + endingId: updatedMembers.length > 0 ? updatedMembers[updatedMembers.length - 1].memberId : null + } + }; + + } catch (error: any) { + console.error('[api/admin/assign-member-ids.post] ❌ Failed to assign member IDs:', error); + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || 'Failed to assign member IDs' + }); + } +}); diff --git a/server/api/registration.post.ts b/server/api/registration.post.ts index e80b9d2..ea00540 100644 --- a/server/api/registration.post.ts +++ b/server/api/registration.post.ts @@ -94,6 +94,9 @@ export default defineEventHandler(async (event) => { // 5. Create Keycloak user with role-based registration console.log('[api/registration.post] Creating Keycloak user with role-based system...'); + const paymentDueDate = new Date(); + paymentDueDate.setMonth(paymentDueDate.getMonth() + 1); // 1 month from registration + const membershipData = { membershipStatus: 'Active', duesStatus: 'unpaid' as const, @@ -101,7 +104,7 @@ export default defineEventHandler(async (event) => { phone: body.phone, address: body.address, registrationDate: new Date().toISOString(), - paymentDueDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(), + paymentDueDate: paymentDueDate.toISOString(), membershipTier: 'user' as const }; @@ -129,7 +132,7 @@ export default defineEventHandler(async (event) => { registration_date: new Date().toISOString(), member_since: new Date().getFullYear().toString(), membership_date_paid: '', - payment_due_date: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString() // 3 months from now + payment_due_date: paymentDueDate.toISOString() // 1 month from registration }; const member = await nocodb.create('members', memberData); diff --git a/server/templates/password-reset.hbs b/server/templates/password-reset.hbs index b7157aa..c15164f 100644 --- a/server/templates/password-reset.hbs +++ b/server/templates/password-reset.hbs @@ -9,17 +9,25 @@ font-family: Arial, sans-serif; line-height: 1.6; color: #333; - max-width: 600px; - margin: 0 auto; + margin: 0; padding: 20px; - background-color: #f4f4f4; + background-image: url('{{baseUrl}}/monaco_high_res.jpg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; + min-height: 100vh; } .email-container { - background: white; - padding: 30px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + padding: 40px; border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + box-shadow: 0 8px 25px rgba(163, 21, 21, 0.15); + max-width: 600px; + margin: 20px auto; + border: 1px solid rgba(163, 21, 21, 0.1); } .header { diff --git a/server/templates/test.hbs b/server/templates/test.hbs index 2739d98..97278bc 100644 --- a/server/templates/test.hbs +++ b/server/templates/test.hbs @@ -9,17 +9,25 @@ font-family: Arial, sans-serif; line-height: 1.6; color: #333; - max-width: 600px; - margin: 0 auto; + margin: 0; padding: 20px; - background-color: #f4f4f4; + background-image: url('{{baseUrl}}/monaco_high_res.jpg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; + min-height: 100vh; } .email-container { - background: white; - padding: 30px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + padding: 40px; border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + box-shadow: 0 8px 25px rgba(163, 21, 21, 0.15); + max-width: 600px; + margin: 20px auto; + border: 1px solid rgba(163, 21, 21, 0.1); } .header { diff --git a/server/templates/verification.hbs b/server/templates/verification.hbs index bedaebb..7666ec9 100644 --- a/server/templates/verification.hbs +++ b/server/templates/verification.hbs @@ -9,17 +9,25 @@ font-family: Arial, sans-serif; line-height: 1.6; color: #333; - max-width: 600px; - margin: 0 auto; + margin: 0; padding: 20px; - background-color: #f4f4f4; + background-image: url('{{baseUrl}}/monaco_high_res.jpg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; + min-height: 100vh; } .email-container { - background: white; - padding: 30px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + padding: 40px; border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + box-shadow: 0 8px 25px rgba(163, 21, 21, 0.15); + max-width: 600px; + margin: 20px auto; + border: 1px solid rgba(163, 21, 21, 0.1); } .header { diff --git a/server/templates/welcome.hbs b/server/templates/welcome.hbs index 8cd26c9..92ff9c5 100644 --- a/server/templates/welcome.hbs +++ b/server/templates/welcome.hbs @@ -9,17 +9,25 @@ font-family: Arial, sans-serif; line-height: 1.6; color: #333; - max-width: 600px; - margin: 0 auto; + margin: 0; padding: 20px; - background-color: #f4f4f4; + background-image: url('{{baseUrl}}/monaco_high_res.jpg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; + min-height: 100vh; } .email-container { - background: white; - padding: 30px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + padding: 40px; border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + box-shadow: 0 8px 25px rgba(163, 21, 21, 0.15); + max-width: 600px; + margin: 20px auto; + border: 1px solid rgba(163, 21, 21, 0.1); } .header {