diff --git a/app/api/admin/hackathons/route.ts b/app/api/admin/hackathons/route.ts index 7b257b4c..10ba8701 100644 --- a/app/api/admin/hackathons/route.ts +++ b/app/api/admin/hackathons/route.ts @@ -117,7 +117,7 @@ export async function PUT(request: NextRequest) { return NextResponse.json({ error: 'Hackathon data is required' }, { status: 400 }) } - const hackathon = await hackathonsService.updateHackathon(slug, data) + const hackathon = await hackathonsService.updateHackathon(slug, data, user.id) return NextResponse.json(hackathon) } catch (error) { diff --git a/app/api/admin/moderation/events/[id]/approve/route.ts b/app/api/admin/moderation/events/[id]/approve/route.ts index a453adfd..767b3fe1 100644 --- a/app/api/admin/moderation/events/[id]/approve/route.ts +++ b/app/api/admin/moderation/events/[id]/approve/route.ts @@ -40,20 +40,58 @@ export async function POST( notes ) - // Send notification email to company - if (approvedEvent.company && approvedEvent.company.email) { - const emailContent = getEventApprovedEmail({ - eventTitle: approvedEvent.title, - companyName: approvedEvent.company.name, - eventUrl: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/events/${approvedEvent.slug}`, - publishDate: new Date().toLocaleDateString(), - notes: notes || '', + // Get creator's email from profiles table + let creatorEmail: string | null = null + let creatorName: string | null = null + + if (approvedEvent.created_by) { + const { createClient } = await import('@/lib/supabase/server') + const supabase = await createClient() + const { data: creatorProfile } = await supabase + .from('profiles') + .select('email, first_name, last_name') + .eq('id', approvedEvent.created_by) + .single() + + if (creatorProfile) { + creatorEmail = creatorProfile.email + creatorName = creatorProfile.first_name + ? `${creatorProfile.first_name} ${creatorProfile.last_name || ''}`.trim() + : null + } + } + + // Prepare email content + const emailContent = getEventApprovedEmail({ + eventTitle: approvedEvent.title, + companyName: approvedEvent.company?.name || 'Your Company', + eventUrl: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/events/${approvedEvent.slug}`, + publishDate: new Date().toLocaleDateString(), + notes: notes || '', + creatorName: creatorName || undefined, + }) + + // Send notification email to event creator (primary) + if (creatorEmail) { + console.log(`📧 Sending event approval email to creator: ${creatorEmail}`) + await sendEmail({ + to: creatorEmail, + subject: emailContent.subject, + html: emailContent.html, + }).catch(error => { + console.error('❌ Failed to send approval email to creator:', error) }) + } + // Also send to company email if different from creator + if (approvedEvent.company?.email && approvedEvent.company.email !== creatorEmail) { + console.log(`📧 Sending event approval email to company: ${approvedEvent.company.email}`) await sendEmail({ to: approvedEvent.company.email, subject: emailContent.subject, html: emailContent.html, + }).catch(error => { + console.error('❌ Failed to send approval email to company:', error) }) } @@ -90,12 +128,19 @@ function getEventApprovedEmail(params: { eventUrl: string publishDate: string notes: string + creatorName?: string }) { + const greeting = params.creatorName ? `Hi ${params.creatorName},` : 'Hello,' + const content = `

🎉 Your Event is Live!

+

+ ${greeting} +

+

Great news! Your event has been approved and is now live on CodeUnia.

diff --git a/app/api/admin/moderation/events/[id]/reject/route.ts b/app/api/admin/moderation/events/[id]/reject/route.ts index b4adb9eb..e82d5e23 100644 --- a/app/api/admin/moderation/events/[id]/reject/route.ts +++ b/app/api/admin/moderation/events/[id]/reject/route.ts @@ -50,20 +50,58 @@ export async function POST( reason ) - // Send notification email to company - if (rejectedEvent.company && rejectedEvent.company.email) { - const emailContent = getEventRejectedEmail({ - eventTitle: rejectedEvent.title, - companyName: rejectedEvent.company.name, - rejectionReason: reason, - editUrl: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/dashboard/company/events/${rejectedEvent.slug}/edit`, - guidelines: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/guidelines`, + // Get creator's email from profiles table + let creatorEmail: string | null = null + let creatorName: string | null = null + + if (rejectedEvent.created_by) { + const { createClient } = await import('@/lib/supabase/server') + const supabase = await createClient() + const { data: creatorProfile } = await supabase + .from('profiles') + .select('email, first_name, last_name') + .eq('id', rejectedEvent.created_by) + .single() + + if (creatorProfile) { + creatorEmail = creatorProfile.email + creatorName = creatorProfile.first_name + ? `${creatorProfile.first_name} ${creatorProfile.last_name || ''}`.trim() + : null + } + } + + // Prepare email content + const emailContent = getEventRejectedEmail({ + eventTitle: rejectedEvent.title, + companyName: rejectedEvent.company?.name || 'Your Company', + rejectionReason: reason, + editUrl: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/dashboard/company/${rejectedEvent.company?.slug}/events`, + guidelines: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/guidelines`, + creatorName: creatorName || undefined, + }) + + // Send notification email to event creator (primary) + if (creatorEmail) { + console.log(`📧 Sending event rejection email to creator: ${creatorEmail}`) + await sendEmail({ + to: creatorEmail, + subject: emailContent.subject, + html: emailContent.html, + }).catch(error => { + console.error('❌ Failed to send rejection email to creator:', error) }) + } + // Also send to company email if different from creator + if (rejectedEvent.company?.email && rejectedEvent.company.email !== creatorEmail) { + console.log(`📧 Sending event rejection email to company: ${rejectedEvent.company.email}`) await sendEmail({ to: rejectedEvent.company.email, subject: emailContent.subject, html: emailContent.html, + }).catch(error => { + console.error('❌ Failed to send rejection email to company:', error) }) } @@ -100,14 +138,21 @@ function getEventRejectedEmail(params: { rejectionReason: string editUrl: string guidelines: string + creatorName?: string }) { + const greeting = params.creatorName ? `Hi ${params.creatorName},` : 'Hello,' + const content = `

Event Review Update

- Thank you for submitting your event to CodeUnia. After review, we're unable to approve your event at this time. + ${greeting} +

+ +

+ Thank you for submitting your event to Codeunia. After review, we're unable to approve your event at this time.

@@ -158,7 +203,7 @@ function getEventRejectedEmail(params: { - Event Review Update - CodeUnia + Event Review Update - Codeunia @@ -167,7 +212,7 @@ function getEventRejectedEmail(params: {
@@ -181,7 +226,7 @@ function getEventRejectedEmail(params: { Need help? Visit our Help Center

- © ${new Date().getFullYear()} CodeUnia. All rights reserved. + © ${new Date().getFullYear()} Codeunia. All rights reserved.

diff --git a/app/api/companies/[slug]/members/[userId]/route.ts b/app/api/companies/[slug]/members/[userId]/route.ts index c1e30786..030b58f9 100644 --- a/app/api/companies/[slug]/members/[userId]/route.ts +++ b/app/api/companies/[slug]/members/[userId]/route.ts @@ -5,7 +5,7 @@ import { companyMemberService } from '@/lib/services/company-member-service' import { CompanyError } from '@/types/company' import { UnifiedCache } from '@/lib/unified-cache-system' import { z } from 'zod' -import { getRoleChangeEmail, sendCompanyEmail } from '@/lib/email/company-emails' +import { getRoleChangeEmail, getMemberRemovedEmail, sendCompanyEmail } from '@/lib/email/company-emails' // Force Node.js runtime for API routes export const runtime = 'nodejs' @@ -263,9 +263,50 @@ export async function DELETE( ) } + // Get member's profile information for email before removing + const { data: memberProfile } = await supabase + .from('profiles') + .select('email, first_name, last_name') + .eq('id', userId) + .single() + + // Get requesting user's name for email + const { data: requestingUserProfile } = await supabase + .from('profiles') + .select('first_name, last_name') + .eq('id', user.id) + .single() + + const removedByName = requestingUserProfile?.first_name + ? `${requestingUserProfile.first_name} ${requestingUserProfile.last_name || ''}`.trim() + : 'a team administrator' + // Remove member await companyMemberService.removeMember(targetMember.id) + // Send removal notification email + if (memberProfile?.email) { + const memberName = memberProfile.first_name || memberProfile.email.split('@')[0] + + const emailContent = getMemberRemovedEmail({ + memberName, + companyName: company.name, + removedBy: removedByName, + role: targetMember.role, + }) + + // Send email asynchronously (don't wait for it) + console.log(`📧 Sending member removal email to ${memberProfile.email}`) + sendCompanyEmail({ + to: memberProfile.email, + subject: emailContent.subject, + html: emailContent.html, + }).catch(error => { + console.error('❌ Failed to send member removal email:', error) + // Don't fail the request if email fails + }) + } + // Invalidate cache await UnifiedCache.purgeByTags(['content', 'api']) diff --git a/app/api/hackathons/[id]/route.ts b/app/api/hackathons/[id]/route.ts index 859ee2d6..d5ca61f0 100644 --- a/app/api/hackathons/[id]/route.ts +++ b/app/api/hackathons/[id]/route.ts @@ -96,7 +96,7 @@ export async function PUT(request: NextRequest, { params }: RouteContext) { ) } - const hackathon = await hackathonsService.updateHackathon(id, hackathonData) + const hackathon = await hackathonsService.updateHackathon(id, hackathonData, user.id) return NextResponse.json({ hackathon }) } catch (error) { diff --git a/app/companies/[slug]/events/page.tsx b/app/companies/[slug]/events/page.tsx index 9a72069d..0ba7e505 100644 --- a/app/companies/[slug]/events/page.tsx +++ b/app/companies/[slug]/events/page.tsx @@ -162,7 +162,7 @@ export default function CompanyEventsPage() { {/* Company Header */} {company && ( -
+
+
-
+
{/* Banner */}
- +
{/* Logo */} diff --git a/components/notifications/notification-utils.tsx b/components/notifications/notification-utils.tsx index 6e0b84f5..3b1ad82b 100644 --- a/components/notifications/notification-utils.tsx +++ b/components/notifications/notification-utils.tsx @@ -18,9 +18,13 @@ export function getNotificationIcon(type: NotificationType): LucideIcon { event_approved: CheckCircle2, event_rejected: XCircle, event_changes_requested: AlertCircle, + event_updated: AlertCircle, + event_status_changed: AlertCircle, hackathon_approved: CheckCircle2, hackathon_rejected: XCircle, hackathon_changes_requested: AlertCircle, + hackathon_updated: AlertCircle, + hackathon_status_changed: AlertCircle, new_event_registration: Calendar, new_hackathon_registration: Calendar, team_member_invited: UserPlus, @@ -40,9 +44,13 @@ export function getNotificationColor(type: NotificationType): string { event_approved: 'text-green-500', event_rejected: 'text-red-500', event_changes_requested: 'text-yellow-500', + event_updated: 'text-orange-500', + event_status_changed: 'text-yellow-500', hackathon_approved: 'text-green-500', hackathon_rejected: 'text-red-500', hackathon_changes_requested: 'text-yellow-500', + hackathon_updated: 'text-orange-500', + hackathon_status_changed: 'text-yellow-500', new_event_registration: 'text-blue-500', new_hackathon_registration: 'text-blue-500', team_member_invited: 'text-blue-500', diff --git a/lib/email/company-emails.ts b/lib/email/company-emails.ts index 0f13d52e..34168d92 100644 --- a/lib/email/company-emails.ts +++ b/lib/email/company-emails.ts @@ -41,7 +41,7 @@ const getEmailTemplate = (content: string) => ` Need help? Contact us at support@codeunia.com

- © ${new Date().getFullYear()} CodeUnia. All rights reserved. + © ${new Date().getFullYear()} Codeunia. All rights reserved.

@@ -64,7 +64,7 @@ export const getCompanyVerificationApprovedEmail = (params: {

- Congratulations! Your company ${params.companyName} has been successfully verified on CodeUnia. + Congratulations! Your company ${params.companyName} has been successfully verified on Codeunia.

@@ -96,7 +96,7 @@ export const getCompanyVerificationApprovedEmail = (params: { ` return { - subject: `🎉 ${params.companyName} has been verified on CodeUnia!`, + subject: `🎉 ${params.companyName} has been verified on Codeunia!`, html: getEmailTemplate(content) } } @@ -113,7 +113,7 @@ export const getCompanyVerificationRejectedEmail = (params: {

- Thank you for your interest in hosting events on CodeUnia. After reviewing your company registration for ${params.companyName}, we need additional information before we can proceed with verification. + Thank you for your interest in hosting events on Codeunia. After reviewing your company registration for ${params.companyName}, we need additional information before we can proceed with verification.

@@ -165,7 +165,7 @@ export const getNewCompanyRegistrationNotification = (params: {

- A new company has registered on CodeUnia and is awaiting verification. + A new company has registered on Codeunia and is awaiting verification.

@@ -228,6 +228,63 @@ export const getNewCompanyRegistrationNotification = (params: { } } +// Team member removed notification email +export const getMemberRemovedEmail = (params: { + memberName: string + companyName: string + removedBy: string + role: string +}) => { + const content = ` +

+ You've Been Removed from ${params.companyName} +

+ +

+ Hi ${params.memberName}, +

+ +

+ We're writing to inform you that your access to ${params.companyName} on Codeunia has been removed by ${params.removedBy}. +

+ +
+

+ Access Removed: You no longer have access to ${params.companyName}'s company dashboard, events, and team resources. +

+
+ +

+ What this means: +

+ +
    +
  • You can no longer access ${params.companyName}'s dashboard
  • +
  • You won't receive notifications about company events
  • +
  • Your previous role was: ${params.role.charAt(0).toUpperCase() + params.role.slice(1)}
  • +
+ +

+ Your personal Codeunia account remains active, and you can still: +

+ +
    +
  • Browse and register for public events
  • +
  • Access your profile and connections
  • +
  • Join other companies if invited
  • +
+ +

+ If you believe this was done in error or have questions, please contact the company administrator directly. +

+ ` + + return { + subject: `Access removed from ${params.companyName}`, + html: getEmailTemplate(content) + } +} + // Role change notification email export const getRoleChangeEmail = (params: { memberName: string @@ -340,7 +397,7 @@ export async function sendCompanyEmail(params: EmailParams) { const resend = new Resend(process.env.RESEND_API_KEY) const { data, error } = await resend.emails.send({ - from: process.env.COMPANY_FROM_EMAIL || 'CodeUnia ', + from: process.env.COMPANY_FROM_EMAIL || 'Codeunia ', to: params.to, subject: params.subject, html: params.html, diff --git a/lib/email/templates/event-rejected.tsx b/lib/email/templates/event-rejected.tsx index 399c0f75..4c1ac719 100644 --- a/lib/email/templates/event-rejected.tsx +++ b/lib/email/templates/event-rejected.tsx @@ -14,7 +14,7 @@ const getEmailTemplate = (content: string) => ` - CodeUnia + Codeunia
-

CodeUnia

+

Codeunia

@@ -23,7 +23,7 @@ const getEmailTemplate = (content: string) => `
@@ -37,7 +37,7 @@ const getEmailTemplate = (content: string) => ` Need help? Contact us at support@codeunia.com

- © ${new Date().getFullYear()} CodeUnia. All rights reserved. + © ${new Date().getFullYear()} Codeunia. All rights reserved.

diff --git a/lib/services/events.ts b/lib/services/events.ts index a9d01750..be3febc8 100644 --- a/lib/services/events.ts +++ b/lib/services/events.ts @@ -401,14 +401,13 @@ class EventsService { * Update an event * @param id Event ID * @param eventData Partial event data to update - * @param _userId ID of the user updating the event (reserved for future use) + * @param userId ID of the user updating the event * @returns Updated event */ async updateEvent( id: number, eventData: Partial, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _userId: string + userId: string ): Promise { const supabase = await createClient() @@ -436,12 +435,24 @@ class EventsService { } = eventData /* eslint-enable @typescript-eslint/no-unused-vars */ + // Check if this is an approved event being edited + // If so, reset to pending status for re-approval + const needsReapproval = existingEvent.approval_status === 'approved' + // Update the updated_at timestamp - const updatePayload = { + const updatePayload: Record = { ...updateData, updated_at: new Date().toISOString(), } + // If event was approved, reset to pending for re-approval + if (needsReapproval) { + updatePayload.approval_status = 'pending' + updatePayload.approved_by = null + updatePayload.approved_at = null + console.log(`🔄 Event ${id} was approved, resetting to pending for re-approval`) + } + const { data: event, error } = await supabase .from('events') .update(updatePayload) @@ -463,6 +474,62 @@ class EventsService { throw new EventError('Failed to update event', EventErrorCodes.NOT_FOUND, 500) } + // If event needed re-approval, create notifications and log + if (needsReapproval) { + // Import notification service dynamically to avoid circular dependencies + const { NotificationService } = await import('./notification-service') + const { moderationService } = await import('./moderation-service') + + // Log the edit action + await moderationService.logModerationAction('edited', id, undefined, userId, 'Event edited after approval - requires re-approval') + + // Notify admins about the updated event + // Get all admin users + const { data: adminUsers } = await supabase + .from('profiles') + .select('id') + .eq('role', 'admin') + + if (adminUsers && adminUsers.length > 0) { + // Create notifications for all admins + const notifications = adminUsers.map(admin => ({ + user_id: admin.id, + company_id: existingEvent.company_id, + type: 'event_updated' as const, + title: 'Event Updated - Requires Re-approval', + message: `"${existingEvent.title}" has been edited and requires re-approval`, + action_url: `/admin/moderation/events/${id}`, + action_label: 'Review Event', + event_id: id.toString(), + metadata: { + event_title: existingEvent.title, + event_slug: existingEvent.slug + } + })) + + await supabase.from('notifications').insert(notifications) + console.log(`📧 Notified ${adminUsers.length} admin(s) about updated event`) + } + + // Notify company members about the status change + if (existingEvent.company_id) { + await NotificationService.notifyCompanyMembers(existingEvent.company_id, { + type: 'event_status_changed', + title: 'Event Requires Re-approval', + message: `Your event "${existingEvent.title}" has been edited and requires re-approval`, + company_id: existingEvent.company_id, + action_url: `/dashboard/company/events/${existingEvent.slug}`, + action_label: 'View Event', + event_id: id.toString(), + metadata: { + event_title: existingEvent.title, + old_status: 'approved', + new_status: 'pending' + } + }) + } + } + clearCache() return event } diff --git a/lib/services/hackathons.ts b/lib/services/hackathons.ts index ab6d5291..21005ef6 100644 --- a/lib/services/hackathons.ts +++ b/lib/services/hackathons.ts @@ -197,14 +197,44 @@ class HackathonsService { return hackathon } - async updateHackathon(slug: string, hackathonData: Partial>): Promise { + async updateHackathon( + slug: string, + hackathonData: Partial>, + userId?: string + ): Promise { const supabase = await createClient() + // Get existing hackathon first + const existingHackathon = await this.getHackathonBySlug(slug) + if (!existingHackathon) { + throw new Error('Hackathon not found') + } + + // Check if this is an approved hackathon being edited + const needsReapproval = existingHackathon.approval_status === 'approved' + + // Prepare update payload + const updatePayload: Record = { + ...hackathonData, + updated_at: new Date().toISOString(), + } + + // If hackathon was approved, reset to pending for re-approval + if (needsReapproval) { + updatePayload.approval_status = 'pending' + updatePayload.approved_by = null + updatePayload.approved_at = null + console.log(`🔄 Hackathon ${existingHackathon.id} was approved, resetting to pending for re-approval`) + } + const { data: hackathon, error } = await supabase .from('hackathons') - .update(hackathonData) + .update(updatePayload) .eq('slug', slug) - .select() + .select(` + *, + company:companies(*) + `) .single() if (error) { @@ -212,6 +242,66 @@ class HackathonsService { throw new Error('Failed to update hackathon') } + // If hackathon needed re-approval, create notifications and log + if (needsReapproval && userId) { + const hackathonId = existingHackathon.id + if (!hackathonId) { + console.error('Hackathon ID is missing, cannot create notifications') + cache.clear() + return hackathon + } + // Import services dynamically to avoid circular dependencies + const { NotificationService } = await import('./notification-service') + const { moderationService } = await import('./moderation-service') + + // Log the edit action + await moderationService.logModerationAction('edited', undefined, hackathonId, userId, 'Hackathon edited after approval - requires re-approval') + + // Notify admins about the updated hackathon + const { data: adminUsers } = await supabase + .from('profiles') + .select('id') + .eq('role', 'admin') + + if (adminUsers && adminUsers.length > 0) { + const notifications = adminUsers.map(admin => ({ + user_id: admin.id, + company_id: existingHackathon.company_id, + type: 'hackathon_updated' as const, + title: 'Hackathon Updated - Requires Re-approval', + message: `"${existingHackathon.title}" has been edited and requires re-approval`, + action_url: `/admin/moderation/hackathons/${hackathonId}`, + action_label: 'Review Hackathon', + hackathon_id: hackathonId.toString(), + metadata: { + hackathon_title: existingHackathon.title, + hackathon_slug: existingHackathon.slug + } + })) + + await supabase.from('notifications').insert(notifications) + console.log(`📧 Notified ${adminUsers.length} admin(s) about updated hackathon`) + } + + // Notify company members about the status change + if (existingHackathon.company_id) { + await NotificationService.notifyCompanyMembers(existingHackathon.company_id, { + type: 'hackathon_status_changed', + title: 'Hackathon Requires Re-approval', + message: `Your hackathon "${existingHackathon.title}" has been edited and requires re-approval`, + company_id: existingHackathon.company_id, + action_url: `/dashboard/company/hackathons/${existingHackathon.slug}`, + action_label: 'View Hackathon', + hackathon_id: hackathonId.toString(), + metadata: { + hackathon_title: existingHackathon.title, + old_status: 'approved', + new_status: 'pending' + } + }) + } + } + // Clear cache after updating hackathon cache.clear() return hackathon diff --git a/types/notifications.ts b/types/notifications.ts index 0d3980c5..c415cf51 100644 --- a/types/notifications.ts +++ b/types/notifications.ts @@ -4,9 +4,13 @@ export type NotificationType = | 'event_approved' | 'event_rejected' | 'event_changes_requested' + | 'event_updated' + | 'event_status_changed' | 'hackathon_approved' | 'hackathon_rejected' | 'hackathon_changes_requested' + | 'hackathon_updated' + | 'hackathon_status_changed' | 'new_event_registration' | 'new_hackathon_registration' | 'team_member_invited'
-

CodeUnia

+

Codeunia