Skip to content

Commit 456eabf

Browse files
committed
feat(company-members): Add role change notification emails
- Implement role change email notification when member roles are updated - Add getRoleChangeEmail function with role-specific permissions display - Include member name, company name, old/new role, and changed by information - Send email asynchronously to avoid blocking the API response - Add dashboard link in email for easy access to company settings - Fix email template branding from "CodeUnia" to "Codeunia" for consistency - Include graceful error handling for email delivery failures - Fetch member and requesting user profile information for personalized emails
1 parent 8f4cf51 commit 456eabf

2 files changed

Lines changed: 146 additions & 2 deletions

File tree

app/api/companies/[slug]/members/[userId]/route.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { companyMemberService } from '@/lib/services/company-member-service'
55
import { CompanyError } from '@/types/company'
66
import { UnifiedCache } from '@/lib/unified-cache-system'
77
import { z } from 'zod'
8+
import { getRoleChangeEmail, sendCompanyEmail } from '@/lib/email/company-emails'
89

910
// Force Node.js runtime for API routes
1011
export const runtime = 'nodejs'
@@ -98,12 +99,59 @@ export async function PUT(
9899
)
99100
}
100101

102+
// Store old role for email notification
103+
const oldRole = targetMember.role
104+
101105
// Update member role
102106
const updatedMember = await companyMemberService.updateMemberRole(
103107
targetMember.id,
104108
role
105109
)
106110

