Skip to content

Commit ae1cb8b

Browse files
committed
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
1 parent e148811 commit ae1cb8b

File tree

4 files changed

+419
-8
lines changed

4 files changed

+419
-8
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { createClient } from '@/lib/supabase/server'
3+
import type { CompanyNotificationPreferences } from '@/types/company'
4+
5+
// GET - Fetch notification preferences
6+
export async function GET(
7+
request: NextRequest,
8+
{ params }: { params: Promise<{ slug: string }> }
9+
) {
10+
try {
11+
const supabase = await createClient()
12+
const { slug } = await params
13+
14+
// Get current user
15+
const {
16+
data: { user },
17+
error: authError,
18+
} = await supabase.auth.getUser()
19+
20+
if (authError || !user) {
21+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
22+
}
23+
24+
// Get company
25+
const { data: company, error: companyError } = await supabase
26+
.from('companies')
27+
.select('id, email_new_registration, email_event_approved, email_event_rejected, email_team_member_joined, email_subscription_expiring')
28+
.eq('slug', slug)
29+
.single()
30+
31+
if (companyError || !company) {
32+
return NextResponse.json({ error: 'Company not found' }, { status: 404 })
33+
}
34+
35+
// Check if user is a member of the company
36+
const { data: membership, error: membershipError } = await supabase
37+
.from('company_members')
38+
.select('role')
39+
.eq('company_id', company.id)
40+
.eq('user_id', user.id)
41+
.eq('status', 'active')
42+
.single()
43+
44+
if (membershipError || !membership) {
45+
return NextResponse.json(
46+
{ error: 'You are not a member of this company' },
47+
{ status: 403 }
48+
)
49+
}
50+
51+
// Only owners and admins can view notification preferences
52+
if (membership.role !== 'owner' && membership.role !== 'admin') {
53+
return NextResponse.json(
54+
{ error: 'Insufficient permissions' },
55+
{ status: 403 }
56+
)
57+
}
58+
59+
const preferences: CompanyNotificationPreferences = {
60+
email_new_registration: company.email_new_registration ?? true,
61+
email_event_approved: company.email_event_approved ?? true,
62+
email_event_rejected: company.email_event_rejected ?? true,
63+
email_team_member_joined: company.email_team_member_joined ?? true,
64+
email_subscription_expiring: company.email_subscription_expiring ?? true,
65+
}
66+
67+
return NextResponse.json({ data: preferences }, { status: 200 })
68+
} catch (error) {
69+
console.error('Error fetching notification preferences:', error)
70+
return NextResponse.json(
71+
{ error: 'Internal server error' },
72+
{ status: 500 }
73+
)
74+
}
75+
}
76+
77+
// PUT - Update notification preferences
78+
export async function PUT(
79+
request: NextRequest,
80+
{ params }: { params: Promise<{ slug: string }> }
81+
) {
82+
try {
83+
const supabase = await createClient()
84+
const { slug } = await params
85+
86+
// Get current user
87+
const {
88+
data: { user },
89+
error: authError,
90+
} = await supabase.auth.getUser()
91+
92+
if (authError || !user) {
93+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
94+
}
95+
96+
// Get company
97+
const { data: company, error: companyError } = await supabase
98+
.from('companies')
99+
.select('id')
100+
.eq('slug', slug)
101+
.single()
102+
103+
if (companyError || !company) {
104+
return NextResponse.json({ error: 'Company not found' }, { status: 404 })
105+
}
106+
107+
// Check if user is a member of the company
108+
const { data: membership, error: membershipError } = await supabase
109+
.from('company_members')
110+
.select('role')
111+
.eq('company_id', company.id)
112+
.eq('user_id', user.id)
113+
.eq('status', 'active')
114+
.single()
115+
116+
if (membershipError || !membership) {
117+
return NextResponse.json(
118+
{ error: 'You are not a member of this company' },
119+
{ status: 403 }
120+
)
121+
}
122+
123+
// Only owners and admins can update notification preferences
124+
if (membership.role !== 'owner' && membership.role !== 'admin') {
125+
return NextResponse.json(
126+
{ error: 'Insufficient permissions' },
127+
{ status: 403 }
128+
)
129+
}
130+
131+
// Parse request body
132+
const body = await request.json()
133+
const preferences: Partial<CompanyNotificationPreferences> = {}
134+
135+
// Validate and sanitize input
136+
if (typeof body.email_new_registration === 'boolean') {
137+
preferences.email_new_registration = body.email_new_registration
138+
}
139+
if (typeof body.email_event_approved === 'boolean') {
140+
preferences.email_event_approved = body.email_event_approved
141+
}
142+
if (typeof body.email_event_rejected === 'boolean') {
143+
preferences.email_event_rejected = body.email_event_rejected
144+
}
145+
if (typeof body.email_team_member_joined === 'boolean') {
146+
preferences.email_team_member_joined = body.email_team_member_joined
147+
}
148+
if (typeof body.email_subscription_expiring === 'boolean') {
149+
preferences.email_subscription_expiring = body.email_subscription_expiring
150+
}
151+
152+
// Update company notification preferences
153+
const { error: updateError } = await supabase
154+
.from('companies')
155+
.update({
156+
...preferences,
157+
updated_at: new Date().toISOString(),
158+
})
159+
.eq('id', company.id)
160+
161+
if (updateError) {
162+
console.error('Error updating notification preferences:', updateError)
163+
return NextResponse.json(
164+
{ error: 'Failed to update notification preferences' },
165+
{ status: 500 }
166+
)
167+
}
168+
169+
return NextResponse.json(
170+
{
171+
message: 'Notification preferences updated successfully',
172+
data: preferences,
173+
},
174+
{ status: 200 }
175+
)
176+
} catch (error) {
177+
console.error('Error updating notification preferences:', error)
178+
return NextResponse.json(
179+
{ error: 'Internal server error' },
180+
{ status: 500 }
181+
)
182+
}
183+
}

