From ae1cb8b1f5beb2888e3dde3c00cc4b744eccff41 Mon Sep 17 00:00:00 2001 From: Akshay Date: Mon, 24 Nov 2025 14:02:06 +0530 Subject: [PATCH] feat(companies): Add email notification preferences management for company settings - Create new API endpoint for fetching and updating company notification preferences - Add GET /api/companies/[slug]/notifications route to retrieve current notification settings - Add PUT /api/companies/[slug]/notifications route to update notification preferences with role-based access control - Implement notification preference types in company type definitions - Add notification preferences service for managing company email notification logic - Integrate notification preferences UI in company settings dashboard page - Add loading and saving states for notification preference updates - Implement role-based authorization (owner/admin only) for notification preference management - Allows company owners and admins to control email notifications for registrations, event approvals/rejections, team member joins, and subscription expiration alerts --- .../companies/[slug]/notifications/route.ts | 183 ++++++++++++++++++ .../company/[slug]/settings/page.tsx | 82 +++++++- lib/services/company-notifications.ts | 148 ++++++++++++++ types/company.ts | 14 ++ 4 files changed, 419 insertions(+), 8 deletions(-) create mode 100644 app/api/companies/[slug]/notifications/route.ts create mode 100644 lib/services/company-notifications.ts diff --git a/app/api/companies/[slug]/notifications/route.ts b/app/api/companies/[slug]/notifications/route.ts new file mode 100644 index 00000000..76e44371 --- /dev/null +++ b/app/api/companies/[slug]/notifications/route.ts @@ -0,0 +1,183 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createClient } from '@/lib/supabase/server' +import type { CompanyNotificationPreferences } from '@/types/company' + +// GET - Fetch notification preferences +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ slug: string }> } +) { + try { + const supabase = await createClient() + const { slug } = await params + + // Get current user + const { + data: { user }, + error: authError, + } = await supabase.auth.getUser() + + if (authError || !user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Get company + const { data: company, error: companyError } = await supabase + .from('companies') + .select('id, email_new_registration, email_event_approved, email_event_rejected, email_team_member_joined, email_subscription_expiring') + .eq('slug', slug) + .single() + + if (companyError || !company) { + return NextResponse.json({ error: 'Company not found' }, { status: 404 }) + } + + // Check if user is a member of the company + const { data: membership, error: membershipError } = await supabase + .from('company_members') + .select('role') + .eq('company_id', company.id) + .eq('user_id', user.id) + .eq('status', 'active') + .single() + + if (membershipError || !membership) { + return NextResponse.json( + { error: 'You are not a member of this company' }, + { status: 403 } + ) + } + + // Only owners and admins can view notification preferences + if (membership.role !== 'owner' && membership.role !== 'admin') { + return NextResponse.json( + { error: 'Insufficient permissions' }, + { status: 403 } + ) + } + + const preferences: CompanyNotificationPreferences = { + email_new_registration: company.email_new_registration ?? true, + email_event_approved: company.email_event_approved ?? true, + email_event_rejected: company.email_event_rejected ?? true, + email_team_member_joined: company.email_team_member_joined ?? true, + email_subscription_expiring: company.email_subscription_expiring ?? true, + } + + return NextResponse.json({ data: preferences }, { status: 200 }) + } catch (error) { + console.error('Error fetching notification preferences:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} + +// PUT - Update notification preferences +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ slug: string }> } +) { + try { + const supabase = await createClient() + const { slug } = await params + + // Get current user + const { + data: { user }, + error: authError, + } = await supabase.auth.getUser() + + if (authError || !user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Get company + const { data: company, error: companyError } = await supabase + .from('companies') + .select('id') + .eq('slug', slug) + .single() + + if (companyError || !company) { + return NextResponse.json({ error: 'Company not found' }, { status: 404 }) + } + + // Check if user is a member of the company + const { data: membership, error: membershipError } = await supabase + .from('company_members') + .select('role') + .eq('company_id', company.id) + .eq('user_id', user.id) + .eq('status', 'active') + .single() + + if (membershipError || !membership) { + return NextResponse.json( + { error: 'You are not a member of this company' }, + { status: 403 } + ) + } + + // Only owners and admins can update notification preferences + if (membership.role !== 'owner' && membership.role !== 'admin') { + return NextResponse.json( + { error: 'Insufficient permissions' }, + { status: 403 } + ) + } + + // Parse request body + const body = await request.json() + const preferences: Partial = {} + + // Validate and sanitize input + if (typeof body.email_new_registration === 'boolean') { + preferences.email_new_registration = body.email_new_registration + } + if (typeof body.email_event_approved === 'boolean') { + preferences.email_event_approved = body.email_event_approved + } + if (typeof body.email_event_rejected === 'boolean') { + preferences.email_event_rejected = body.email_event_rejected + } + if (typeof body.email_team_member_joined === 'boolean') { + preferences.email_team_member_joined = body.email_team_member_joined + } + if (typeof body.email_subscription_expiring === 'boolean') { + preferences.email_subscription_expiring = body.email_subscription_expiring + } + + // Update company notification preferences + const { error: updateError } = await supabase + .from('companies') + .update({ + ...preferences, + updated_at: new Date().toISOString(), + }) + .eq('id', company.id) + + if (updateError) { + console.error('Error updating notification preferences:', updateError) + return NextResponse.json( + { error: 'Failed to update notification preferences' }, + { status: 500 } + ) + } + + return NextResponse.json( + { + message: 'Notification preferences updated successfully', + data: preferences, + }, + { status: 200 } + ) + } catch (error) { + console.error('Error updating notification preferences:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/app/dashboard/company/[slug]/settings/page.tsx b/app/dashboard/company/[slug]/settings/page.tsx index 5800333e..351ab57a 100644 --- a/app/dashboard/company/[slug]/settings/page.tsx +++ b/app/dashboard/company/[slug]/settings/page.tsx @@ -75,6 +75,8 @@ export default function CompanySettingsPage() { email_team_member_joined: true, email_subscription_expiring: true, }) + const [loadingNotifications, setLoadingNotifications] = useState(false) + const [savingNotifications, setSavingNotifications] = useState(false) // Update form data when company changes React.useEffect(() => { @@ -105,6 +107,28 @@ export default function CompanySettingsPage() { } }, [currentCompany]) + // Fetch notification preferences when company changes + React.useEffect(() => { + const fetchNotificationPreferences = async () => { + if (!currentCompany) return + + setLoadingNotifications(true) + try { + const response = await fetch(`/api/companies/${currentCompany.slug}/notifications`) + if (response.ok) { + const result = await response.json() + setNotificationPrefs(result.data) + } + } catch (error) { + console.error('Error fetching notification preferences:', error) + } finally { + setLoadingNotifications(false) + } + } + + fetchNotificationPreferences() + }, [currentCompany]) + if (contextLoading || isPendingInvitation) { return (
@@ -944,7 +968,12 @@ export default function CompanySettingsPage() { -
+ {loadingNotifications ? ( +
+ +
+ ) : ( +
+ )}
diff --git a/lib/services/company-notifications.ts b/lib/services/company-notifications.ts new file mode 100644 index 00000000..291571ff --- /dev/null +++ b/lib/services/company-notifications.ts @@ -0,0 +1,148 @@ +import { createClient } from '@/lib/supabase/server' +import type { CompanyNotificationPreferences } from '@/types/company' + +/** + * Get notification preferences for a company + */ +export async function getCompanyNotificationPreferences( + companyId: string +): Promise { + try { + const supabase = await createClient() + + const { data, error } = await supabase + .from('companies') + .select( + 'email_new_registration, email_event_approved, email_event_rejected, email_team_member_joined, email_subscription_expiring' + ) + .eq('id', companyId) + .single() + + if (error || !data) { + console.error('Error fetching company notification preferences:', error) + return null + } + + return { + email_new_registration: data.email_new_registration ?? true, + email_event_approved: data.email_event_approved ?? true, + email_event_rejected: data.email_event_rejected ?? true, + email_team_member_joined: data.email_team_member_joined ?? true, + email_subscription_expiring: data.email_subscription_expiring ?? true, + } + } catch (error) { + console.error('Error in getCompanyNotificationPreferences:', error) + return null + } +} + +/** + * Check if a specific notification type is enabled for a company + */ +export async function isNotificationEnabled( + companyId: string, + notificationType: keyof CompanyNotificationPreferences +): Promise { + const preferences = await getCompanyNotificationPreferences(companyId) + + if (!preferences) { + // Default to true if we can't fetch preferences + return true + } + + return preferences[notificationType] ?? true +} + +/** + * Get company owner and admin emails for notifications + */ +export async function getCompanyNotificationRecipients( + companyId: string +): Promise { + try { + const supabase = await createClient() + + // Get all active owners and admins + const { data: members, error } = await supabase + .from('company_members') + .select('user_id') + .eq('company_id', companyId) + .in('role', ['owner', 'admin']) + .eq('status', 'active') + + if (error || !members || members.length === 0) { + console.error('Error fetching company members:', error) + return [] + } + + // Get user emails + const userIds = members.map((m) => m.user_id) + const { data: users, error: usersError } = await supabase + .from('profiles') + .select('email') + .in('id', userIds) + + if (usersError || !users) { + console.error('Error fetching user emails:', usersError) + return [] + } + + return users.map((u) => u.email).filter(Boolean) + } catch (error) { + console.error('Error in getCompanyNotificationRecipients:', error) + return [] + } +} + +/** + * Send notification email if enabled + * This is a helper function that checks preferences before sending + */ +export async function sendCompanyNotificationIfEnabled( + companyId: string, + notificationType: keyof CompanyNotificationPreferences, + emailData: { + subject: string + body: string + recipients?: string[] // Optional: override default recipients + } +): Promise { + try { + // Check if notification is enabled + const isEnabled = await isNotificationEnabled(companyId, notificationType) + + if (!isEnabled) { + console.log(`Notification ${notificationType} is disabled for company ${companyId}`) + return false + } + + // Get recipients if not provided + const recipients = emailData.recipients || await getCompanyNotificationRecipients(companyId) + + if (recipients.length === 0) { + console.log(`No recipients found for company ${companyId}`) + return false + } + + // TODO: Integrate with your email service (e.g., Resend, SendGrid, etc.) + // For now, just log the email + console.log('Sending notification email:', { + companyId, + notificationType, + recipients, + subject: emailData.subject, + }) + + // Example integration with email service: + // await sendEmail({ + // to: recipients, + // subject: emailData.subject, + // html: emailData.body, + // }) + + return true + } catch (error) { + console.error('Error in sendCompanyNotificationIfEnabled:', error) + return false + } +} diff --git a/types/company.ts b/types/company.ts index 3a7a0848..cf698036 100644 --- a/types/company.ts +++ b/types/company.ts @@ -36,6 +36,12 @@ export interface Company { subscription_expires_at?: string settings?: Record status: 'active' | 'suspended' | 'deleted' + // Notification preferences + email_new_registration?: boolean + email_event_approved?: boolean + email_event_rejected?: boolean + email_team_member_joined?: boolean + email_subscription_expiring?: boolean created_at: string updated_at: string created_by?: string @@ -121,6 +127,14 @@ export interface CompanyFilters { offset?: number } +export interface CompanyNotificationPreferences { + email_new_registration: boolean + email_event_approved: boolean + email_event_rejected: boolean + email_team_member_joined: boolean + email_subscription_expiring: boolean +} + export interface SubscriptionTierLimits { events_per_month: number | null // null = unlimited team_members: number | null