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
1 change: 1 addition & 0 deletions app/(dashboard)/bookings/bookings-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const statusColors: Record<string, string> = {
'Reserved': 'bg-[#0f3d20] text-[#4ade80]',
'Alternate Room': 'bg-[#0e2f4f] text-[#4285f4]',
'Alternate Time': 'bg-[#0e2f4f] text-[#4285f4]',
'Alternate Room and Time': 'bg-[#0e2f4f] text-[#4285f4]',
'Waitlisted': 'bg-[#3d0f0f] text-[#f87171]',
'Unavailable': 'bg-[#3d0f0f] text-[#f87171]',
'Pending Cancellation': 'bg-[#3d2200] text-[#fb923c]',
Expand Down
1 change: 1 addition & 0 deletions app/(dashboard)/bookings/edit-one-time-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const STATUSES = [
'Reserved',
'Alternate Room',
'Alternate Time',
'Alternate Room and Time',
'Waitlisted',
'Unavailable',
'Pending Cancellation',
Expand Down
1 change: 1 addition & 0 deletions app/(dashboard)/bookings/edit-tabling-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const STATUSES = [
'Reserved',
'Alternate Room',
'Alternate Time',
'Alternate Room and Time',
'Waitlisted',
'Unavailable',
'Pending Cancellation',
Expand Down
1 change: 1 addition & 0 deletions app/(dashboard)/bookings/edit-weekly-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const STATUSES = [
'Reserved',
'Alternate Room',
'Alternate Time',
'Alternate Room and Time',
'Waitlisted',
'Unavailable',
'Pending Cancellation',
Expand Down
1 change: 1 addition & 0 deletions app/(dashboard)/bookings/one-time-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const STATUSES = [
'Reserved',
'Alternate Room',
'Alternate Time',
'Alternate Room and Time',
'Waitlisted',
'Unavailable',
'Pending Cancellation',
Expand Down
1 change: 1 addition & 0 deletions app/(dashboard)/bookings/tabling-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const STATUSES = [
'Reserved',
'Alternate Room',
'Alternate Time',
'Alternate Room and Time',
'Waitlisted',
'Unavailable',
'Pending Cancellation',
Expand Down
1 change: 1 addition & 0 deletions app/(dashboard)/bookings/weekly-booking-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const statusCellHex: Record<string, string> = {
'Reserved': '#4ade80',
'Alternate Room': '#4285f4',
'Alternate Time': '#4285f4',
'Alternate Room and Time': '#4285f4',
'Waitlisted': '#f87171',
'Unavailable': '#f87171',
'Pending Cancellation': '#fb923c',
Expand Down
1 change: 1 addition & 0 deletions app/(dashboard)/bookings/weekly-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const STATUSES = [
'Reserved',
'Alternate Room',
'Alternate Time',
'Alternate Room and Time',
'Waitlisted',
'Unavailable',
'Pending Cancellation',
Expand Down
2 changes: 1 addition & 1 deletion app/(dashboard)/dashboard-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ export default function DashboardShell({
<span className="text-[#c8102e] font-bold text-xl tracking-tight">Chambers</span>
</div>
<p className="text-slate-500 text-xs mt-0.5">NU Student Gov. Association</p>
<p className="text-slate-600 text-xs mt-1">v1.14.1</p>
<p className="text-slate-600 text-xs mt-1">v1.14.2</p>
{userName && (
<div className="flex items-start justify-between mt-2">
<p className="text-slate-500 text-xs italic">{getGreeting()},<br />{userName}</p>
Expand Down
1 change: 1 addition & 0 deletions app/(dashboard)/management/archive-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ const statusColors: Record<string, string> = {
'Reserved': 'bg-[#0f3d20] text-[#4ade80]',
'Alternate Room': 'bg-[#0e2f4f] text-[#4285f4]',
'Alternate Time': 'bg-[#0e2f4f] text-[#4285f4]',
'Alternate Room and Time': 'bg-[#0e2f4f] text-[#4285f4]',
'Waitlisted': 'bg-[#3d0f0f] text-[#f87171]',
'Unavailable': 'bg-[#3d0f0f] text-[#f87171]',
'Pending Cancellation': 'bg-[#3d2200] text-[#fb923c]',
Expand Down
1 change: 1 addition & 0 deletions app/(dashboard)/management/audit-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const statusColors: Record<string, string> = {
'Reserved': 'bg-[#0f3d20] text-[#4ade80]',
'Alternate Room': 'bg-[#0e2f4f] text-[#4285f4]',
'Alternate Time': 'bg-[#0e2f4f] text-[#4285f4]',
'Alternate Room and Time': 'bg-[#0e2f4f] text-[#4285f4]',
'Waitlisted': 'bg-[#3d0f0f] text-[#f87171]',
'Unavailable': 'bg-[#3d0f0f] text-[#f87171]',
'Pending Cancellation': 'bg-[#3d2200] text-[#fb923c]',
Expand Down
3 changes: 3 additions & 0 deletions app/(dashboard)/my-rooms/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const statusColors: Record<string, string> = {
'Reserved': 'bg-[#0f3d20] border-[#22c55e]',
'Alternate Room': 'bg-[#0e2f4f] border-[#4285f4]',
'Alternate Time': 'bg-[#0e2f4f] border-[#4285f4]',
'Alternate Room and Time': 'bg-[#0e2f4f] border-[#4285f4]',
'Waitlisted': 'bg-[#3d0f0f] border-[#ef4444]',
'Unavailable': 'bg-[#3d0f0f] border-[#ef4444]',
'Pending Cancellation': 'bg-[#3d2200] border-[#f97316]',
Expand All @@ -118,6 +119,7 @@ export const statusBarColors: Record<string, string> = {
'Reserved': 'bg-[#22c55e]',
'Alternate Room': 'bg-[#4285f4]',
'Alternate Time': 'bg-[#4285f4]',
'Alternate Room and Time': 'bg-[#4285f4]',
'Waitlisted': 'bg-[#ef4444]',
'Unavailable': 'bg-[#ef4444]',
'Pending Cancellation': 'bg-[#f97316]',
Expand All @@ -132,6 +134,7 @@ export const statusTextColors: Record<string, string> = {
'Reserved': 'text-[#4ade80]',
'Alternate Room': 'text-[#4285f4]',
'Alternate Time': 'text-[#4285f4]',
'Alternate Room and Time': 'text-[#4285f4]',
'Waitlisted': 'text-[#f87171]',
'Unavailable': 'text-[#f87171]',
'Pending Cancellation': 'text-[#fb923c]',
Expand Down
11 changes: 5 additions & 6 deletions app/api/cron/slack-reminders/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
} from '@/lib/meeting-reminders'

/**
* Posts tomorrow's committee meetings to each committee's Slack channel
* (issue #95).
* Posts tomorrow's committee meetings -- or that tomorrow's meeting is off -- to
* each committee's Slack channel (issues #95, #104).
*
* Driven by .github/workflows/slack-reminders.yml, following the same pattern as
* /api/cron/warm: a scheduled GitHub Action rather than a Vercel cron, because
Expand Down Expand Up @@ -41,11 +41,11 @@ const adminSupabase = createAdminClient(
* which is what lets the body-level filters below narrow the occurrence rows.
*/
const SELECT = `
occurrence_date, room_name, start_time, end_time, status, purpose, hidden, weekly_booking_id,
occurrence_date, room_name, start_time, end_time, status, hidden, weekly_booking_id,
weekly_room_bookings!inner(
room_name, start_time, end_time, status,
bookings!inner(
purpose, hidden,
hidden,
bodies!inner(name, body_type, slack_channel_id, slack_reminders_enabled)
)
)
Expand Down Expand Up @@ -103,7 +103,6 @@ export async function GET(request: Request) {
start_time: row.start_time,
end_time: row.end_time,
status: row.status,
purpose: row.purpose,
hidden: row.hidden,
weekly_booking_id: row.weekly_booking_id,
series: {
Expand All @@ -112,7 +111,7 @@ export async function GET(request: Request) {
end_time: series.end_time,
status: series.status,
},
booking: { purpose: booking.purpose, hidden: booking.hidden },
booking: { hidden: booking.hidden },
body: { name: body.name, slack_channel_id: body.slack_channel_id },
})
}
Expand Down
12 changes: 12 additions & 0 deletions app/faq/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ export default async function FaqPage() {
</section>
</div>

<div className="space-y-6">
<section className="space-y-2">
<h2 className="text-[#f0f6ff] font-medium text-base">v1.14.2 &mdash; released</h2>
<p className="text-[#93b8d8] text-sm leading-relaxed">
Bookings can now be marked Alternate Room and Time, for when both the room and the time have changed. It appears right under Alternate Time in status lists, in the same blue as the other alternates.
</p>
<p className="text-[#93b8d8] text-sm leading-relaxed">
Committee meeting reminders in Slack have new wording. They name the room and time and call out in bold whichever one is an alternate, point you to your Chair or Director when a meeting is virtual, and say plainly when there is no meeting. A week that is waitlisted, tentative or pending cancellation gets no reminder at all until its status is settled.
</p>
</section>
</div>

<div className="space-y-6">
<section className="space-y-2">
<h2 className="text-[#f0f6ff] font-medium text-base">v1.14.1 &mdash; released</h2>
Expand Down
72 changes: 42 additions & 30 deletions lib/meeting-reminders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,25 @@ import { APP_TIME_ZONE } from '@/lib/app-zone'
export const REMINDER_HOUR = 9

/**
* Statuses that mean the meeting is not happening, so a reminder would be wrong.
* The statuses a reminder is written for, and which lines of it each one calls
* out as alternate (issue #104).
*
* Deliberately short. 'Pending Cancellation' is *not* here: that week may still
* go ahead, and the people in the channel are exactly the ones who need to know
* it is in doubt -- so it is reported, with its status shown.
* An allow-list rather than a block-list: anything not named here -- Waitlisted,
* Tentative, Pending Cancellation, Unavailable, Missed, Repurposed, and any
* status added later -- posts nothing, because none of them says plainly whether
* or where the committee is meeting.
*/
const NOT_HAPPENING = new Set(['Cancelled', 'Repurposed', 'Missed'])

/** Statuses ordinary enough that naming them in the reminder would be noise. */
const UNREMARKABLE = new Set(['Reserved', 'Confirmed'])
const REMINDED_STATUSES = new Set([
'Reserved',
'Alternate Room',
'Alternate Time',
'Alternate Room and Time',
'Virtual',
'Cancelled',
])

const ALTERNATE_ROOM = new Set(['Alternate Room', 'Alternate Room and Time'])
const ALTERNATE_TIME = new Set(['Alternate Time', 'Alternate Room and Time'])

/** 'YYYY-MM-DD' and the hour, in APP_TIME_ZONE, for an instant. */
export function appZoneParts(now: Date = new Date()): { date: string; hour: number } {
Expand Down Expand Up @@ -61,7 +70,6 @@ export interface ReminderCandidate {
start_time: string | null
end_time: string | null
status: string | null
purpose: string | null
hidden: boolean | null
weekly_booking_id: string
series: {
Expand All @@ -71,7 +79,6 @@ export interface ReminderCandidate {
status: string | null
}
booking: {
purpose: string | null
hidden: boolean | null
}
body: {
Expand All @@ -89,17 +96,16 @@ export interface ResolvedMeeting {
roomName: string | null
startTime: string | null
endTime: string | null
status: string | null
purpose: string | null
status: string
}

/**
* Resolves a candidate against its series, or returns null when no reminder
* should be posted for it.
*
* An occurrence field that is null inherits -- from the series for room, times
* and status, and from the booking above it for purpose and visibility. That
* precedence is the same one My Rooms and the update emails apply.
* and status, and from the booking above it for visibility. That precedence is
* the same one My Rooms and the update emails apply.
*/
export function resolveMeeting(c: ReminderCandidate): ResolvedMeeting | null {
if (!c.body.slack_channel_id) return null
Expand All @@ -110,7 +116,7 @@ export function resolveMeeting(c: ReminderCandidate): ResolvedMeeting | null {
if (c.hidden ?? c.booking.hidden) return null

const status = c.status ?? c.series.status
if (status && NOT_HAPPENING.has(status)) return null
if (!status || !REMINDED_STATUSES.has(status)) return null

return {
weeklyBookingId: c.weekly_booking_id,
Expand All @@ -121,7 +127,6 @@ export function resolveMeeting(c: ReminderCandidate): ResolvedMeeting | null {
startTime: c.start_time ?? c.series.start_time,
endTime: c.end_time ?? c.series.end_time,
status,
purpose: c.purpose ?? c.booking.purpose,
}
}

Expand Down Expand Up @@ -149,26 +154,33 @@ function esc(s: string): string {
}

/**
* The reminder text.
* The reminder text, in the wording set out in issue #104.
*
* Cancelled gets a single line, Virtual points members to their Chair or
* Director rather than naming a room, and every other reminded status names the
* room and the time -- with "Alternate" in bold on whichever of the two moved,
* so a member reading quickly sees what is different from the usual week.
*
* States what is known and stays quiet about what is not: a week with no room
* secured says so rather than printing a dash, and an ordinary status is left
* off entirely so that a status line always means something is unusual.
* A room or time that is missing says so rather than printing a blank.
*/
export function formatReminder(m: ResolvedMeeting): string {
const start = formatTime(m.startTime)
const end = formatTime(m.endTime)
const when = start && end ? `${start}–${end}` : start ?? 'time to be confirmed'
const body = `*${esc(m.bodyName)}*`

const lines = [
`:calendar: *${esc(m.bodyName)}* meets tomorrow — ${formatDate(m.date)}, ${when}`,
]
if (m.status === 'Cancelled') return `${body} has no meeting tomorrow.`

lines.push(m.roomName ? `*Room:* ${esc(m.roomName)}` : '*Room:* not yet confirmed')
const opening = `${body} meets tomorrow! Join us on ${formatDate(m.date)}.`

if (m.purpose?.trim()) lines.push(`*Purpose:* ${esc(m.purpose.trim())}`)
if (m.status === 'Virtual') {
return [opening, 'Check with your Chair/Director for virtual meeting information.'].join('\n')
}

const start = formatTime(m.startTime)
const end = formatTime(m.endTime)
const when = start && end ? `${start}–${end}` : start ?? 'to be confirmed'
const room = m.roomName ? esc(m.roomName) : 'not yet confirmed'

if (m.status && !UNREMARKABLE.has(m.status)) lines.push(`*Status:* ${esc(m.status)}`)
const roomLabel = ALTERNATE_ROOM.has(m.status) ? '*Alternate* Room' : 'Room'
const timeLabel = ALTERNATE_TIME.has(m.status) ? '*Alternate* Time' : 'Time'

return lines.join('\n')
return [opening, `${roomLabel}: ${room}`, `${timeLabel}: ${when}`].join('\n')
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "chambers",
"version": "1.14.1",
"version": "1.14.2",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-- Add 'Alternate Room and Time' as a booking status (issue #105).
--
-- A booking could be marked as having an alternate room or an alternate time,
-- but not both, even though CSC hands out both at once. Picking either one
-- misdescribed the booking, and the Slack reminder (issue #104) needs to know
-- which of the two lines to call out.
--
-- The same vocabulary is enforced on all four tables that carry a booking
-- status, so all four constraints are replaced together. The list is otherwise
-- unchanged, and in the order the forms present it. This must be applied before
-- the release that offers the status, or saving a booking with it fails the
-- check.

alter table public.one_time_room_bookings
drop constraint if exists one_time_room_bookings_status_check;
alter table public.one_time_room_bookings
add constraint one_time_room_bookings_status_check
check (status = any (array['Reserved'::text, 'Alternate Room'::text, 'Alternate Time'::text, 'Alternate Room and Time'::text, 'Waitlisted'::text, 'Unavailable'::text, 'Pending Cancellation'::text, 'Cancelled'::text, 'Virtual'::text, 'Missed'::text, 'Repurposed'::text, 'Tentative'::text]));

alter table public.weekly_room_bookings
drop constraint if exists weekly_room_bookings_status_check;
alter table public.weekly_room_bookings
add constraint weekly_room_bookings_status_check
check (status = any (array['Reserved'::text, 'Alternate Room'::text, 'Alternate Time'::text, 'Alternate Room and Time'::text, 'Waitlisted'::text, 'Unavailable'::text, 'Pending Cancellation'::text, 'Cancelled'::text, 'Virtual'::text, 'Missed'::text, 'Repurposed'::text, 'Tentative'::text]));

alter table public.weekly_room_occurrences
drop constraint if exists weekly_room_occurrences_status_check;
alter table public.weekly_room_occurrences
add constraint weekly_room_occurrences_status_check
check (status = any (array['Reserved'::text, 'Alternate Room'::text, 'Alternate Time'::text, 'Alternate Room and Time'::text, 'Waitlisted'::text, 'Unavailable'::text, 'Pending Cancellation'::text, 'Cancelled'::text, 'Virtual'::text, 'Missed'::text, 'Repurposed'::text, 'Tentative'::text]));

alter table public.tabling_sessions
drop constraint if exists tabling_sessions_status_check;
alter table public.tabling_sessions
add constraint tabling_sessions_status_check
check (status = any (array['Reserved'::text, 'Alternate Room'::text, 'Alternate Time'::text, 'Alternate Room and Time'::text, 'Waitlisted'::text, 'Unavailable'::text, 'Pending Cancellation'::text, 'Cancelled'::text, 'Virtual'::text, 'Missed'::text, 'Repurposed'::text, 'Tentative'::text]));
Loading
Loading