From 852341820db7a26784e239e58417844beb4f9728 Mon Sep 17 00:00:00 2001 From: pataniaeli Date: Wed, 9 Sep 2026 18:10:33 -0400 Subject: [PATCH 1/2] feat: allow non-creative edits to Spaces bookings past advance notice (#94) Advance notice exists so that time is *claimed* a set number of hours before it is used, so the thing to test is the time an edit newly claims -- not whether the start moved, which is what the PATCH route tested. Shortening a booking, pushing its start later, renaming it or cancelling it release time or leave it alone, and none of them needs notice. lib/spaces-advance-notice.ts works out the earliest instant an edit newly claims and refuses only that. Extending the end of a booking whose end is still outside the window is therefore allowed, as the issue asks, because the block being claimed starts at the old end. Relocating a booking wholesale to a date past the cutoff is allowed for the same reason, and is not treated as an extension. Creation claims its whole interval, so the POST route reduces to the test it already ran and now shares the rule. `now` defaults to bostonWallClockNow() and not to Date.now(). A space booking's start_time holds Boston wall-clock digits with a Z on the end, so measuring it against a real instant makes every booking look an offset earlier than it is -- issue #87, fixed in 1e3d894, which this would otherwise have reintroduced. The default belongs in the rule rather than at each call site because the browser is a caller too, and its clock is in whatever zone the viewer happens to be sitting in. The real blocker was in the calendar: the notice-zone guard in the click handler returned before the "is this a booking I own?" branch, so a booking sitting inside the window could not be opened at all, let alone shortened or cancelled. Your own booking is now reached first, in both the click and the hover handler, since opening a booking you already hold claims nothing. The edit modal runs the same rule as the form changes, so a change that would be refused says so before it is submitted rather than after a round trip. Co-Authored-By: Claude Opus 5 --- app/(dashboard)/sga-spaces/page.tsx | 1 + .../sga-spaces/space-booking-modal.tsx | 26 +++++- app/(dashboard)/sga-spaces/space-calendar.tsx | 44 ++++----- app/api/spaces/bookings/[id]/route.ts | 24 +++-- app/api/spaces/bookings/route.ts | 19 ++-- lib/spaces-advance-notice.ts | 89 +++++++++++++++++++ 6 files changed, 156 insertions(+), 47 deletions(-) create mode 100644 lib/spaces-advance-notice.ts diff --git a/app/(dashboard)/sga-spaces/page.tsx b/app/(dashboard)/sga-spaces/page.tsx index 63aed00..216eb03 100644 --- a/app/(dashboard)/sga-spaces/page.tsx +++ b/app/(dashboard)/sga-spaces/page.tsx @@ -458,6 +458,7 @@ export default function SGASpacesPage() { editBookingId={editBooking.id} initialTitle={editBooking.title} initialAttendees={editBooking.attendees} + minHoursAdvance={minHoursAdvance} onClose={() => setEditBooking(null)} onSuccess={() => { setEditBooking(null) diff --git a/app/(dashboard)/sga-spaces/space-booking-modal.tsx b/app/(dashboard)/sga-spaces/space-booking-modal.tsx index a9e48b6..59cf1a7 100644 --- a/app/(dashboard)/sga-spaces/space-booking-modal.tsx +++ b/app/(dashboard)/sga-spaces/space-booking-modal.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useRef, useCallback } from 'react' import TimePicker from '../bookings/time-picker' import DateField from '@/app/_components/date-field' +import { advanceNoticeError } from '@/lib/spaces-advance-notice' interface User { id: string @@ -29,6 +30,8 @@ interface SpaceBookingModalProps { initialAttendees?: User[] onCancelBooking?: () => Promise spaces?: Space[] + /** Hours of notice required before newly claimed time. 0 disables the rule. */ + minHoursAdvance?: number } function isoToDateAndTime(iso: string): { date: string; time: string } { @@ -61,6 +64,7 @@ export default function SpaceBookingModal({ initialAttendees = [], onCancelBooking, spaces, + minHoursAdvance = 0, }: SpaceBookingModalProps) { const isEditing = !!editBookingId @@ -132,6 +136,18 @@ export default function SpaceBookingModal({ } } + // The same rule the server applies, run as the form changes so an edit that + // would be refused says so before it is submitted (issue #94). Only for edits: + // a new booking cannot be drawn inside the notice window in the first place, + // and warning about the slot you have not finished picking would be noise. + const noticeWarning = isEditing && date + ? advanceNoticeError( + { start: dateAndTimeToIso(date, startTime), end: endTimeToIso(date, endTime) }, + { start: initialStart, end: initialEnd }, + minHoursAdvance + ) + : null + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError(null) @@ -303,6 +319,14 @@ export default function SpaceBookingModal({ )} + {/* Advance notice — a warning, not an error: the booking as it stands is + fine, it is the pending change that would be refused. */} + {noticeWarning && !error && ( +
+ {noticeWarning} +
+ )} + {/* Error */} {error && (
@@ -321,7 +345,7 @@ export default function SpaceBookingModal({

NU Student Gov. Association

-

v1.14.0

+

v1.14.1

{userName && (

{getGreeting()},
{userName}

diff --git a/app/faq/page.tsx b/app/faq/page.tsx index 1611c8a..2daec35 100644 --- a/app/faq/page.tsx +++ b/app/faq/page.tsx @@ -39,6 +39,21 @@ export default async function FaqPage() {
+
+
+

v1.14.1 — released

+

+ Update emails about a weekly booking now describe the week that actually changed. They had been pointing at whichever week carried the oldest override, which was usually not the week anyone had touched — so the email named a date months off and listed no changes at all. If one save moves several weeks, the email now covers each of them rather than only the first. +

+

+ Senate session types you deselect in Settings now stop the emails and the alerts too, not just the rows in My Rooms. If you follow Full Body but not Office Hours, you will still hear about a change that moved both. +

+

+ An SGA Space booking inside the advance notice window can be edited again. You can shorten it, start it later, rename it or cancel it outright at any point — only adding time to a booking still needs notice, and extending one that ends outside the window is fine. Previously such a booking could not be opened at all. +

+
+
+

v1.14.0 — released

diff --git a/package-lock.json b/package-lock.json index 7cc8c12..36b0822 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chambers", - "version": "1.14.0", + "version": "1.14.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chambers", - "version": "1.14.0", + "version": "1.14.1", "dependencies": { "@supabase/ssr": "^0.9.0", "@supabase/supabase-js": "^2.99.1", diff --git a/package.json b/package.json index 0defb8c..54158d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chambers", - "version": "1.14.0", + "version": "1.14.1", "private": true, "scripts": { "dev": "next dev",