111+
// Get member's profile information for email
112+
const { data: memberProfile } = await supabase
113+
.from('profiles')
114+
.select('email, first_name, last_name')
115+
.eq('id', userId)
116+
.single()
117+
118+
// Get requesting user's name for email
119+
const { data: requestingUserProfile } = await supabase
120+
.from('profiles')
121+
.select('first_name, last_name')
122+
.eq('id', user.id)
123+
.single()
124+
125+
const changedByName = requestingUserProfile?.first_name
126+
? `${requestingUserProfile.first_name} ${requestingUserProfile.last_name || ''}`.trim()
127+
: 'a team administrator'
128+
129+
// Send role change notification email
130+
if (memberProfile?.email && oldRole !== role) {
131+
const memberName = memberProfile.first_name || memberProfile.email.split('@')[0]
132+
const dashboardUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'https://codeunia.com'}/dashboard/company/${company.slug}`
133+
134+
const emailContent = getRoleChangeEmail({
135+
memberName,
136+
companyName: company.name,
137+
oldRole,
138+
newRole: role,
139+
changedBy: changedByName,
140+
dashboardUrl,
141+
})
142+
143+
// Send email asynchronously (don't wait for it)
144+
console.log(`📧 Sending role change email to ${memberProfile.email}: ${oldRole}${role}`)
145+
sendCompanyEmail({
146+
to: memberProfile.email,
147+
subject: emailContent.subject,
148+
html: emailContent.html,
149+
}).catch(error => {
150+
console.error('❌ Failed to send role change email:', error)
151+
// Don't fail the request if email fails
152+
})
153+
}
154+
107155
// Invalidate cache
108156
await UnifiedCache.purgeByTags(['content', 'api'])
109157

lib/email/company-emails.ts

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const getEmailTemplate = (content: string) => `
1313
<head>
1414
<meta charset="utf-8">
1515
<meta name="viewport" content="width=device-width, initial-scale=1.0">
16-
<title>CodeUnia</title>
16+
<title>Codeunia</title>
1717
</head>
1818
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
1919
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5f5f5; padding: 20px;">
@@ -23,7 +23,7 @@ const getEmailTemplate = (content: string) => `
2323
<!-- Header -->
2424
<tr>
2525
<td style="background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); padding: 30px; text-align: center;">
26-
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: bold;">CodeUnia</h1>
26+
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: bold;">Codeunia</h1>
2727
</td>
2828
</tr>
2929
@@ -228,6 +228,102 @@ export const getNewCompanyRegistrationNotification = (params: {
228228
}
229229
}
230230

231+
// Role change notification email
232+
export const getRoleChangeEmail = (params: {
233+
memberName: string
234+
companyName: string
235+
oldRole: string
236+
newRole: string
237+
changedBy: string
238+
dashboardUrl: string
239+
}) => {
240+
const rolePermissions: Record<string, string[]> = {
241+
owner: [
242+
'Full control over company settings',
243+
'Manage all team members and roles',
244+
'Create, edit, and delete all events',
245+
'Access billing and subscription',
246+
'View all analytics and reports'
247+
],
248+
admin: [
249+
'Create, edit, and publish events',
250+
'Manage team members (except owners)',
251+
'View analytics and reports',
252+
'Manage company profile'
253+
],
254+
editor: [
255+
'Create and edit draft events',
256+
'View published events',
257+
'View basic analytics'
258+
],
259+
viewer: [
260+
'View company events',
261+
'View basic analytics',
262+
'Read-only access'
263+
]
264+
}
265+
266+
const permissions = rolePermissions[params.newRole.toLowerCase()] || []
267+
268+
const content = `
269+
<h2 style="margin: 0 0 20px 0; color: #111827; font-size: 20px;">
270+
Your Role Has Been Updated
271+
</h2>
272+
273+
<p style="margin: 0 0 15px 0; color: #374151; font-size: 16px; line-height: 1.5;">
274+
Hi ${params.memberName},
275+
</p>
276+
277+
<p style="margin: 0 0 15px 0; color: #374151; font-size: 16px; line-height: 1.5;">
278+
Your role at <strong>${params.companyName}</strong> has been updated by ${params.changedBy}.
279+
</p>
280+
281+
<div style="background-color: #eff6ff; border-left: 4px solid #3b82f6; padding: 15px; margin: 20px 0; border-radius: 4px;">
282+
<table width="100%" cellpadding="0" cellspacing="0">
283+
<tr>
284+
<td style="padding: 8px 0; color: #6b7280; font-size: 14px; width: 120px;">
285+
<strong>Previous Role:</strong>
286+
</td>
287+
<td style="padding: 8px 0; color: #111827; font-size: 14px;">
288+
${params.oldRole.charAt(0).toUpperCase() + params.oldRole.slice(1)}
289+
</td>
290+
</tr>
291+
<tr>
292+
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">
293+
<strong>New Role:</strong>
294+
</td>
295+
<td style="padding: 8px 0; color: #111827; font-size: 14px;">
296+
<span style="background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); color: white; padding: 4px 12px; border-radius: 4px; font-weight: 600;">
297+
${params.newRole.charAt(0).toUpperCase() + params.newRole.slice(1)}
298+
</span>
299+
</td>
300+
</tr>
301+
</table>
302+
</div>
303+
304+
<p style="margin: 0 0 10px 0; color: #374151; font-size: 16px; line-height: 1.5;">
305+
<strong>Your new permissions include:</strong>
306+
</p>
307+
308+
<ul style="margin: 0 0 20px 0; padding-left: 20px; color: #374151; font-size: 14px; line-height: 1.8;">
309+
${permissions.map(perm => `<li>${perm}</li>`).join('')}
310+
</ul>
311+
312+
<a href="${params.dashboardUrl}" style="display: inline-block; background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 6px; font-weight: 600; margin-top: 10px;">
313+
Go to Dashboard
314+
</a>
315+
316+
<p style="margin: 20px 0 0 0; color: #6b7280; font-size: 14px; line-height: 1.5;">
317+
If you have questions about your new role or permissions, please contact your team administrator.
318+
</p>
319+
`
320+
321+
return {
322+
subject: `Your role at ${params.companyName} has been updated`,
323+
html: getEmailTemplate(content)
324+
}
325+
}
326+
231327
// Send email function using Resend
232328
export async function sendCompanyEmail(params: EmailParams) {
233329
try {

0 commit comments

Comments
 (0)