admin_role: string | null
iems_role: string | null
memberships: Membership[]
@@ -32,6 +33,8 @@ export interface Settings {
available_bodies: AvailableBody[]
}
+export const SENATE_TYPES = ['Full Body', 'Weekly', 'Office Hours'] as const
+
interface SettingsModalProps {
onClose: () => void
cachedSettings?: Settings | null
@@ -126,8 +129,23 @@ export default function SettingsModal({ onClose, cachedSettings, onSettingsLoade
})
}
+ const toggleSenateType = async (type: string, value: boolean) => {
+ if (!settings) return
+ const updated = { ...settings.senate_type_preferences, [type]: value }
+ setSettings(prev => prev ? { ...prev, senate_type_preferences: updated } : prev)
+ await fetch('/api/me/settings', {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ senate_type_preferences: updated }),
+ })
+ // My Rooms only re-reads this preference when it (re)fetches its bookings, so
+ // nudge it to refresh if it's open underneath this modal.
+ window.dispatchEvent(new Event('chambers:senate-prefs-updated'))
+ }
+
const eligibleKeys = settings ? getPrefsForRole(settings.admin_role, settings.iems_role) : []
const selectedBody = settings?.available_bodies.find(b => b.id === selectedBodyId)
+ const isSenateMember = settings?.memberships.some(m => m.bodies?.name === 'Senate') ?? false
return (
@@ -248,6 +266,26 @@ export default function SettingsModal({ onClose, cachedSettings, onSettingsLoade
)}
+ {/* Senate Session Types */}
+ {!loading && isSenateMember && (
+
+
Senate Session Types Shown in My Rooms
+
+ {SENATE_TYPES.map(type => (
+
+ ))}
+
+
+ )}
+
{/* Email Notifications */}
{!loading && eligibleKeys.length > 0 && (
diff --git a/app/api/me/settings/route.ts b/app/api/me/settings/route.ts
index 975b1e1..08de6ff 100644
--- a/app/api/me/settings/route.ts
+++ b/app/api/me/settings/route.ts
@@ -27,7 +27,7 @@ export async function GET() {
] = await Promise.all([
adminSupabase
.from('users')
- .select('full_name, email_preferences, admin_role, iems_role, board_memberships(id, role, bodies(id, name, division))')
+ .select('full_name, email_preferences, senate_type_preferences, admin_role, iems_role, board_memberships(id, role, bodies(id, name, division))')
.eq('id', user.id)
.single(),
adminSupabase
@@ -57,6 +57,7 @@ export async function GET() {
return NextResponse.json({
full_name: profile.full_name,
email_preferences: profile.email_preferences,
+ senate_type_preferences: profile.senate_type_preferences,
admin_role: profile.admin_role,
iems_role: profile.iems_role,
memberships: profile.board_memberships ?? [],
@@ -75,7 +76,7 @@ export async function PATCH(request: Request) {
if (rateLimitRes) return rateLimitRes
const body = await request.json()
- const { full_name, email_preferences } = body
+ const { full_name, email_preferences, senate_type_preferences } = body
if (full_name !== undefined && (typeof full_name !== 'string' || full_name.trim() === '')) {
return NextResponse.json({ error: 'full_name must be a non-empty string' }, { status: 400 })
@@ -88,9 +89,17 @@ export async function PATCH(request: Request) {
return NextResponse.json({ error: 'email_preferences must be a plain object' }, { status: 400 })
}
+ if (
+ senate_type_preferences !== undefined &&
+ (typeof senate_type_preferences !== 'object' || Array.isArray(senate_type_preferences) || senate_type_preferences === null)
+ ) {
+ return NextResponse.json({ error: 'senate_type_preferences must be a plain object' }, { status: 400 })
+ }
+
const updates: Record = {}
if (full_name !== undefined) updates.full_name = full_name.trim()
if (email_preferences !== undefined) updates.email_preferences = email_preferences
+ if (senate_type_preferences !== undefined) updates.senate_type_preferences = senate_type_preferences
if (Object.keys(updates).length === 0) {
return NextResponse.json({ error: 'No valid fields to update' }, { status: 400 })
diff --git a/app/api/my-rooms/route.ts b/app/api/my-rooms/route.ts
index 7d33861..0b72b0c 100644
--- a/app/api/my-rooms/route.ts
+++ b/app/api/my-rooms/route.ts
@@ -14,8 +14,8 @@ export async function GET() {
const isAdmin = !!user.app_metadata?.is_admin
- // Get user's body memberships and active semester in parallel
- const [{ data: memberships }, { data: activeSemester }] = await Promise.all([
+ // Get user's body memberships, active semester, and Senate type preferences in parallel
+ const [{ data: memberships }, { data: activeSemester }, { data: profile }] = await Promise.all([
supabase
.from('board_memberships')
.select('body_id, role')
@@ -25,10 +25,17 @@ export async function GET() {
.select('id')
.eq('is_active', true)
.single(),
+ supabase
+ .from('users')
+ .select('senate_type_preferences')
+ .eq('id', user.id)
+ .single(),
])
+ const senateTypePreferences = profile?.senate_type_preferences ?? {}
+
if (!memberships || memberships.length === 0) {
- return NextResponse.json({ bookings: [], leadershipBodyIds: [] })
+ return NextResponse.json({ bookings: [], leadershipBodyIds: [], senateTypePreferences })
}
const bodyIds = memberships.map(m => m.body_id)
@@ -42,6 +49,7 @@ export async function GET() {
weeklyBookings: [],
tablingBookings: [],
leadershipBodyIds: [...leadershipBodyIds],
+ senateTypePreferences,
})
}
@@ -98,5 +106,6 @@ export async function GET() {
tablingBookings: visible(tablingBookings || []),
// Returned so the client doesn't have to re-query board_memberships itself.
leadershipBodyIds: [...leadershipBodyIds],
+ senateTypePreferences,
})
}
\ No newline at end of file
diff --git a/app/api/spaces/blackouts/[id]/route.ts b/app/api/spaces/blackouts/[id]/route.ts
index c4589a1..8adf091 100644
--- a/app/api/spaces/blackouts/[id]/route.ts
+++ b/app/api/spaces/blackouts/[id]/route.ts
@@ -44,7 +44,7 @@ async function cascadeCancelBookings(spaceId: string | null, startTime: string,
startTime: b.start_time,
endTime: b.end_time,
to: creatorEmail,
- cc: ccEmails,
+ bcc: ccEmails,
})
})).catch(e => console.error('Blackout cascade emails failed:', e))
)
diff --git a/app/api/spaces/blackouts/route.ts b/app/api/spaces/blackouts/route.ts
index 1b97b64..f33edb9 100644
--- a/app/api/spaces/blackouts/route.ts
+++ b/app/api/spaces/blackouts/route.ts
@@ -113,7 +113,7 @@ export async function POST(request: Request) {
startTime: b.start_time,
endTime: b.end_time,
to: creatorEmail,
- cc: ccEmails,
+ bcc: ccEmails,
})
})).catch(e => console.error('Blackout cascade emails failed:', e))
)
diff --git a/app/api/spaces/bookings/[id]/route.ts b/app/api/spaces/bookings/[id]/route.ts
index fe94af8..5f684a5 100644
--- a/app/api/spaces/bookings/[id]/route.ts
+++ b/app/api/spaces/bookings/[id]/route.ts
@@ -194,7 +194,7 @@ export async function DELETE(_request: Request, { params }: { params: Promise<{
startTime: booking.start_time,
endTime: booking.end_time,
to: creatorEmail,
- cc: ccEmails,
+ bcc: ccEmails,
})
}
} catch (e) {
diff --git a/lib/emails/booking-updated.ts b/lib/emails/booking-updated.ts
index 778628f..da1d6d4 100644
--- a/lib/emails/booking-updated.ts
+++ b/lib/emails/booking-updated.ts
@@ -37,7 +37,8 @@ export async function sendBookingUpdatedEmail(params: BookingUpdatedEmailParams)
await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL!,
- to: recipients,
+ to: process.env.RESEND_FROM_EMAIL!,
+ bcc: recipients,
subject: 'Chambers \u2014 Your Booking Has Been Updated',
text: `Your ${sBodyName} booking has been updated by a Chambers administrator.
diff --git a/lib/emails/space-booking-cancelled.ts b/lib/emails/space-booking-cancelled.ts
index f3241be..14e3321 100644
--- a/lib/emails/space-booking-cancelled.ts
+++ b/lib/emails/space-booking-cancelled.ts
@@ -8,7 +8,7 @@ interface SpaceBookingCancelledParams {
startTime: string // ISO timestamptz
endTime: string // ISO timestamptz
to: string
- cc?: string[]
+ bcc?: string[]
}
function formatDateTime(iso: string): string {
@@ -77,7 +77,7 @@ function buildCancelIcs(bookingId: string, title: string, spaceName: string, sta
}
export async function sendSpaceBookingCancelledEmail(params: SpaceBookingCancelledParams) {
- const { bookingId, title, spaceName, startTime, endTime, to, cc } = params
+ const { bookingId, title, spaceName, startTime, endTime, to, bcc } = params
if (!to) return
const sTitle = sanitize(title)
@@ -85,7 +85,7 @@ export async function sendSpaceBookingCancelledEmail(params: SpaceBookingCancell
await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL!,
to,
- ...(cc?.length ? { cc } : {}),
+ ...(bcc?.length ? { bcc } : {}),
subject: `Chambers — SGA Space Booking Cancelled: ${sTitle}`,
text: `Your SGA Space booking has been cancelled.
diff --git a/lib/emails/space-booking-confirmed.ts b/lib/emails/space-booking-confirmed.ts
index ed952b1..debc705 100644
--- a/lib/emails/space-booking-confirmed.ts
+++ b/lib/emails/space-booking-confirmed.ts
@@ -89,7 +89,8 @@ export async function sendSpaceBookingConfirmedEmail(params: SpaceBookingConfirm
await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL!,
- to: recipients,
+ to: process.env.RESEND_FROM_EMAIL!,
+ bcc: recipients,
subject: `Chambers \u2014 SGA Space Booking Confirmed: ${sTitle}`,
text: `Your SGA Space booking has been confirmed.
diff --git a/package-lock.json b/package-lock.json
index c8af67d..ca4e55d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "chambers",
- "version": "1.12.3",
+ "version": "1.12.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chambers",
- "version": "1.12.3",
+ "version": "1.12.4",
"dependencies": {
"@supabase/ssr": "^0.9.0",
"@supabase/supabase-js": "^2.99.1",
diff --git a/package.json b/package.json
index b6ae32d..73f7c3c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "chambers",
- "version": "1.12.3",
+ "version": "1.12.4",
"private": true,
"scripts": {
"dev": "next dev",
diff --git a/supabase/migrations/20260825020000_add_senate_type_preferences.sql b/supabase/migrations/20260825020000_add_senate_type_preferences.sql
new file mode 100644
index 0000000..d55fd1e
--- /dev/null
+++ b/supabase/migrations/20260825020000_add_senate_type_preferences.sql
@@ -0,0 +1,6 @@
+-- Per-user preference for which Senate session types (Full Body, Weekly, Office
+-- Hours) show up in My Rooms. Mirrors email_preferences: a jsonb map of type ->
+-- boolean, where a missing key defaults to true (visible) so existing users see
+-- everything until they opt out of a type.
+alter table public.users
+ add column senate_type_preferences jsonb not null default '{}'::jsonb;