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 {