898 lines
No EOL
38 KiB
TypeScript
898 lines
No EOL
38 KiB
TypeScript
import type { APIRoute } from 'astro';
|
|
import { initializeEmailServices, authenticatePocketBase, getStatusColor, getStatusText, getNextStepsText } from '../../../scripts/email/EmailHelpers';
|
|
|
|
export const POST: APIRoute = async ({ request }) => {
|
|
try {
|
|
console.log('📨 Reimbursement email API called');
|
|
|
|
const {
|
|
type,
|
|
reimbursementId,
|
|
previousStatus,
|
|
newStatus,
|
|
changedByUserId,
|
|
comment,
|
|
commentByUserId,
|
|
isPrivate,
|
|
additionalContext,
|
|
authData
|
|
} = await request.json();
|
|
|
|
console.log('📋 Request data:', {
|
|
type,
|
|
reimbursementId,
|
|
hasAuthData: !!authData,
|
|
authDataHasToken: !!(authData?.token),
|
|
authDataHasModel: !!(authData?.model),
|
|
commentLength: comment?.length || 0,
|
|
commentByUserId,
|
|
isPrivate
|
|
});
|
|
|
|
if (!type || !reimbursementId) {
|
|
console.error('❌ Missing required parameters');
|
|
return new Response(
|
|
JSON.stringify({ error: 'Missing required parameters: type and reimbursementId' }),
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
// Initialize services
|
|
const { pb, resend, fromEmail, replyToEmail } = await initializeEmailServices();
|
|
|
|
// Authenticate with PocketBase if auth data is provided (skip for test emails)
|
|
if (type !== 'test') {
|
|
authenticatePocketBase(pb, authData);
|
|
}
|
|
|
|
let success = false;
|
|
|
|
console.log(`🎯 Processing reimbursement email type: ${type}`);
|
|
|
|
switch (type) {
|
|
case 'status_change':
|
|
if (!newStatus) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'Missing newStatus for status_change notification' }),
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
success = await sendStatusChangeEmail(pb, resend, fromEmail, replyToEmail, {
|
|
reimbursementId,
|
|
newStatus,
|
|
previousStatus,
|
|
changedByUserId,
|
|
additionalContext
|
|
});
|
|
break;
|
|
|
|
case 'comment':
|
|
if (!comment || !commentByUserId) {
|
|
console.error('❌ Missing comment or commentByUserId for comment notification');
|
|
return new Response(
|
|
JSON.stringify({ error: 'Missing comment or commentByUserId for comment notification' }),
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
success = await sendCommentEmail(pb, resend, fromEmail, replyToEmail, {
|
|
reimbursementId,
|
|
comment,
|
|
commentByUserId,
|
|
isPrivate: isPrivate || false
|
|
});
|
|
break;
|
|
|
|
case 'submission':
|
|
success = await sendSubmissionEmail(pb, resend, fromEmail, replyToEmail, {
|
|
reimbursementId
|
|
});
|
|
break;
|
|
|
|
case 'test':
|
|
const { email } = additionalContext || {};
|
|
if (!email) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'Missing email for test notification' }),
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
success = await sendTestEmail(resend, fromEmail, replyToEmail, email);
|
|
break;
|
|
|
|
default:
|
|
console.error('❌ Unknown reimbursement notification type:', type);
|
|
return new Response(
|
|
JSON.stringify({ error: `Unknown reimbursement notification type: ${type}` }),
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
console.log(`📊 Reimbursement email operation result: ${success ? 'SUCCESS' : 'FAILED'}`);
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
success,
|
|
message: success ? 'Reimbursement email notification sent successfully' : 'Failed to send reimbursement email notification'
|
|
}),
|
|
{
|
|
status: success ? 200 : 500,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}
|
|
);
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error in reimbursement email notification API:', error);
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Internal server error',
|
|
details: error instanceof Error ? error.message : 'Unknown error'
|
|
}),
|
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
};
|
|
|
|
// Helper functions for reimbursement email types
|
|
async function sendStatusChangeEmail(pb: any, resend: any, fromEmail: string, replyToEmail: string, data: any): Promise<boolean> {
|
|
try {
|
|
console.log('📧 Starting reimbursement status change email process...');
|
|
console.log('Environment check:', {
|
|
hasResendKey: !!import.meta.env.RESEND_API_KEY,
|
|
fromEmail,
|
|
replyToEmail,
|
|
pocketbaseUrl: import.meta.env.POCKETBASE_URL
|
|
});
|
|
|
|
// Check if this is a test scenario
|
|
const isTestData = data.reimbursementId?.includes('test') || data.reimbursementId === 'test-id';
|
|
|
|
let reimbursement, user;
|
|
|
|
if (isTestData) {
|
|
console.log('🧪 Using test data for demonstration');
|
|
// Use mock data for testing
|
|
reimbursement = {
|
|
id: data.reimbursementId,
|
|
title: 'Test Reimbursement Request',
|
|
total_amount: 125.50,
|
|
date_of_purchase: new Date().toISOString(),
|
|
department: 'general',
|
|
payment_method: 'Personal Card',
|
|
status: data.previousStatus || 'submitted',
|
|
submitted_by: 'test-user-id',
|
|
audit_notes: ''
|
|
};
|
|
|
|
user = {
|
|
id: 'test-user-id',
|
|
name: 'Test User',
|
|
email: data.additionalContext?.testEmail || 'test@example.com'
|
|
};
|
|
|
|
console.log('✅ Test data prepared:', {
|
|
reimbursementTitle: reimbursement.title,
|
|
userEmail: user.email
|
|
});
|
|
} else {
|
|
// Get real reimbursement details
|
|
console.log('🔍 Fetching reimbursement details for:', data.reimbursementId);
|
|
reimbursement = await pb.collection('reimbursement').getOne(data.reimbursementId);
|
|
console.log('✅ Reimbursement fetched:', { id: reimbursement.id, title: reimbursement.title });
|
|
|
|
// Get submitter user details
|
|
console.log('👤 Fetching user details for:', reimbursement.submitted_by);
|
|
user = await pb.collection('users').getOne(reimbursement.submitted_by);
|
|
if (!user || !user.email) {
|
|
console.error('❌ User not found or no email:', reimbursement.submitted_by);
|
|
return false;
|
|
}
|
|
console.log('✅ User fetched:', { id: user.id, name: user.name, email: user.email });
|
|
}
|
|
|
|
// Get changed by user name if provided
|
|
let changedByName = 'System';
|
|
if (data.changedByUserId) {
|
|
try {
|
|
const changedByUser = await pb.collection('users').getOne(data.changedByUserId);
|
|
changedByName = changedByUser?.name || 'Unknown User';
|
|
console.log('👤 Changed by user:', changedByName);
|
|
} catch (error) {
|
|
console.warn('⚠️ Could not get changed by user name:', error);
|
|
}
|
|
}
|
|
|
|
const subject = `Reimbursement Status Updated: ${reimbursement.title}`;
|
|
const statusColor = getStatusColor(data.newStatus);
|
|
const statusText = getStatusText(data.newStatus);
|
|
|
|
console.log('📝 Email details:', {
|
|
to: user.email,
|
|
subject,
|
|
status: data.newStatus
|
|
});
|
|
|
|
// Add audit note when reimbursement is declined (skip for test data)
|
|
if (data.newStatus === 'rejected' && !isTestData) {
|
|
try {
|
|
console.log('📝 Adding audit note for declined reimbursement...');
|
|
|
|
// Prepare audit note content
|
|
let auditNote = `Status changed to REJECTED by ${changedByName}`;
|
|
if (data.additionalContext?.rejectionReason) {
|
|
auditNote += `\nRejection Reason: ${data.additionalContext.rejectionReason}`;
|
|
}
|
|
auditNote += `\nDate: ${new Date().toLocaleString()}`;
|
|
|
|
// Get existing audit notes or initialize empty string
|
|
const existingNotes = reimbursement.audit_notes || '';
|
|
const updatedNotes = existingNotes
|
|
? `${existingNotes}\n\n--- DECLINE RECORD ---\n${auditNote}`
|
|
: `--- DECLINE RECORD ---\n${auditNote}`;
|
|
|
|
// Update the reimbursement record with the new audit notes
|
|
await pb.collection('reimbursement').update(data.reimbursementId, {
|
|
audit_notes: updatedNotes
|
|
});
|
|
|
|
console.log('✅ Audit note added successfully for declined reimbursement');
|
|
} catch (auditError) {
|
|
console.error('❌ Failed to add audit note for declined reimbursement:', auditError);
|
|
// Don't fail the entire email process if audit note fails
|
|
}
|
|
} else if (data.newStatus === 'rejected' && isTestData) {
|
|
console.log('🧪 Skipping audit note update for test data');
|
|
}
|
|
|
|
// Helper function to generate status progress bar HTML (email-compatible)
|
|
function generateStatusProgressBar(currentStatus: string): string {
|
|
const statusOrder = ['submitted', 'under_review', 'approved', 'in_progress', 'paid'];
|
|
const rejectedStatus = ['submitted', 'under_review', 'rejected'];
|
|
|
|
const isRejected = currentStatus === 'rejected';
|
|
const statuses = isRejected ? rejectedStatus : statusOrder;
|
|
|
|
const statusIcons: Record<string, string> = {
|
|
submitted: '→',
|
|
under_review: '•',
|
|
approved: '✓',
|
|
rejected: '✗',
|
|
in_progress: '○',
|
|
paid: '✓'
|
|
};
|
|
|
|
const statusLabels: Record<string, string> = {
|
|
submitted: 'Submitted',
|
|
under_review: 'Under Review',
|
|
approved: 'Approved',
|
|
rejected: 'Rejected',
|
|
in_progress: 'In Progress',
|
|
paid: 'Paid'
|
|
};
|
|
|
|
const currentIndex = statuses.indexOf(currentStatus);
|
|
|
|
let progressBarHtml = `
|
|
<div style="background: #f8fafc; padding: 30px 20px; border-radius: 8px; margin: 20px 0; border: 1px solid #e2e8f0;">
|
|
<h3 style="margin: 0 0 30px 0; color: #1e293b; font-size: 16px; font-weight: 600; text-align: center;">Request Progress</h3>
|
|
<table style="width: 100%; max-width: 500px; margin: 0 auto; border-collapse: collapse; position: relative;">
|
|
<tr style="position: relative;">
|
|
<td colspan="${statuses.length * 2 - 1}" style="height: 2px; background: #e2e8f0; position: absolute; top: 21px; left: 0; right: 0; z-index: 3;"></td>
|
|
</tr>
|
|
<tr style="position: relative; z-index: 1;">
|
|
`;
|
|
|
|
statuses.forEach((status, index) => {
|
|
const isActive = index <= currentIndex;
|
|
const isCurrent = status === currentStatus;
|
|
|
|
let backgroundColor, textColor, lineColor;
|
|
if (isCurrent) {
|
|
if (status === 'rejected') {
|
|
backgroundColor = '#ef4444';
|
|
textColor = 'white';
|
|
lineColor = '#ef4444';
|
|
} else if (status === 'paid') {
|
|
backgroundColor = '#10b981';
|
|
textColor = 'white';
|
|
lineColor = '#10b981';
|
|
} else if (status === 'in_progress') {
|
|
backgroundColor = '#f59e0b';
|
|
textColor = 'white';
|
|
lineColor = '#f59e0b';
|
|
} else {
|
|
backgroundColor = '#3b82f6';
|
|
textColor = 'white';
|
|
lineColor = '#3b82f6';
|
|
}
|
|
} else if (isActive) {
|
|
backgroundColor = '#e2e8f0';
|
|
textColor = '#475569';
|
|
lineColor = '#cbd5e1';
|
|
} else {
|
|
backgroundColor = '#f8fafc';
|
|
textColor = '#94a3b8';
|
|
lineColor = '#e2e8f0';
|
|
}
|
|
|
|
// Status circle
|
|
progressBarHtml += `
|
|
<td style="text-align: center; padding: 0; vertical-align: top; position: relative; width: ${100/statuses.length}%;">
|
|
<div style="position: relative; z-index: 1; padding: 5px 0;">
|
|
<div style="
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: ${backgroundColor};
|
|
color: ${textColor};
|
|
text-align: center;
|
|
line-height: 32px;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
border: 3px solid #f8fafc;
|
|
box-shadow: none;
|
|
margin: 0 auto 8px auto;
|
|
">
|
|
${statusIcons[status]}
|
|
</div>
|
|
<div style="
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: ${isCurrent ? (status === 'rejected' ? '#ef4444' : status === 'paid' ? '#10b981' : status === 'in_progress' ? '#f59e0b' : '#3b82f6') : isActive ? '#475569' : '#94a3b8'};
|
|
text-align: center;
|
|
line-height: 1.2;
|
|
white-space: nowrap;
|
|
">
|
|
${statusLabels[status]}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
`;
|
|
|
|
// Connecting line (except for the last status)
|
|
if (index < statuses.length - 1) {
|
|
const nextIsActive = (index + 1) <= currentIndex;
|
|
const connectionColor = nextIsActive ? lineColor : '#e2e8f0';
|
|
|
|
progressBarHtml += `
|
|
<td style="padding: 0; vertical-align: top; position: relative; width: 20px;">
|
|
<div style="
|
|
height: 2px;
|
|
background: ${connectionColor};
|
|
position: absolute;
|
|
top: 21px;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 3;
|
|
"></div>
|
|
</td>
|
|
`;
|
|
}
|
|
});
|
|
|
|
progressBarHtml += `
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
`;
|
|
|
|
return progressBarHtml;
|
|
}
|
|
|
|
const html = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${subject}</title>
|
|
</head>
|
|
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px; margin-bottom: 30px;">
|
|
<h1 style="color: white; margin: 0; font-size: 24px;">IEEE UCSD Reimbursement Update</h1>
|
|
</div>
|
|
|
|
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin-bottom: 25px;">
|
|
<h2 style="margin-top: 0; color: #2c3e50;">Status Update</h2>
|
|
<p>Hello ${user.name},</p>
|
|
<p>Your reimbursement request "<strong>${reimbursement.title}</strong>" has been updated.</p>
|
|
|
|
${generateStatusProgressBar(data.newStatus)}
|
|
|
|
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid ${statusColor}; margin: 20px 0;">
|
|
<div style="margin-bottom: 15px;">
|
|
<span style="font-weight: bold; color: #666;">Status:</span>
|
|
<span style="background: ${statusColor}; color: white; padding: 6px 12px; border-radius: 20px; font-size: 14px; font-weight: 500; margin-left: 10px;">${statusText}</span>
|
|
</div>
|
|
|
|
${data.previousStatus && data.previousStatus !== data.newStatus ? `
|
|
<div style="color: #666; font-size: 14px;">
|
|
Changed from: <span style="text-decoration: line-through;">${getStatusText(data.previousStatus)}</span> → <strong>${statusText}</strong>
|
|
</div>
|
|
` : ''}
|
|
|
|
${changedByName !== 'System' ? `
|
|
<div style="color: #666; font-size: 14px; margin-top: 10px;">
|
|
Updated by: ${changedByName}
|
|
</div>
|
|
` : ''}
|
|
|
|
${data.newStatus === 'rejected' && data.additionalContext?.rejectionReason ? `
|
|
<div style="background: #f8d7da; padding: 15px; border-radius: 6px; border: 1px solid #f5c6cb; margin-top: 15px;">
|
|
<div style="font-weight: bold; color: #721c24; margin-bottom: 8px;">Rejection Reason:</div>
|
|
<div style="color: #721c24; font-style: italic;">${data.additionalContext.rejectionReason}</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
<div style="margin: 25px 0;">
|
|
<h3 style="color: #2c3e50; margin-bottom: 15px;">Reimbursement Details</h3>
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; width: 30%;">Amount:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">$${reimbursement.total_amount.toFixed(2)}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Date of Purchase:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${new Date(reimbursement.date_of_purchase).toLocaleDateString()}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Department:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${reimbursement.department.charAt(0).toUpperCase() + reimbursement.department.slice(1)}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; font-weight: bold;">Payment Method:</td>
|
|
<td style="padding: 8px 0;">${reimbursement.payment_method}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 20px 0;">
|
|
<p style="margin: 0; font-size: 14px;"><strong>Next Steps:</strong></p>
|
|
<p style="margin: 5px 0 0 0; font-size: 14px;">
|
|
${getNextStepsText(data.newStatus)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="text-align: center; padding: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
|
|
<p>This is an automated notification from IEEE UCSD Reimbursement System.</p>
|
|
<p>If you have any questions, please contact us at <a href="mailto:${replyToEmail}" style="color: #667eea;">${replyToEmail}</a></p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
console.log('Attempting to send email via Resend...');
|
|
const result = await resend.emails.send({
|
|
from: fromEmail,
|
|
to: [user.email],
|
|
replyTo: replyToEmail,
|
|
subject,
|
|
html,
|
|
});
|
|
|
|
console.log('Resend response:', result);
|
|
console.log('Status change email sent successfully!');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Failed to send status change email:', error);
|
|
console.error('Error details:', {
|
|
name: error instanceof Error ? error.name : 'Unknown',
|
|
message: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function sendCommentEmail(pb: any, resend: any, fromEmail: string, replyToEmail: string, data: any): Promise<boolean> {
|
|
try {
|
|
console.log('Starting comment email process...');
|
|
console.log('Comment data received:', {
|
|
reimbursementId: data.reimbursementId,
|
|
commentByUserId: data.commentByUserId,
|
|
isPrivate: data.isPrivate,
|
|
commentLength: data.comment?.length || 0
|
|
});
|
|
|
|
// Don't send emails for private comments
|
|
if (data.isPrivate) {
|
|
console.log('Comment is private, skipping email notification');
|
|
return true;
|
|
}
|
|
|
|
// Get reimbursement details
|
|
console.log('Fetching reimbursement details for:', data.reimbursementId);
|
|
const reimbursement = await pb.collection('reimbursement').getOne(data.reimbursementId);
|
|
console.log('Reimbursement fetched:', {
|
|
id: reimbursement.id,
|
|
title: reimbursement.title,
|
|
submitted_by: reimbursement.submitted_by
|
|
});
|
|
|
|
// Get submitter user details
|
|
console.log('Fetching submitter user details for:', reimbursement.submitted_by);
|
|
const user = await pb.collection('users').getOne(reimbursement.submitted_by);
|
|
if (!user || !user.email) {
|
|
console.error('User not found or no email:', reimbursement.submitted_by);
|
|
return false;
|
|
}
|
|
console.log('Submitter user fetched:', {
|
|
id: user.id,
|
|
name: user.name,
|
|
email: user.email
|
|
});
|
|
|
|
// Get commenter user name
|
|
console.log('Fetching commenter user details for:', data.commentByUserId);
|
|
let commentByName = 'Unknown User';
|
|
try {
|
|
const commentByUser = await pb.collection('users').getOne(data.commentByUserId);
|
|
commentByName = commentByUser?.name || 'Unknown User';
|
|
console.log('Commenter user fetched:', {
|
|
id: commentByUser?.id,
|
|
name: commentByName
|
|
});
|
|
} catch (error) {
|
|
console.warn('Could not get commenter user name:', error);
|
|
}
|
|
|
|
const subject = `New Comment on Reimbursement: ${reimbursement.title}`;
|
|
|
|
console.log('Comment email details:', {
|
|
to: user.email,
|
|
subject,
|
|
commentBy: commentByName,
|
|
commentPreview: data.comment.substring(0, 50) + (data.comment.length > 50 ? '...' : '')
|
|
});
|
|
|
|
const html = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${subject}</title>
|
|
</head>
|
|
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px; margin-bottom: 30px;">
|
|
<h1 style="color: white; margin: 0; font-size: 24px;">IEEE UCSD Reimbursement Comment</h1>
|
|
</div>
|
|
|
|
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin-bottom: 25px;">
|
|
<h2 style="margin-top: 0; color: #2c3e50;">New Comment Added</h2>
|
|
<p>Hello ${user.name},</p>
|
|
<p>A new comment has been added to your reimbursement request "<strong>${reimbursement.title}</strong>".</p>
|
|
|
|
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #3498db; margin: 20px 0;">
|
|
<div style="margin-bottom: 15px;">
|
|
<span style="font-weight: bold; color: #2980b9;">Comment by:</span> ${commentByName}
|
|
</div>
|
|
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px;">
|
|
<p style="margin: 0; font-style: italic;">${data.comment}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin: 25px 0;">
|
|
<h3 style="color: #2c3e50; margin-bottom: 15px;">Reimbursement Details</h3>
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; width: 30%;">Status:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
<span style="background: ${getStatusColor(reimbursement.status)}; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">
|
|
${getStatusText(reimbursement.status)}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Amount:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">$${reimbursement.total_amount.toFixed(2)}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; font-weight: bold;">Date of Purchase:</td>
|
|
<td style="padding: 8px 0;">${new Date(reimbursement.date_of_purchase).toLocaleDateString()}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="text-align: center; padding: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
|
|
<p>This is an automated notification from IEEE UCSD Reimbursement System.</p>
|
|
<p>If you have any questions, please contact us at <a href="mailto:${replyToEmail}" style="color: #667eea;">${replyToEmail}</a></p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
console.log('Attempting to send comment email via Resend...');
|
|
const result = await resend.emails.send({
|
|
from: fromEmail,
|
|
to: [user.email],
|
|
replyTo: replyToEmail,
|
|
subject,
|
|
html,
|
|
});
|
|
|
|
console.log('Resend comment email response:', result);
|
|
console.log('Comment email sent successfully!');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Failed to send comment email:', error);
|
|
console.error('Comment email error details:', {
|
|
name: error instanceof Error ? error.name : 'Unknown',
|
|
message: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function sendSubmissionEmail(pb: any, resend: any, fromEmail: string, replyToEmail: string, data: any): Promise<boolean> {
|
|
try {
|
|
// Get reimbursement details
|
|
const reimbursement = await pb.collection('reimbursement').getOne(data.reimbursementId);
|
|
|
|
// Get submitter user details
|
|
const user = await pb.collection('users').getOne(reimbursement.submitted_by);
|
|
if (!user || !user.email) {
|
|
console.error('User not found or no email:', reimbursement.submitted_by);
|
|
return false;
|
|
}
|
|
|
|
// Send confirmation email to submitter
|
|
const submitterSubject = `Reimbursement Submitted: ${reimbursement.title}`;
|
|
|
|
const submitterHtml = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${submitterSubject}</title>
|
|
</head>
|
|
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
<div style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%); padding: 30px; border-radius: 10px; margin-bottom: 30px;">
|
|
<h1 style="color: white; margin: 0; font-size: 24px;">Reimbursement Submitted Successfully</h1>
|
|
</div>
|
|
|
|
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin-bottom: 25px;">
|
|
<h2 style="margin-top: 0; color: #2c3e50;">Submission Confirmed</h2>
|
|
<p>Hello ${user.name},</p>
|
|
<p>Your reimbursement request has been successfully submitted and is now under review.</p>
|
|
|
|
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #28a745; margin: 20px 0;">
|
|
<h3 style="margin-top: 0; color: #155724;">Reimbursement Details</h3>
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; width: 30%;">Title:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${reimbursement.title}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Amount:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">$${reimbursement.total_amount.toFixed(2)}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Date of Purchase:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${new Date(reimbursement.date_of_purchase).toLocaleDateString()}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Department:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${reimbursement.department.charAt(0).toUpperCase() + reimbursement.department.slice(1)}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Payment Method:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${reimbursement.payment_method}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; font-weight: bold;">Status:</td>
|
|
<td style="padding: 8px 0;">
|
|
<span style="background: #ffc107; color: #212529; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">
|
|
Submitted
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div style="background: #d4edda; padding: 15px; border-radius: 8px; border-left: 4px solid #28a745; margin: 20px 0;">
|
|
<h4 style="margin: 0 0 10px 0; color: #155724;">What happens next?</h4>
|
|
<ul style="margin: 0; padding-left: 20px; color: #155724;">
|
|
<li>Your receipts will be reviewed by our team</li>
|
|
<li>You'll receive email updates as the status changes</li>
|
|
<li>Once approved, payment will be processed</li>
|
|
<li>Typical processing time is 1-2 weeks</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="text-align: center; padding: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
|
|
<p>This is an automated notification from IEEE UCSD Reimbursement System.</p>
|
|
<p>If you have any questions, please contact us at <a href="mailto:${replyToEmail}" style="color: #667eea;">${replyToEmail}</a></p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
// Send notification email to treasurer
|
|
const treasurerSubject = `New Reimbursement Request: ${reimbursement.title} - $${reimbursement.total_amount.toFixed(2)}`;
|
|
|
|
const treasurerHtml = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${treasurerSubject}</title>
|
|
</head>
|
|
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
<div style="background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); padding: 30px; border-radius: 10px; margin-bottom: 30px;">
|
|
<h1 style="color: white; margin: 0; font-size: 24px;">New Reimbursement Request</h1>
|
|
</div>
|
|
|
|
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin-bottom: 25px;">
|
|
<h2 style="margin-top: 0; color: #2c3e50;">Action Required</h2>
|
|
<p>Hello Treasurer,</p>
|
|
<p>A new reimbursement request has been submitted and is awaiting review.</p>
|
|
|
|
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #007bff; margin: 20px 0;">
|
|
<h3 style="margin-top: 0; color: #004085;">Reimbursement Details</h3>
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; width: 30%;">Submitted by:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${user.name} (${user.email})</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Title:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${reimbursement.title}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Amount:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; color: #28a745;">$${reimbursement.total_amount.toFixed(2)}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Date of Purchase:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${new Date(reimbursement.date_of_purchase).toLocaleDateString()}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Department:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${reimbursement.department.charAt(0).toUpperCase() + reimbursement.department.slice(1)}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Payment Method:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${reimbursement.payment_method}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Submitted:</td>
|
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${new Date(reimbursement.created).toLocaleString()}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; font-weight: bold;">Status:</td>
|
|
<td style="padding: 8px 0;">
|
|
<span style="background: #ffc107; color: #212529; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">
|
|
Submitted - Awaiting Review
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
${reimbursement.additional_info ? `
|
|
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee;">
|
|
<h4 style="margin: 0 0 10px 0; color: #495057;">Additional Information:</h4>
|
|
<div style="background: #f8f9fa; padding: 12px; border-radius: 6px; font-style: italic;">
|
|
${reimbursement.additional_info}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
<div style="background: #e7f3ff; padding: 15px; border-radius: 8px; border-left: 4px solid #007bff; margin: 20px 0;">
|
|
<h4 style="margin: 0 0 10px 0; color: #004085;">Next Steps:</h4>
|
|
<ul style="margin: 0; padding-left: 20px; color: #004085;">
|
|
<li>Review the submitted receipts and documentation</li>
|
|
<li>Log into the reimbursement portal to approve or request changes</li>
|
|
<li>The submitter will be notified of any status updates</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="text-align: center; padding: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
|
|
<p>This is an automated notification from IEEE UCSD Reimbursement System.</p>
|
|
<p>If you have any questions, please contact the submitter directly at <a href="mailto:${user.email}" style="color: #667eea;">${user.email}</a></p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
// Send both emails
|
|
const submitterResult = await resend.emails.send({
|
|
from: fromEmail,
|
|
to: [user.email],
|
|
replyTo: replyToEmail,
|
|
subject: submitterSubject,
|
|
html: submitterHtml,
|
|
});
|
|
|
|
const treasurerResult = await resend.emails.send({
|
|
from: fromEmail,
|
|
to: ['treasurer@ieeeatucsd.org'],
|
|
replyTo: user.email, // Set reply-to as the submitter for treasurer's convenience
|
|
subject: treasurerSubject,
|
|
html: treasurerHtml,
|
|
});
|
|
|
|
console.log('Submission confirmation email sent successfully:', submitterResult);
|
|
console.log('Treasurer notification email sent successfully:', treasurerResult);
|
|
|
|
// Return true if at least one email was sent successfully
|
|
return !!(submitterResult && treasurerResult);
|
|
} catch (error) {
|
|
console.error('Failed to send submission emails:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function sendTestEmail(resend: any, fromEmail: string, replyToEmail: string, email: string): Promise<boolean> {
|
|
try {
|
|
console.log('Starting test email process...');
|
|
console.log('Test email configuration:', {
|
|
fromEmail,
|
|
replyToEmail,
|
|
toEmail: email,
|
|
hasResend: !!resend
|
|
});
|
|
|
|
const subject = 'Test Email from IEEE UCSD Reimbursement System';
|
|
|
|
const html = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${subject}</title>
|
|
</head>
|
|
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px; margin-bottom: 30px;">
|
|
<h1 style="color: white; margin: 0; font-size: 24px;">Test Email</h1>
|
|
</div>
|
|
|
|
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin-bottom: 25px;">
|
|
<h2 style="margin-top: 0; color: #2c3e50;">Email System Test</h2>
|
|
<p>This is a test email from the IEEE UCSD Reimbursement System.</p>
|
|
<p>If you receive this email, the notification system is working correctly!</p>
|
|
|
|
<div style="background: #d4edda; padding: 15px; border-radius: 8px; border-left: 4px solid #28a745; margin: 20px 0;">
|
|
<p style="margin: 0; color: #155724;">Email delivery successful</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="text-align: center; padding: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
|
|
<p>This is a test notification from IEEE UCSD Reimbursement System.</p>
|
|
<p>If you have any questions, please contact us at <a href="mailto:${replyToEmail}" style="color: #667eea;">${replyToEmail}</a></p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
console.log('Sending test email via Resend...');
|
|
const result = await resend.emails.send({
|
|
from: fromEmail,
|
|
to: [email],
|
|
replyTo: replyToEmail,
|
|
subject,
|
|
html,
|
|
});
|
|
|
|
console.log('Resend test email response:', result);
|
|
console.log('Test email sent successfully!');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Failed to send test email:', error);
|
|
console.error('Test email error details:', {
|
|
name: error instanceof Error ? error.name : 'Unknown',
|
|
message: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined
|
|
});
|
|
return false;
|
|
}
|
|
} |