Implement dues reminder system with monthly payment cycle
Build And Push Image / docker (push) Failing after 1m10s
Details
Build And Push Image / docker (push) Failing after 1m10s
Details
- Add API endpoint and email templates for dues reminders - Change due date calculation from yearly to monthly billing - Add visual status indicators for overdue and due-soon members - Enhance member cards with status stripes and styling
This commit is contained in:
parent
7784fab23f
commit
888059a612
|
|
@ -254,10 +254,10 @@ watch(duesPaid, (newValue) => {
|
|||
} else {
|
||||
form.value['Membership Date Paid'] = '';
|
||||
if (!form.value['Payment Due Date']) {
|
||||
// Set due date to one year from member since date or today
|
||||
// Set due date to one month from member since date or today
|
||||
const memberSince = form.value['Member Since'] || new Date().toISOString().split('T')[0];
|
||||
const dueDate = new Date(memberSince);
|
||||
dueDate.setFullYear(dueDate.getFullYear() + 1);
|
||||
dueDate.setMonth(dueDate.getMonth() + 1);
|
||||
form.value['Payment Due Date'] = dueDate.toISOString().split('T')[0];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -415,10 +415,10 @@ watch(duesPaid, (newValue) => {
|
|||
} else {
|
||||
form.value.membership_date_paid = '';
|
||||
if (!form.value.payment_due_date) {
|
||||
// Set due date to one year from member since date or today
|
||||
// Set due date to one month from member since date or today
|
||||
const memberSince = form.value.member_since || new Date().toISOString().split('T')[0];
|
||||
const dueDate = new Date(memberSince);
|
||||
dueDate.setFullYear(dueDate.getFullYear() + 1);
|
||||
dueDate.setMonth(dueDate.getMonth() + 1);
|
||||
form.value.payment_due_date = dueDate.toISOString().split('T')[0];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,23 @@
|
|||
<template>
|
||||
<v-card
|
||||
class="member-card"
|
||||
:class="{ 'member-card--inactive': !isActive }"
|
||||
:class="{
|
||||
'member-card--inactive': !isActive,
|
||||
'member-card--overdue': isOverdue,
|
||||
'member-card--due-soon': isDuesComingDue
|
||||
}"
|
||||
elevation="2"
|
||||
@click="$emit('view', member)"
|
||||
>
|
||||
<!-- Status Stripe -->
|
||||
<div
|
||||
v-if="isOverdue || isDuesComingDue"
|
||||
class="status-stripe"
|
||||
:class="{
|
||||
'status-stripe--overdue': isOverdue,
|
||||
'status-stripe--due-soon': isDuesComingDue
|
||||
}"
|
||||
/>
|
||||
<!-- Member Status Badge -->
|
||||
<div class="member-status-badge">
|
||||
<v-chip
|
||||
|
|
@ -547,4 +560,33 @@ const formatDate = (dateString: string): string => {
|
|||
.text-error {
|
||||
color: rgb(var(--v-theme-error)) !important;
|
||||
}
|
||||
|
||||
/* Status Stripe Styles */
|
||||
.status-stripe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
border-radius: 12px 0 0 12px;
|
||||
}
|
||||
|
||||
.status-stripe--overdue {
|
||||
background: linear-gradient(180deg, #f44336 0%, #d32f2f 100%);
|
||||
box-shadow: 2px 0 8px rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
.status-stripe--due-soon {
|
||||
background: linear-gradient(180deg, #ff9800 0%, #f57c00 100%);
|
||||
box-shadow: 2px 0 8px rgba(255, 152, 0, 0.3);
|
||||
}
|
||||
|
||||
.member-card--overdue {
|
||||
border-left: 4px solid #f44336;
|
||||
}
|
||||
|
||||
.member-card--due-soon {
|
||||
border-left: 4px solid #ff9800;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
import { sendEmail } from '~/server/utils/email';
|
||||
import { createNocoDBClient } from '~/server/utils/nocodb';
|
||||
import { calculateDaysUntilDue, calculateOverdueDays, getNextDuesDate, isPaymentOverOneYear, isDuesActuallyCurrent } from '~/utils/dues-calculations';
|
||||
import { getRegistrationConfig } from '~/server/utils/admin-config';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const memberId = getRouterParam(event, 'id');
|
||||
const { reminderType } = await readBody(event);
|
||||
|
||||
if (!memberId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Member ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
if (!['due-soon', 'overdue'].includes(reminderType)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid reminder type. Must be "due-soon" or "overdue"'
|
||||
});
|
||||
}
|
||||
|
||||
// Get member data
|
||||
const nocodb = createNocoDBClient();
|
||||
const member = await nocodb.findOne('nc_members', memberId);
|
||||
|
||||
if (!member) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Member not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (!member.email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Member does not have an email address'
|
||||
});
|
||||
}
|
||||
|
||||
// Get current admin configuration for payment details
|
||||
const registrationConfig = getRegistrationConfig();
|
||||
|
||||
// Calculate dues status
|
||||
const memberName = member.FullName || `${member.first_name} ${member.last_name}`.trim() || 'Member';
|
||||
const nextDueDate = getNextDuesDate(member);
|
||||
const membershipFee = `€${registrationConfig.membershipFee}`;
|
||||
const paymentIban = registrationConfig.iban;
|
||||
const accountHolder = registrationConfig.accountHolder;
|
||||
const portalUrl = `${process.env.NUXT_PUBLIC_DOMAIN || 'https://monacousa.org'}/dashboard`;
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
// Format dates
|
||||
const formatDate = (dateString: string | Date | null | undefined): string => {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch {
|
||||
return String(dateString);
|
||||
}
|
||||
};
|
||||
|
||||
let emailData;
|
||||
let subject;
|
||||
let template;
|
||||
|
||||
if (reminderType === 'due-soon') {
|
||||
// Check if this member actually has dues coming due
|
||||
const daysUntilDue = calculateDaysUntilDue(member);
|
||||
|
||||
if (!daysUntilDue || daysUntilDue.daysUntilDue <= 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Member does not have dues coming due soon'
|
||||
});
|
||||
}
|
||||
|
||||
subject = `🏎️ MonacoUSA - Annual Dues Due in ${daysUntilDue.daysUntilDue} Days`;
|
||||
template = 'dues-due-soon';
|
||||
|
||||
emailData = {
|
||||
memberName,
|
||||
memberId: member.member_id || member.Id,
|
||||
memberEmail: member.email,
|
||||
memberSince: formatDate(member.member_since),
|
||||
amount: membershipFee,
|
||||
dueDate: formatDate(daysUntilDue.nextDueDate),
|
||||
daysUntilDue: daysUntilDue.daysUntilDue,
|
||||
paymentIban,
|
||||
accountHolder,
|
||||
portalUrl,
|
||||
currentYear
|
||||
};
|
||||
|
||||
} else if (reminderType === 'overdue') {
|
||||
// Check if this member is actually overdue
|
||||
const isDuesCurrent = isDuesActuallyCurrent(member);
|
||||
|
||||
if (isDuesCurrent) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Member does not have overdue dues'
|
||||
});
|
||||
}
|
||||
|
||||
const overdueDays = calculateOverdueDays(member);
|
||||
const originalDueDate = member.payment_due_date || nextDueDate;
|
||||
|
||||
subject = `🚨 MonacoUSA - URGENT: Overdue Dues Notice (${overdueDays} days overdue)`;
|
||||
template = 'dues-overdue';
|
||||
|
||||
emailData = {
|
||||
memberName,
|
||||
memberId: member.member_id || member.Id,
|
||||
memberEmail: member.email,
|
||||
memberSince: formatDate(member.member_since),
|
||||
amount: membershipFee,
|
||||
originalDueDate: formatDate(originalDueDate),
|
||||
daysOverdue: overdueDays,
|
||||
paymentIban,
|
||||
accountHolder,
|
||||
portalUrl,
|
||||
currentYear
|
||||
};
|
||||
}
|
||||
|
||||
// Send the email
|
||||
console.log(`[Dues Reminder] Sending ${reminderType} reminder to ${member.email}`, {
|
||||
memberId,
|
||||
memberName,
|
||||
reminderType,
|
||||
emailData
|
||||
});
|
||||
|
||||
await sendEmail({
|
||||
to: member.email,
|
||||
subject,
|
||||
template,
|
||||
data: emailData
|
||||
});
|
||||
|
||||
// Log the reminder sent (could store in database for tracking)
|
||||
console.log(`[Dues Reminder] Successfully sent ${reminderType} reminder to ${memberName} (${member.email})`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `${reminderType === 'due-soon' ? 'Due soon' : 'Overdue'} reminder sent successfully`,
|
||||
data: {
|
||||
memberId,
|
||||
memberName,
|
||||
memberEmail: member.email,
|
||||
reminderType,
|
||||
sentAt: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[Dues Reminder] Error sending reminder:', error);
|
||||
|
||||
// Handle specific error cases
|
||||
if (error.statusCode) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message || 'Failed to send dues reminder'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dues Payment Reminder - MonacoUSA</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #a31515 0%, #d32f2f 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
.content {
|
||||
background: #ffffff;
|
||||
padding: 30px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
.logo {
|
||||
max-width: 200px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.alert-warning {
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
color: #856404;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.member-info {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.amount-due {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #a31515;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #a31515 0%, #d32f2f 100%);
|
||||
color: white;
|
||||
padding: 12px 30px;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
margin: 10px;
|
||||
}
|
||||
.btn:hover {
|
||||
background: linear-gradient(135deg, #8a1212 0%, #b71c1c 100%);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🏎️ MonacoUSA</h1>
|
||||
<h2>Annual Dues Payment Reminder</h2>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="alert-warning">
|
||||
<strong>⏰ Payment Due Soon!</strong> Your annual membership dues will be due in {{daysUntilDue}} days.
|
||||
</div>
|
||||
|
||||
<p>Dear {{memberName}},</p>
|
||||
|
||||
<p>This is a friendly reminder that your annual MonacoUSA membership dues will be due soon.</p>
|
||||
|
||||
<div class="member-info">
|
||||
<h3>Member Information</h3>
|
||||
<p><strong>Name:</strong> {{memberName}}</p>
|
||||
<p><strong>Member ID:</strong> {{memberId}}</p>
|
||||
<p><strong>Email:</strong> {{memberEmail}}</p>
|
||||
<p><strong>Member Since:</strong> {{memberSince}}</p>
|
||||
</div>
|
||||
|
||||
<div class="amount-due">
|
||||
Annual Dues: {{amount}}
|
||||
<br>
|
||||
<small style="font-size: 16px; color: #666;">Due Date: {{dueDate}}</small>
|
||||
</div>
|
||||
|
||||
<p>To ensure uninterrupted access to MonacoUSA benefits and avoid any service disruption, please submit your payment before the due date.</p>
|
||||
|
||||
<div class="member-info">
|
||||
<h3>Payment Information</h3>
|
||||
<p>Membership fee is {{amount}}/yr per person, paid using the IBAN below:</p>
|
||||
<p><strong>IBAN Euro:</strong> {{paymentIban}}</p>
|
||||
<p><strong>Account Holder:</strong> {{accountHolder}}</p>
|
||||
<p><strong>Payment Reference:</strong> {{memberId}} - Annual Dues</p>
|
||||
</div>
|
||||
|
||||
<p><strong>Important:</strong> Please include your member ID ({{memberId}}) in the payment reference to ensure proper credit to your account.</p>
|
||||
|
||||
<p>If you have already made your payment, please disregard this reminder. If you have any questions about your membership or payment, please contact us:</p>
|
||||
|
||||
<p><strong>Contact Information:</strong></p>
|
||||
<p>📧 Email: info@monacousa.org</p>
|
||||
|
||||
<p>Thank you for your continued membership with MonacoUSA!</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The MonacoUSA Team</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>This is an automated message from MonacoUSA. Please do not reply to this email.</p>
|
||||
<p>© {{currentYear}} MonacoUSA. All rights reserved.</p>
|
||||
<p>If you no longer wish to receive these reminders, please contact us at info@monacousa.org</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Overdue Dues Notice - MonacoUSA</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #d32f2f 0%, #f44336 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
.content {
|
||||
background: #ffffff;
|
||||
padding: 30px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
.logo {
|
||||
max-width: 200px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.alert-danger {
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.member-info {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.amount-due {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #d32f2f;
|
||||
text-align: center;
|
||||
background: #ffebee;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
border: 2px solid #f5c6cb;
|
||||
}
|
||||
.urgency-notice {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
color: #856404;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #d32f2f 0%, #f44336 100%);
|
||||
color: white;
|
||||
padding: 15px 35px;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
margin: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.btn:hover {
|
||||
background: linear-gradient(135deg, #b71c1c 0%, #d32f2f 100%);
|
||||
}
|
||||
.consequences {
|
||||
background: #ffebee;
|
||||
border-left: 4px solid #f44336;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🏎️ MonacoUSA</h1>
|
||||
<h2>⚠️ Overdue Dues Notice</h2>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="alert-danger">
|
||||
<strong>🚨 URGENT: Payment Overdue!</strong> Your annual membership dues are now {{daysOverdue}} days overdue.
|
||||
</div>
|
||||
|
||||
<p>Dear {{memberName}},</p>
|
||||
|
||||
<p>This is an urgent notice that your annual MonacoUSA membership dues are now overdue and require immediate payment to avoid service interruption.</p>
|
||||
|
||||
<div class="member-info">
|
||||
<h3>Member Information</h3>
|
||||
<p><strong>Name:</strong> {{memberName}}</p>
|
||||
<p><strong>Member ID:</strong> {{memberId}}</p>
|
||||
<p><strong>Email:</strong> {{memberEmail}}</p>
|
||||
<p><strong>Member Since:</strong> {{memberSince}}</p>
|
||||
</div>
|
||||
|
||||
<div class="amount-due">
|
||||
Overdue Amount: {{amount}}
|
||||
<br>
|
||||
<small style="font-size: 16px; color: #666;">Original Due Date: {{originalDueDate}}</small>
|
||||
<br>
|
||||
<small style="font-size: 14px; color: #d32f2f;">{{daysOverdue}} days overdue</small>
|
||||
</div>
|
||||
|
||||
<div class="member-info">
|
||||
<h3>Payment Information</h3>
|
||||
<p>Membership fee is {{amount}}/yr per person, paid using the IBAN below:</p>
|
||||
<p><strong>IBAN Euro:</strong> {{paymentIban}}</p>
|
||||
<p><strong>Account Holder:</strong> {{accountHolder}}</p>
|
||||
<p><strong>Payment Reference:</strong> {{memberId}} - Annual Dues</p>
|
||||
</div>
|
||||
|
||||
<p><strong>Important:</strong> Please include your member ID ({{memberId}}) in the payment reference to ensure proper credit to your account.</p>
|
||||
|
||||
<div class="consequences">
|
||||
<p><strong>Important:</strong> Failure of payment will lead to a revoking of MonacoUSA Member Privileges.</p>
|
||||
</div>
|
||||
|
||||
<p>If you have already submitted payment, please forward your payment confirmation to info@monacousa.org to update your account status.</p>
|
||||
|
||||
<p><strong>Questions or Concerns?</strong> Contact us:</p>
|
||||
<p>📧 Email: info@monacousa.org</p>
|
||||
|
||||
<p>We value your membership and hope to resolve this matter promptly.</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The MonacoUSA Membership Team</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>This is an automated notice from MonacoUSA. Please do not reply to this email.</p>
|
||||
<p>© {{currentYear}} MonacoUSA. All rights reserved.</p>
|
||||
<p>Please submit your payment to avoid membership suspension.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -3,7 +3,7 @@ import type { Member, DuesCalculationUtils } from '~/utils/types';
|
|||
// Constants for date calculations
|
||||
export const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
|
||||
export const DAYS_IN_YEAR = 365;
|
||||
export const UPCOMING_THRESHOLD_DAYS = 30;
|
||||
export const UPCOMING_THRESHOLD_DAYS = 30; // Trigger reminders 1 month before yearly due date
|
||||
|
||||
/**
|
||||
* Calculate days between two dates
|
||||
|
|
|
|||
Loading…
Reference in New Issue