app/dashboard/company/[slug]/settings/page.tsx

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ export default function CompanySettingsPage() {
7575
email_team_member_joined: true,
7676
email_subscription_expiring: true,
7777
})
78+
const [loadingNotifications, setLoadingNotifications] = useState(false)
79+
const [savingNotifications, setSavingNotifications] = useState(false)
7880

7981
// Update form data when company changes
8082
React.useEffect(() => {
@@ -105,6 +107,28 @@ export default function CompanySettingsPage() {
105107
}
106108
}, [currentCompany])
107109

110+
// Fetch notification preferences when company changes
111+
React.useEffect(() => {
112+
const fetchNotificationPreferences = async () => {
113+
if (!currentCompany) return
114+
115+
setLoadingNotifications(true)
116+
try {
117+
const response = await fetch(`/api/companies/${currentCompany.slug}/notifications`)
118+
if (response.ok) {
119+
const result = await response.json()
120+
setNotificationPrefs(result.data)
121+
}
122+
} catch (error) {
123+
console.error('Error fetching notification preferences:', error)
124+
} finally {
125+
setLoadingNotifications(false)
126+
}
127+
}
128+
129+
fetchNotificationPreferences()
130+
}, [currentCompany])
131+
108132
if (contextLoading || isPendingInvitation) {
109133
return (
110134
<div className="flex items-center justify-center min-h-[60vh]">
@@ -944,7 +968,12 @@ export default function CompanySettingsPage() {
944968
</CardDescription>
945969
</CardHeader>
946970
<CardContent className="space-y-6">
947-
<div className="space-y-4">
971+
{loadingNotifications ? (
972+
<div className="flex items-center justify-center py-8">
973+
<Loader2 className="h-8 w-8 animate-spin text-primary" />
974+
</div>
975+
) : (
976+
<div className="space-y-4">
948977
<div className="flex items-center justify-between">
949978
<div className="space-y-0.5">
950979
<Label htmlFor="email_new_registration" className="text-zinc-200">
@@ -1054,18 +1083,55 @@ export default function CompanySettingsPage() {
10541083
/>
10551084
</div>
10561085
</div>
1086+
)}
10571087

10581088
<div className="flex justify-end">
10591089
<Button
1060-
onClick={() => {
1061-
toast({
1062-
title: 'Success',
1063-
description: 'Notification preferences saved',
1064-
})
1090+
onClick={async () => {
1091+
if (!currentCompany) return
1092+
1093+
setSavingNotifications(true)
1094+
try {
1095+
const response = await fetch(`/api/companies/${currentCompany.slug}/notifications`, {
1096+
method: 'PUT',
1097+
headers: {
1098+
'Content-Type': 'application/json',
1099+
},
1100+
body: JSON.stringify(notificationPrefs),
1101+
})
1102+
1103+
if (!response.ok) {
1104+
throw new Error('Failed to save notification preferences')
1105+
}
1106+
1107+
toast({
1108+
title: 'Success',
1109+
description: 'Notification preferences saved successfully',
1110+
})
1111+
} catch (error) {
1112+
console.error('Error saving notification preferences:', error)
1113+
toast({
1114+
title: 'Error',
1115+
description: 'Failed to save notification preferences',
1116+
variant: 'destructive',
1117+
})
1118+
} finally {
1119+
setSavingNotifications(false)
1120+
}
10651121
}}
1122+
disabled={savingNotifications || loadingNotifications}
10661123
>
1067-
<Save className="mr-2 h-4 w-4" />
1068-
Save Preferences
1124+
{savingNotifications ? (
1125+
<>
1126+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
1127+
Saving...
1128+
</>
1129+
) : (
1130+
<>
1131+
<Save className="mr-2 h-4 w-4" />
1132+
Save Preferences
1133+
</>
1134+
)}
10691135
</Button>
10701136
</div>
10711137
</CardContent>

0 commit comments

Comments
 (0)