Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/api/administrator/bookings/one-time/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { createClient as createAdminClient } from '@supabase/supabase-js'
import { NextResponse } from 'next/server'
import { sendMissedReservationEmail, formatDateLong } from '@/lib/emails/missed-reservation'
import { sendMissedReservationEmail } from '@/lib/emails/missed-reservation'
import { sendBookingUpdatedEmail } from '@/lib/emails/booking-updated'
import { sendBookingCreatedEmail } from '@/lib/emails/booking-created'
import { changed, collectChanges, formatDate, formatTime } from '@/lib/emails/changes'
Expand Down Expand Up @@ -315,7 +315,8 @@ export async function PATCH(request: Request) {

await sendMissedReservationEmail({
bodyName,
date: formatDateLong(firstSession.booking_date),
date: firstSession.booking_date,
roomOrTable: firstSession.room_name,
startTime: firstSession.start_time,
endTime: firstSession.end_time,
contacts,
Expand Down
5 changes: 3 additions & 2 deletions app/api/administrator/bookings/tabling/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { createClient as createAdminClient } from '@supabase/supabase-js'
import { NextResponse } from 'next/server'
import { sendMissedReservationEmail, formatDateLong } from '@/lib/emails/missed-reservation'
import { sendMissedReservationEmail } from '@/lib/emails/missed-reservation'
import { sendBookingUpdatedEmail } from '@/lib/emails/booking-updated'
import { sendBookingCreatedEmail } from '@/lib/emails/booking-created'
import { changed, collectChanges, formatDate, formatTime } from '@/lib/emails/changes'
Expand Down Expand Up @@ -325,7 +325,8 @@ export async function PATCH(request: Request) {

await sendMissedReservationEmail({
bodyName,
date: formatDateLong(sessions[0].session_date),
date: sessions[0].session_date,
roomOrTable: sessions[0].location,
startTime: sessions[0].start_time,
endTime: sessions[0].end_time,
contacts,
Expand Down
54 changes: 44 additions & 10 deletions app/api/administrator/bookings/weekly/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { createClient as createAdminClient } from '@supabase/supabase-js'
import { NextResponse } from 'next/server'
import { sendMissedReservationEmail, formatDateLong } from '@/lib/emails/missed-reservation'
import { sendMissedReservationEmail } from '@/lib/emails/missed-reservation'
import { sendBookingUpdatedEmail } from '@/lib/emails/booking-updated'
import { sendBookingCreatedEmail } from '@/lib/emails/booking-created'
import { changed, collectChanges, formatDate, formatTime } from '@/lib/emails/changes'
Expand Down Expand Up @@ -437,8 +437,25 @@ export async function PATCH(request: Request) {
.eq('booking_id', booking_id)
.eq('status', 'Pending')

const isMissed = status === 'Missed' || occurrences.some((o: { status: string | null }) => o.status === 'Missed')
if (isMissed) {
// Which weeks this save actually marked Missed.
//
// Two things were wrong with asking "is anything Missed" (issue #99). It
// described the alert using the series' start date and time no matter which
// week had been missed -- the same defect as #91, in the one caller that
// rewrite did not reach. And because a missed week stays Missed, the condition
// stayed true forever: every later edit to the series sent Operational Affairs
// another alert about a week they had been told about weeks earlier.
//
// movedOccurrences is what this save changed, so a week only alerts on the
// edit that missed it.
const newlyMissedWeeks = movedOccurrences.filter(o => o.status === 'Missed')

// A series-level Missed applies to every week at once, so there is no single
// week to name and the series is the story. Compared against the stored value
// so resubmitting an already-missed series does not re-alert either.
const seriesNewlyMissed = status === 'Missed' && prevWeekly?.status !== 'Missed'

if (seriesNewlyMissed || newlyMissedWeeks.length) {
waitUntil(
(async () => {
try {
Expand All @@ -447,13 +464,30 @@ export async function PATCH(request: Request) {
})
const contacts = leaders.map(l => l.fullName).filter(Boolean)

await sendMissedReservationEmail({
bodyName,
date: formatDateLong(start_date),
startTime: start_time,
endTime: end_time,
contacts,
})
// One alert per missed reservation rather than one per save. Each
// missed room is its own incident for Operational Affairs to chase,
// and a single email naming one of several would hide the rest.
const missed = seriesNewlyMissed
? [{ date: start_date, startTime: start_time, endTime: end_time, roomOrTable: room_name }]
: newlyMissedWeeks.map(o => ({
date: o.occurrence_date,
// Null on an occurrence means inherit, so these are the values
// that actually applied to the week that was missed.
startTime: o.start_time ?? start_time,
endTime: o.end_time ?? end_time,
roomOrTable: o.room_name ?? room_name,
}))

for (const m of missed) {
await sendMissedReservationEmail({
bodyName,
date: m.date,
roomOrTable: m.roomOrTable,
startTime: m.startTime,
endTime: m.endTime,
contacts,
})
}
} catch (e) {
console.error('Resend email failed:', e)
}
Expand Down
49 changes: 33 additions & 16 deletions lib/emails/missed-reservation.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,55 @@
import { resend } from '@/lib/resend'
import { sanitize, buildEmailHtml } from './utils'
import { formatDate, formatTime } from './changes'

/**
* The alert Operational Affairs gets when a reservation is marked Missed.
*
* This template did not move when the booking emails were rewritten in #79, and
* drifted (issue #99). It rendered its times straight out of the payload, so an
* alert read "Time of Reservation: 18:30:00 to 20:00:00" where every other
* Chambers email said "6:30 PM to 8:00 PM"; it carried a second copy of the date
* formatter under a different name; and it took its date already formatted,
* which is why the raw times were easy to miss -- the one value the caller
* prepared looked right, and the two it passed through did not.
*
* It now takes raw values and formats them here, like every other template.
*/
interface MissedReservationEmailParams {
bodyName: string
/** 'YYYY-MM-DD'. Formatted here, not by the caller. */
date: string
/** 'HH:MM' or 'HH:MM:SS' -- both are formatted the same way. */
startTime: string
endTime: string
contacts: string[]
}

export function formatDateLong(dateStr: string): string {
return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
})
/** The room or table that went unused. Omitted when the caller has none. */
roomOrTable?: string | null
}

export async function sendMissedReservationEmail(params: MissedReservationEmailParams) {
const { bodyName, date, startTime, endTime, contacts } = params
const { bodyName, date, startTime, endTime, contacts, roomOrTable = null } = params

const sBodyName = sanitize(bodyName)
const sContacts = contacts.map(sanitize).join(', ')
// An em dash, so a list of names cannot read as one hyphenated name, and a
// placeholder rather than an empty line when a body has no leadership on file.
const sContacts = contacts.length ? contacts.map(sanitize).join(', ') : 'None on file'
const sRoomOrTable = roomOrTable ? sanitize(roomOrTable) : null

const roomText = sRoomOrTable ? `Room/Table: ${sRoomOrTable}\n` : ''
const roomHtml = sRoomOrTable ? `<strong>Room/Table:</strong> ${sRoomOrTable}<br>` : ''

await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL!,
// Straight to Operational Affairs rather than bcc'd to a membership: this is
// an internal alert about a body, not a notification to it.
to: process.env.OPS_EMAIL!,
subject: 'Chambers Alert - Reservation Missed',
subject: 'Chambers Reservation Missed',
text: `This is an automatic alert that a SGA reservation was marked as missed by a Chambers administrator.

Responsible Body: ${sBodyName}
Date of Reservation: ${date}
Time of Reservation: ${startTime} to ${endTime}
${roomText}Date of Reservation: ${formatDate(date)}
Time of Reservation: ${formatTime(startTime)} to ${formatTime(endTime)}

Contacts: ${sContacts}

Expand All @@ -41,8 +58,8 @@ For further information, please reach out to the Comptroller.`,
<p style="margin:0 0 16px;">This is an automatic alert that a SGA reservation was marked as missed by a Chambers administrator.</p>
<p style="margin:0;line-height:1.8;">
<strong>Responsible Body:</strong> ${sBodyName}<br>
<strong>Date of Reservation:</strong> ${date}<br>
<strong>Time of Reservation:</strong> ${startTime} to ${endTime}<br>
${roomHtml}<strong>Date of Reservation:</strong> ${formatDate(date)}<br>
<strong>Time of Reservation:</strong> ${formatTime(startTime)} to ${formatTime(endTime)}<br>
<strong>Contacts:</strong> ${sContacts}
</p>
`, 'For further information, please reach out to the Comptroller.'),
Expand Down
Loading