add officer role update
This commit is contained in:
parent
40b2ea48c1
commit
a57a4e6889
5 changed files with 601 additions and 1 deletions
|
@ -5,6 +5,7 @@ import type { User, Officer } from '../../../schemas/pocketbase/schema';
|
||||||
import { Button } from '../universal/Button';
|
import { Button } from '../universal/Button';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { Toast } from '../universal/Toast';
|
import { Toast } from '../universal/Toast';
|
||||||
|
import { EmailClient } from '../../../scripts/email/EmailClient';
|
||||||
|
|
||||||
// Interface for officer with expanded user data
|
// Interface for officer with expanded user data
|
||||||
interface OfficerWithUser extends Officer {
|
interface OfficerWithUser extends Officer {
|
||||||
|
@ -252,12 +253,34 @@ export default function OfficerManagement() {
|
||||||
try {
|
try {
|
||||||
const pb = auth.getPocketBase();
|
const pb = auth.getPocketBase();
|
||||||
|
|
||||||
|
// Store previous values for the email notification
|
||||||
|
const previousRole = officerToReplace.existingOfficer.role;
|
||||||
|
const previousType = officerToReplace.existingOfficer.type;
|
||||||
|
const currentUserId = auth.getUserId();
|
||||||
|
|
||||||
// Update the existing officer record
|
// Update the existing officer record
|
||||||
await pb.collection(Collections.OFFICERS).update(officerToReplace.existingOfficer.id, {
|
await pb.collection(Collections.OFFICERS).update(officerToReplace.existingOfficer.id, {
|
||||||
role: officerToReplace.newRole,
|
role: officerToReplace.newRole,
|
||||||
type: officerToReplace.newType
|
type: officerToReplace.newType
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Send email notification (non-blocking)
|
||||||
|
try {
|
||||||
|
await EmailClient.notifyOfficerRoleChange(
|
||||||
|
officerToReplace.existingOfficer.id,
|
||||||
|
previousRole,
|
||||||
|
previousType,
|
||||||
|
officerToReplace.newRole,
|
||||||
|
officerToReplace.newType,
|
||||||
|
currentUserId || undefined,
|
||||||
|
false // This is an update, not a new officer
|
||||||
|
);
|
||||||
|
console.log('Officer role change notification email sent successfully');
|
||||||
|
} catch (emailError) {
|
||||||
|
console.error('Failed to send officer role change notification email:', emailError);
|
||||||
|
// Don't show error to user - email failure shouldn't disrupt the main operation
|
||||||
|
}
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
toast.success(`Officer role updated successfully for ${officerToReplace.existingOfficer.expand?.user.name}`);
|
toast.success(`Officer role updated successfully for ${officerToReplace.existingOfficer.expand?.user.name}`);
|
||||||
|
|
||||||
|
@ -427,8 +450,13 @@ export default function OfficerManagement() {
|
||||||
|
|
||||||
// Check if user is already an officer
|
// Check if user is already an officer
|
||||||
const existingOfficer = officers.find(officer => officer.expand?.user.id === user.id);
|
const existingOfficer = officers.find(officer => officer.expand?.user.id === user.id);
|
||||||
|
const currentUserId = auth.getUserId();
|
||||||
|
|
||||||
if (existingOfficer) {
|
if (existingOfficer) {
|
||||||
|
// Store previous values for the email notification
|
||||||
|
const previousRole = existingOfficer.role;
|
||||||
|
const previousType = existingOfficer.type;
|
||||||
|
|
||||||
// Update existing officer
|
// Update existing officer
|
||||||
const updatedOfficer = await pb.collection(Collections.OFFICERS).update(existingOfficer.id, {
|
const updatedOfficer = await pb.collection(Collections.OFFICERS).update(existingOfficer.id, {
|
||||||
role: newOfficerRole,
|
role: newOfficerRole,
|
||||||
|
@ -436,6 +464,23 @@ export default function OfficerManagement() {
|
||||||
});
|
});
|
||||||
|
|
||||||
successfulUpdates.push(updatedOfficer);
|
successfulUpdates.push(updatedOfficer);
|
||||||
|
|
||||||
|
// Send email notification for role update (non-blocking)
|
||||||
|
try {
|
||||||
|
await EmailClient.notifyOfficerRoleChange(
|
||||||
|
existingOfficer.id,
|
||||||
|
previousRole,
|
||||||
|
previousType,
|
||||||
|
newOfficerRole,
|
||||||
|
validType,
|
||||||
|
currentUserId || undefined,
|
||||||
|
false // This is an update, not a new officer
|
||||||
|
);
|
||||||
|
console.log(`Officer role change notification sent for ${user.name}`);
|
||||||
|
} catch (emailError) {
|
||||||
|
console.error(`Failed to send officer role change notification for ${user.name}:`, emailError);
|
||||||
|
// Don't show error to user - email failure shouldn't disrupt the main operation
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create new officer record
|
// Create new officer record
|
||||||
const createdOfficer = await pb.collection(Collections.OFFICERS).create({
|
const createdOfficer = await pb.collection(Collections.OFFICERS).create({
|
||||||
|
@ -445,6 +490,23 @@ export default function OfficerManagement() {
|
||||||
});
|
});
|
||||||
|
|
||||||
successfulCreations.push(createdOfficer);
|
successfulCreations.push(createdOfficer);
|
||||||
|
|
||||||
|
// Send email notification for new officer (non-blocking)
|
||||||
|
try {
|
||||||
|
await EmailClient.notifyOfficerRoleChange(
|
||||||
|
createdOfficer.id,
|
||||||
|
undefined, // No previous role for new officers
|
||||||
|
undefined, // No previous type for new officers
|
||||||
|
newOfficerRole,
|
||||||
|
validType,
|
||||||
|
currentUserId || undefined,
|
||||||
|
true // This is a new officer
|
||||||
|
);
|
||||||
|
console.log(`New officer notification sent for ${user.name}`);
|
||||||
|
} catch (emailError) {
|
||||||
|
console.error(`Failed to send new officer notification for ${user.name}:`, emailError);
|
||||||
|
// Don't show error to user - email failure shouldn't disrupt the main operation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to process officer for user ${user.name}:`, error);
|
console.error(`Failed to process officer for user ${user.name}:`, error);
|
||||||
|
@ -679,8 +741,28 @@ export default function OfficerManagement() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Store previous values for the email notification
|
||||||
|
const previousType = officerToEdit.type;
|
||||||
|
|
||||||
await updateService.updateField(Collections.OFFICERS, officerId, 'type', newType);
|
await updateService.updateField(Collections.OFFICERS, officerId, 'type', newType);
|
||||||
|
|
||||||
|
// Send email notification for role type change (non-blocking)
|
||||||
|
try {
|
||||||
|
await EmailClient.notifyOfficerRoleChange(
|
||||||
|
officerId,
|
||||||
|
officerToEdit.role, // Role stays the same
|
||||||
|
previousType,
|
||||||
|
officerToEdit.role, // Role stays the same
|
||||||
|
newType,
|
||||||
|
currentUserId || undefined,
|
||||||
|
false // This is an update, not a new officer
|
||||||
|
);
|
||||||
|
console.log(`Officer type change notification sent for ${officerToEdit.expand?.user.name}`);
|
||||||
|
} catch (emailError) {
|
||||||
|
console.error(`Failed to send officer type change notification for ${officerToEdit.expand?.user.name}:`, emailError);
|
||||||
|
// Don't show error to user - email failure shouldn't disrupt the main operation
|
||||||
|
}
|
||||||
|
|
||||||
toast.success('Officer updated successfully');
|
toast.success('Officer updated successfully');
|
||||||
|
|
||||||
// Refresh officers list
|
// Refresh officers list
|
||||||
|
|
155
src/pages/api/email/send-officer-notification.ts
Normal file
155
src/pages/api/email/send-officer-notification.ts
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { OfficerEmailNotifications } from '../../../scripts/email/OfficerEmailNotifications';
|
||||||
|
import type { OfficerRoleChangeEmailData } from '../../../scripts/email/OfficerEmailNotifications';
|
||||||
|
import { initializeEmailServices, authenticatePocketBase } from '../../../scripts/email/EmailHelpers';
|
||||||
|
import { Collections } from '../../../schemas/pocketbase';
|
||||||
|
import type { User, Officer } from '../../../schemas/pocketbase/schema';
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
console.log('📨 Officer notification email API called');
|
||||||
|
|
||||||
|
const requestData = await request.json();
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
officerId,
|
||||||
|
additionalContext,
|
||||||
|
authData
|
||||||
|
} = requestData;
|
||||||
|
|
||||||
|
console.log('📋 Request data:', {
|
||||||
|
type,
|
||||||
|
officerId,
|
||||||
|
hasAdditionalContext: !!additionalContext,
|
||||||
|
hasAuthData: !!authData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (type !== 'officer_role_change') {
|
||||||
|
console.error('❌ Invalid notification type for officer endpoint:', type);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: `Invalid notification type: ${type}` }),
|
||||||
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!officerId) {
|
||||||
|
console.error('❌ Missing required parameter: officerId');
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: 'Missing required parameter: officerId' }),
|
||||||
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize services - this creates a fresh PocketBase instance for server-side use
|
||||||
|
const { pb } = await initializeEmailServices();
|
||||||
|
|
||||||
|
// Authenticate with PocketBase if auth data is provided
|
||||||
|
authenticatePocketBase(pb, authData);
|
||||||
|
|
||||||
|
const emailService = OfficerEmailNotifications.getInstance();
|
||||||
|
|
||||||
|
// Get the officer record with user data
|
||||||
|
console.log('🔍 Fetching officer data...');
|
||||||
|
const officer = await pb.collection(Collections.OFFICERS).getOne(officerId, {
|
||||||
|
expand: 'user'
|
||||||
|
}) as Officer & { expand?: { user: User } };
|
||||||
|
|
||||||
|
if (!officer) {
|
||||||
|
console.error('❌ Officer not found:', officerId);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: 'Officer not found' }),
|
||||||
|
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user data from the expanded relation
|
||||||
|
const user = officer.expand?.user;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.error('❌ User data not found for officer:', officerId);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: 'User data not found for officer' }),
|
||||||
|
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract additional context data
|
||||||
|
const {
|
||||||
|
previousRole,
|
||||||
|
previousType,
|
||||||
|
newRole,
|
||||||
|
newType,
|
||||||
|
changedByUserId,
|
||||||
|
isNewOfficer
|
||||||
|
} = additionalContext || {};
|
||||||
|
|
||||||
|
// Get the name of the person who made the change
|
||||||
|
let changedByName = '';
|
||||||
|
if (changedByUserId) {
|
||||||
|
try {
|
||||||
|
const changedByUser = await pb.collection(Collections.USERS).getOne(changedByUserId) as User;
|
||||||
|
changedByName = changedByUser?.name || 'Unknown User';
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not fetch changed by user name:', error);
|
||||||
|
changedByName = 'Unknown User';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare email data
|
||||||
|
const emailData: OfficerRoleChangeEmailData = {
|
||||||
|
user,
|
||||||
|
officer,
|
||||||
|
previousRole,
|
||||||
|
previousType,
|
||||||
|
newRole: newRole || officer.role,
|
||||||
|
newType: newType || officer.type,
|
||||||
|
changedBy: changedByName,
|
||||||
|
isNewOfficer: isNewOfficer || false
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📧 Sending officer role change notification...');
|
||||||
|
console.log('📧 Email data:', {
|
||||||
|
userName: user.name,
|
||||||
|
userEmail: user.email,
|
||||||
|
officerRole: emailData.newRole,
|
||||||
|
officerType: emailData.newType,
|
||||||
|
previousRole: emailData.previousRole,
|
||||||
|
previousType: emailData.previousType,
|
||||||
|
changedBy: emailData.changedBy,
|
||||||
|
isNewOfficer: emailData.isNewOfficer
|
||||||
|
});
|
||||||
|
|
||||||
|
const success = await emailService.sendRoleChangeNotification(emailData);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
console.log('✅ Officer role change notification sent successfully');
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Officer role change notification sent successfully'
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error('❌ Failed to send officer role change notification');
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to send officer role change notification'
|
||||||
|
}),
|
||||||
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error in officer notification API:', error);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: 'Internal server error',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
}),
|
||||||
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -24,6 +24,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||||
// Determine which endpoint to redirect to based on email type
|
// Determine which endpoint to redirect to based on email type
|
||||||
const reimbursementTypes = ['status_change', 'comment', 'submission', 'test'];
|
const reimbursementTypes = ['status_change', 'comment', 'submission', 'test'];
|
||||||
const eventRequestTypes = ['event_request_submission', 'event_request_status_change', 'pr_completed', 'design_pr_notification'];
|
const eventRequestTypes = ['event_request_submission', 'event_request_status_change', 'pr_completed', 'design_pr_notification'];
|
||||||
|
const officerTypes = ['officer_role_change'];
|
||||||
|
|
||||||
let targetEndpoint = '';
|
let targetEndpoint = '';
|
||||||
|
|
||||||
|
@ -43,6 +44,15 @@ export const POST: APIRoute = async ({ request }) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
targetEndpoint = '/api/email/send-event-request-email';
|
targetEndpoint = '/api/email/send-event-request-email';
|
||||||
|
} else if (officerTypes.includes(type)) {
|
||||||
|
const { officerId } = requestData;
|
||||||
|
if (!officerId) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: 'Missing officerId for officer notification' }),
|
||||||
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
targetEndpoint = '/api/email/send-officer-notification';
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ Unknown notification type:', type);
|
console.error('❌ Unknown notification type:', type);
|
||||||
return new Response(
|
return new Response(
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
import { Authentication } from '../pocketbase/Authentication';
|
import { Authentication } from '../pocketbase/Authentication';
|
||||||
|
|
||||||
interface EmailNotificationRequest {
|
interface EmailNotificationRequest {
|
||||||
type: 'status_change' | 'comment' | 'submission' | 'test' | 'event_request_submission' | 'event_request_status_change' | 'pr_completed' | 'design_pr_notification';
|
type: 'status_change' | 'comment' | 'submission' | 'test' | 'event_request_submission' | 'event_request_status_change' | 'pr_completed' | 'design_pr_notification' | 'officer_role_change';
|
||||||
reimbursementId?: string;
|
reimbursementId?: string;
|
||||||
eventRequestId?: string;
|
eventRequestId?: string;
|
||||||
|
officerId?: string;
|
||||||
previousStatus?: string;
|
previousStatus?: string;
|
||||||
newStatus?: string;
|
newStatus?: string;
|
||||||
changedByUserId?: string;
|
changedByUserId?: string;
|
||||||
|
@ -74,6 +75,36 @@ export class EmailClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async sendOfficerNotification(request: EmailNotificationRequest): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const authData = this.getAuthData();
|
||||||
|
const requestWithAuth = {
|
||||||
|
...request,
|
||||||
|
authData
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch('/api/email/send-officer-notification', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestWithAuth),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: EmailNotificationResponse = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Officer notification API error:', result.error || result.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.success;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send officer notification:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send status change notification
|
* Send status change notification
|
||||||
*/
|
*/
|
||||||
|
@ -204,4 +235,30 @@ export class EmailClient {
|
||||||
additionalContext: { action }
|
additionalContext: { action }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send officer role change notification
|
||||||
|
*/
|
||||||
|
static async notifyOfficerRoleChange(
|
||||||
|
officerId: string,
|
||||||
|
previousRole?: string,
|
||||||
|
previousType?: string,
|
||||||
|
newRole?: string,
|
||||||
|
newType?: string,
|
||||||
|
changedByUserId?: string,
|
||||||
|
isNewOfficer?: boolean
|
||||||
|
): Promise<boolean> {
|
||||||
|
return this.sendOfficerNotification({
|
||||||
|
type: 'officer_role_change',
|
||||||
|
officerId,
|
||||||
|
additionalContext: {
|
||||||
|
previousRole,
|
||||||
|
previousType,
|
||||||
|
newRole,
|
||||||
|
newType,
|
||||||
|
changedByUserId,
|
||||||
|
isNewOfficer
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
296
src/scripts/email/OfficerEmailNotifications.ts
Normal file
296
src/scripts/email/OfficerEmailNotifications.ts
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
import { Resend } from 'resend';
|
||||||
|
import type { User, Officer } from '../../schemas/pocketbase/schema';
|
||||||
|
import { OfficerTypes } from '../../schemas/pocketbase';
|
||||||
|
|
||||||
|
// Email template data interfaces
|
||||||
|
export interface OfficerRoleChangeEmailData {
|
||||||
|
user: User;
|
||||||
|
officer: Officer;
|
||||||
|
previousRole?: string;
|
||||||
|
previousType?: string;
|
||||||
|
newRole: string;
|
||||||
|
newType: string;
|
||||||
|
changedBy?: string;
|
||||||
|
isNewOfficer?: boolean; // If this is a new officer appointment
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OfficerEmailNotifications {
|
||||||
|
private resend: Resend;
|
||||||
|
private fromEmail: string;
|
||||||
|
private replyToEmail: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Initialize Resend with API key from environment
|
||||||
|
const apiKey = import.meta.env.RESEND_API_KEY;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('RESEND_API_KEY environment variable is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resend = new Resend(apiKey);
|
||||||
|
this.fromEmail = import.meta.env.FROM_EMAIL || 'IEEE UCSD <noreply@transactional.ieeeatucsd.org>';
|
||||||
|
this.replyToEmail = import.meta.env.REPLY_TO_EMAIL || 'ieee@ucsd.edu';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static instance: OfficerEmailNotifications | null = null;
|
||||||
|
|
||||||
|
public static getInstance(): OfficerEmailNotifications {
|
||||||
|
if (!OfficerEmailNotifications.instance) {
|
||||||
|
OfficerEmailNotifications.instance = new OfficerEmailNotifications();
|
||||||
|
}
|
||||||
|
return OfficerEmailNotifications.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send officer role change notification email
|
||||||
|
*/
|
||||||
|
async sendRoleChangeNotification(data: OfficerRoleChangeEmailData): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const { user, officer, previousRole, previousType, newRole, newType, changedBy, isNewOfficer } = data;
|
||||||
|
|
||||||
|
const subject = isNewOfficer
|
||||||
|
? `Welcome to IEEE UCSD Leadership - ${newRole}`
|
||||||
|
: `Your IEEE UCSD Officer Role has been Updated`;
|
||||||
|
|
||||||
|
const typeColor = this.getOfficerTypeColor(newType);
|
||||||
|
const typeText = this.getOfficerTypeDisplayName(newType);
|
||||||
|
|
||||||
|
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 Officer Update</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin-bottom: 25px;">
|
||||||
|
<h2 style="margin-top: 0; color: #2c3e50;">
|
||||||
|
${isNewOfficer ? 'Welcome to the Team!' : 'Role Update'}
|
||||||
|
</h2>
|
||||||
|
<p>Hello ${user.name},</p>
|
||||||
|
|
||||||
|
${isNewOfficer ? `
|
||||||
|
<p>Congratulations! You have been appointed as an officer for IEEE UCSD. We're excited to have you join our leadership team!</p>
|
||||||
|
` : `
|
||||||
|
<p>Your officer role has been updated in the IEEE UCSD system.</p>
|
||||||
|
`}
|
||||||
|
|
||||||
|
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid ${typeColor}; margin: 20px 0;">
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<h3 style="margin: 0 0 10px 0; color: #2c3e50;">Your Current Role</h3>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
|
<span style="font-weight: bold; font-size: 18px; color: #2c3e50;">${newRole}</span>
|
||||||
|
<span style="background: ${typeColor}; color: white; padding: 6px 12px; border-radius: 20px; font-size: 14px; font-weight: 500;">${typeText}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${!isNewOfficer && (previousRole || previousType) ? `
|
||||||
|
<div style="color: #666; font-size: 14px; padding: 10px 0; border-top: 1px solid #eee;">
|
||||||
|
<strong>Previous:</strong> ${previousRole || 'Unknown Role'} (${this.getOfficerTypeDisplayName(previousType || '')})
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${changedBy ? `
|
||||||
|
<div style="color: #666; font-size: 14px; margin-top: 10px;">
|
||||||
|
${isNewOfficer ? 'Appointed' : 'Updated'} by: ${changedBy}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 25px 0;">
|
||||||
|
<h3 style="color: #2c3e50; margin-bottom: 15px;">Officer Information</h3>
|
||||||
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; width: 30%;">Name:</td>
|
||||||
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${user.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Email:</td>
|
||||||
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${user.email}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Role:</td>
|
||||||
|
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${newRole}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; font-weight: bold;">Officer Type:</td>
|
||||||
|
<td style="padding: 8px 0;">${typeText}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.getOfficerTypeDescription(newType)}
|
||||||
|
|
||||||
|
<div style="background: #e8f4fd; padding: 15px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #3498db;">
|
||||||
|
<h4 style="margin: 0 0 10px 0; color: #2980b9;">Next Steps:</h4>
|
||||||
|
<ul style="margin: 0; padding-left: 20px;">
|
||||||
|
<li>Check your access to the officer dashboard</li>
|
||||||
|
<li>Familiarize yourself with your new responsibilities</li>
|
||||||
|
<li>Reach out to other officers if you have questions</li>
|
||||||
|
${isNewOfficer ? '<li>Attend the next officer meeting to get up to speed</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 Officer Management System.</p>
|
||||||
|
<p>If you have any questions about your role, please contact us at <a href="mailto:${this.replyToEmail}" style="color: #667eea;">${this.replyToEmail}</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.resend.emails.send({
|
||||||
|
from: this.fromEmail,
|
||||||
|
to: [user.email],
|
||||||
|
replyTo: this.replyToEmail,
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Officer role change email sent successfully:', result);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send officer role change email:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color for officer type badge
|
||||||
|
*/
|
||||||
|
private getOfficerTypeColor(type: string): string {
|
||||||
|
switch (type) {
|
||||||
|
case OfficerTypes.ADMINISTRATOR:
|
||||||
|
return '#dc3545'; // Red for admin
|
||||||
|
case OfficerTypes.EXECUTIVE:
|
||||||
|
return '#6f42c1'; // Purple for executive
|
||||||
|
case OfficerTypes.GENERAL:
|
||||||
|
return '#007bff'; // Blue for general
|
||||||
|
case OfficerTypes.HONORARY:
|
||||||
|
return '#fd7e14'; // Orange for honorary
|
||||||
|
case OfficerTypes.PAST:
|
||||||
|
return '#6c757d'; // Gray for past
|
||||||
|
default:
|
||||||
|
return '#28a745'; // Green as default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display name for officer type
|
||||||
|
*/
|
||||||
|
private getOfficerTypeDisplayName(type: string): string {
|
||||||
|
switch (type) {
|
||||||
|
case OfficerTypes.ADMINISTRATOR:
|
||||||
|
return 'Administrator';
|
||||||
|
case OfficerTypes.EXECUTIVE:
|
||||||
|
return 'Executive Officer';
|
||||||
|
case OfficerTypes.GENERAL:
|
||||||
|
return 'General Officer';
|
||||||
|
case OfficerTypes.HONORARY:
|
||||||
|
return 'Honorary Officer';
|
||||||
|
case OfficerTypes.PAST:
|
||||||
|
return 'Past Officer';
|
||||||
|
default:
|
||||||
|
return 'Officer';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get description for officer type
|
||||||
|
*/
|
||||||
|
private getOfficerTypeDescription(type: string): string {
|
||||||
|
const baseStyle = "background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 20px 0;";
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case OfficerTypes.ADMINISTRATOR:
|
||||||
|
return `
|
||||||
|
<div style="${baseStyle}">
|
||||||
|
<p style="margin: 0; font-size: 14px;"><strong>Administrator Role:</strong></p>
|
||||||
|
<p style="margin: 5px 0 0 0; font-size: 14px;">
|
||||||
|
As an administrator, you have full access to manage officers, events, and system settings. You can add/remove other officers and access all administrative features.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
case OfficerTypes.EXECUTIVE:
|
||||||
|
return `
|
||||||
|
<div style="${baseStyle}">
|
||||||
|
<p style="margin: 0; font-size: 14px;"><strong>Executive Officer Role:</strong></p>
|
||||||
|
<p style="margin: 5px 0 0 0; font-size: 14px;">
|
||||||
|
As an executive officer, you have leadership responsibilities and access to advanced features in the officer dashboard. You can manage events and participate in key decision-making.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
case OfficerTypes.GENERAL:
|
||||||
|
return `
|
||||||
|
<div style="${baseStyle}">
|
||||||
|
<p style="margin: 0; font-size: 14px;"><strong>General Officer Role:</strong></p>
|
||||||
|
<p style="margin: 5px 0 0 0; font-size: 14px;">
|
||||||
|
As a general officer, you have access to the officer dashboard and can help with event management, member engagement, and other organizational activities.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
case OfficerTypes.HONORARY:
|
||||||
|
return `
|
||||||
|
<div style="${baseStyle}">
|
||||||
|
<p style="margin: 0; font-size: 14px;"><strong>Honorary Officer Role:</strong></p>
|
||||||
|
<p style="margin: 5px 0 0 0; font-size: 14px;">
|
||||||
|
As an honorary officer, you are recognized for your contributions to IEEE UCSD. You have access to officer resources and are part of our leadership community.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
case OfficerTypes.PAST:
|
||||||
|
return `
|
||||||
|
<div style="${baseStyle}">
|
||||||
|
<p style="margin: 0; font-size: 14px;"><strong>Past Officer Status:</strong></p>
|
||||||
|
<p style="margin: 5px 0 0 0; font-size: 14px;">
|
||||||
|
Thank you for your service to IEEE UCSD! As a past officer, you maintain access to alumni resources and remain part of our leadership community.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
default:
|
||||||
|
return `
|
||||||
|
<div style="${baseStyle}">
|
||||||
|
<p style="margin: 0; font-size: 14px;"><strong>Officer Role:</strong></p>
|
||||||
|
<p style="margin: 5px 0 0 0; font-size: 14px;">
|
||||||
|
Welcome to the IEEE UCSD officer team! You now have access to officer resources and can contribute to our organization's activities.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch notify multiple officers (for bulk operations)
|
||||||
|
*/
|
||||||
|
async notifyBulkRoleChanges(
|
||||||
|
notifications: OfficerRoleChangeEmailData[]
|
||||||
|
): Promise<{ successful: number; failed: number }> {
|
||||||
|
let successful = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const notification of notifications) {
|
||||||
|
try {
|
||||||
|
const result = await this.sendRoleChangeNotification(notification);
|
||||||
|
if (result) {
|
||||||
|
successful++;
|
||||||
|
} else {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a small delay to avoid rate limiting
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send bulk notification:', error);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { successful, failed };
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue