diff --git a/app/(dashboard)/bookings/bookings-tab.tsx b/app/(dashboard)/bookings/bookings-tab.tsx index 70ba92d..dcb30fc 100644 --- a/app/(dashboard)/bookings/bookings-tab.tsx +++ b/app/(dashboard)/bookings/bookings-tab.tsx @@ -128,6 +128,7 @@ const statusColors: Record = { '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]', diff --git a/app/(dashboard)/bookings/edit-one-time-form.tsx b/app/(dashboard)/bookings/edit-one-time-form.tsx index 62ccf0f..431083f 100644 --- a/app/(dashboard)/bookings/edit-one-time-form.tsx +++ b/app/(dashboard)/bookings/edit-one-time-form.tsx @@ -10,6 +10,7 @@ const STATUSES = [ 'Reserved', 'Alternate Room', 'Alternate Time', + 'Alternate Room and Time', 'Waitlisted', 'Unavailable', 'Pending Cancellation', diff --git a/app/(dashboard)/bookings/edit-tabling-form.tsx b/app/(dashboard)/bookings/edit-tabling-form.tsx index 5383c25..262d150 100644 --- a/app/(dashboard)/bookings/edit-tabling-form.tsx +++ b/app/(dashboard)/bookings/edit-tabling-form.tsx @@ -10,6 +10,7 @@ const STATUSES = [ 'Reserved', 'Alternate Room', 'Alternate Time', + 'Alternate Room and Time', 'Waitlisted', 'Unavailable', 'Pending Cancellation', diff --git a/app/(dashboard)/bookings/edit-weekly-form.tsx b/app/(dashboard)/bookings/edit-weekly-form.tsx index 2679efa..3864910 100644 --- a/app/(dashboard)/bookings/edit-weekly-form.tsx +++ b/app/(dashboard)/bookings/edit-weekly-form.tsx @@ -10,6 +10,7 @@ const STATUSES = [ 'Reserved', 'Alternate Room', 'Alternate Time', + 'Alternate Room and Time', 'Waitlisted', 'Unavailable', 'Pending Cancellation', diff --git a/app/(dashboard)/bookings/one-time-form.tsx b/app/(dashboard)/bookings/one-time-form.tsx index 378e929..36528a9 100644 --- a/app/(dashboard)/bookings/one-time-form.tsx +++ b/app/(dashboard)/bookings/one-time-form.tsx @@ -12,6 +12,7 @@ const STATUSES = [ 'Reserved', 'Alternate Room', 'Alternate Time', + 'Alternate Room and Time', 'Waitlisted', 'Unavailable', 'Pending Cancellation', diff --git a/app/(dashboard)/bookings/tabling-form.tsx b/app/(dashboard)/bookings/tabling-form.tsx index 6a3a54c..508db02 100644 --- a/app/(dashboard)/bookings/tabling-form.tsx +++ b/app/(dashboard)/bookings/tabling-form.tsx @@ -12,6 +12,7 @@ const STATUSES = [ 'Reserved', 'Alternate Room', 'Alternate Time', + 'Alternate Room and Time', 'Waitlisted', 'Unavailable', 'Pending Cancellation', diff --git a/app/(dashboard)/bookings/weekly-booking-grid.tsx b/app/(dashboard)/bookings/weekly-booking-grid.tsx index f689a18..b4ea02b 100644 --- a/app/(dashboard)/bookings/weekly-booking-grid.tsx +++ b/app/(dashboard)/bookings/weekly-booking-grid.tsx @@ -69,6 +69,7 @@ const statusCellHex: Record = { 'Reserved': '#4ade80', 'Alternate Room': '#4285f4', 'Alternate Time': '#4285f4', + 'Alternate Room and Time': '#4285f4', 'Waitlisted': '#f87171', 'Unavailable': '#f87171', 'Pending Cancellation': '#fb923c', diff --git a/app/(dashboard)/bookings/weekly-form.tsx b/app/(dashboard)/bookings/weekly-form.tsx index a0e7768..51e2455 100644 --- a/app/(dashboard)/bookings/weekly-form.tsx +++ b/app/(dashboard)/bookings/weekly-form.tsx @@ -12,6 +12,7 @@ const STATUSES = [ 'Reserved', 'Alternate Room', 'Alternate Time', + 'Alternate Room and Time', 'Waitlisted', 'Unavailable', 'Pending Cancellation', diff --git a/app/(dashboard)/dashboard-shell.tsx b/app/(dashboard)/dashboard-shell.tsx index 75a99f8..eb21646 100644 --- a/app/(dashboard)/dashboard-shell.tsx +++ b/app/(dashboard)/dashboard-shell.tsx @@ -435,7 +435,7 @@ export default function DashboardShell({ Chambers

NU Student Gov. Association

-

v1.14.1

+

v1.14.2

{userName && (

{getGreeting()},
{userName}

diff --git a/app/(dashboard)/management/archive-tab.tsx b/app/(dashboard)/management/archive-tab.tsx index aeebe28..283b93a 100644 --- a/app/(dashboard)/management/archive-tab.tsx +++ b/app/(dashboard)/management/archive-tab.tsx @@ -126,6 +126,7 @@ const statusColors: Record = { '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]', diff --git a/app/(dashboard)/management/audit-tab.tsx b/app/(dashboard)/management/audit-tab.tsx index 1e943a5..1852451 100644 --- a/app/(dashboard)/management/audit-tab.tsx +++ b/app/(dashboard)/management/audit-tab.tsx @@ -32,6 +32,7 @@ const statusColors: Record = { '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]', diff --git a/app/(dashboard)/my-rooms/shared.ts b/app/(dashboard)/my-rooms/shared.ts index fda810c..ff3fbda 100644 --- a/app/(dashboard)/my-rooms/shared.ts +++ b/app/(dashboard)/my-rooms/shared.ts @@ -104,6 +104,7 @@ export const statusColors: Record = { '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]', @@ -118,6 +119,7 @@ export const statusBarColors: Record = { '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]', @@ -132,6 +134,7 @@ export const statusTextColors: Record = { '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]', diff --git a/app/api/cron/slack-reminders/route.ts b/app/api/cron/slack-reminders/route.ts index 27db2ce..89e916c 100644 --- a/app/api/cron/slack-reminders/route.ts +++ b/app/api/cron/slack-reminders/route.ts @@ -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 @@ -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) ) ) @@ -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: { @@ -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 }, }) } diff --git a/app/faq/page.tsx b/app/faq/page.tsx index 2daec35..25f4715 100644 --- a/app/faq/page.tsx +++ b/app/faq/page.tsx @@ -39,6 +39,18 @@ export default async function FaqPage() {
+
+
+

v1.14.2 — released

+

+ 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. +

+

+ 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. +

+
+
+

v1.14.1 — released

diff --git a/lib/meeting-reminders.ts b/lib/meeting-reminders.ts index 1a58c8e..dbcf421 100644 --- a/lib/meeting-reminders.ts +++ b/lib/meeting-reminders.ts @@ -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 } { @@ -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: { @@ -71,7 +79,6 @@ export interface ReminderCandidate { status: string | null } booking: { - purpose: string | null hidden: boolean | null } body: { @@ -89,8 +96,7 @@ export interface ResolvedMeeting { roomName: string | null startTime: string | null endTime: string | null - status: string | null - purpose: string | null + status: string } /** @@ -98,8 +104,8 @@ export interface ResolvedMeeting { * 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 @@ -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, @@ -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, } } @@ -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') } diff --git a/package-lock.json b/package-lock.json index 36b0822..611b31f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chambers", - "version": "1.14.1", + "version": "1.14.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chambers", - "version": "1.14.1", + "version": "1.14.2", "dependencies": { "@supabase/ssr": "^0.9.0", "@supabase/supabase-js": "^2.99.1", diff --git a/package.json b/package.json index 54158d9..70ef0e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chambers", - "version": "1.14.1", + "version": "1.14.2", "private": true, "scripts": { "dev": "next dev", diff --git a/supabase/migrations/20260914000000_alternate_room_and_time_status.sql b/supabase/migrations/20260914000000_alternate_room_and_time_status.sql new file mode 100644 index 0000000..14cb401 --- /dev/null +++ b/supabase/migrations/20260914000000_alternate_room_and_time_status.sql @@ -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])); diff --git a/supabase/migrations/rollback/20260914_alternate_room_and_time_status_rollback.sql b/supabase/migrations/rollback/20260914_alternate_room_and_time_status_rollback.sql new file mode 100644 index 0000000..d971605 --- /dev/null +++ b/supabase/migrations/rollback/20260914_alternate_room_and_time_status_rollback.sql @@ -0,0 +1,36 @@ +-- Rollback for 20260914000000_alternate_room_and_time_status.sql. +-- +-- Rows already using the new status have to go somewhere before the old +-- constraints go back, or adding them fails on those rows. They become +-- 'Alternate Room': the room is the part of the change a member acts on when +-- they walk to it, so it is the less misleading half to keep. That the time +-- also moved is lost. + +update public.one_time_room_bookings set status = 'Alternate Room' where status = 'Alternate Room and Time'; +update public.weekly_room_bookings set status = 'Alternate Room' where status = 'Alternate Room and Time'; +update public.weekly_room_occurrences set status = 'Alternate Room' where status = 'Alternate Room and Time'; +update public.tabling_sessions set status = 'Alternate Room' where status = 'Alternate Room and Time'; + +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, '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, '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, '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, 'Waitlisted'::text, 'Unavailable'::text, 'Pending Cancellation'::text, 'Cancelled'::text, 'Virtual'::text, 'Missed'::text, 'Repurposed'::text, 'Tentative'::text]));