Skip to content
Open
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: 5 additions & 0 deletions app/(dashboard)/bookings/bookings-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ interface OneTimeBooking extends BookingBase {
booking_date: string
start_time: string
end_time: string
meeting_time: string | null
status: string
reservation_code: string | null
}[] | null
Expand All @@ -66,6 +67,7 @@ interface WeeklyBooking extends BookingBase {
end_date: string
start_time: string
end_time: string
meeting_time: string | null
status: string
reservation_code: string | null
weekly_room_occurrences: {
Expand All @@ -74,6 +76,8 @@ interface WeeklyBooking extends BookingBase {
room_name: string | null
start_time: string | null
end_time: string | null
/** Overrides the series meeting time for this date; null inherits (issue #126). */
meeting_time: string | null
status: string | null
reservation_code: string | null
senate_type: string | null
Expand All @@ -97,6 +101,7 @@ interface TablingBooking extends BookingBase {
session_date: string
start_time: string
end_time: string
meeting_time: string | null
status: string
reservation_code: string | null
}[]
Expand Down
18 changes: 18 additions & 0 deletions app/(dashboard)/bookings/edit-one-time-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ interface OneTimeSession {
booking_date: string
start_time: string
end_time: string
/** Blank means the session meets when its reservation starts (issue #126). */
meeting_time: string
status: string
reservation_code: string
}
Expand All @@ -47,6 +49,7 @@ const emptySession = (): OneTimeSession => ({
booking_date: '',
start_time: '',
end_time: '',
meeting_time: '',
status: 'Reserved',
reservation_code: '',
})
Expand All @@ -65,6 +68,7 @@ interface EditOneTimeFormProps {
booking_date: string
start_time: string
end_time: string
meeting_time: string | null
status: string
reservation_code: string | null
}[] | null
Expand Down Expand Up @@ -96,6 +100,7 @@ export default function EditOneTimeForm({ booking, bodies, onClose, onSuccess }:
booking_date: d.booking_date ?? '',
start_time: d.start_time.slice(0, 5) ?? '',
end_time: d.end_time.slice(0, 5) ?? '',
meeting_time: d.meeting_time?.slice(0, 5) ?? '',
status: d.status ?? 'Reserved',
reservation_code: d.reservation_code ?? '',
})) ?? [emptySession()]
Expand Down Expand Up @@ -221,6 +226,19 @@ export default function EditOneTimeForm({ booking, bodies, onClose, onSuccess }:
</div>
</div>

{/* When the meeting itself starts, as opposed to when the room is
held. Tracks the start time until touched; the server stores
nothing when the two agree (issue #126). */}
<div>
<label className={labelCls}>Meeting Time</label>
<div className="sm:w-1/2 sm:pr-1.5">
<TimePicker
value={s.meeting_time || s.start_time}
onChange={v => updateSession(i, 'meeting_time', v)}
/>
</div>
</div>

<div>
<label className={labelCls}>Status</label>
<select
Expand Down
23 changes: 22 additions & 1 deletion app/(dashboard)/bookings/edit-tabling-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ interface Session {
session_date: string
start_time: string
end_time: string
/** Blank means the session meets when its reservation starts (issue #126). */
meeting_time: string | null
status: string
reservation_code: string | null
isNew?: boolean
Expand Down Expand Up @@ -65,6 +67,7 @@ const emptySession = (): Session => ({
session_date: '',
start_time: '09:00',
end_time: '10:00',
meeting_time: '',
status: 'Reserved',
reservation_code: null,
isNew: true,
Expand All @@ -86,7 +89,12 @@ export default function EditTablingForm({ booking, bodies, onClose, onSuccess }:
})

const [sessions, setSessions] = useState<Session[]>(
t?.tabling_sessions.map(s => ({ ...s, start_time: s.start_time.slice(0, 5), end_time: s.end_time.slice(0, 5) })) || []
t?.tabling_sessions.map(s => ({
...s,
start_time: s.start_time.slice(0, 5),
end_time: s.end_time.slice(0, 5),
meeting_time: s.meeting_time?.slice(0, 5) ?? '',
})) || []
)

const [saving, setSaving] = useState(false)
Expand Down Expand Up @@ -230,6 +238,19 @@ export default function EditTablingForm({ booking, bodies, onClose, onSuccess }:
</div>
</div>

{/* When the meeting itself starts, as opposed to when the room is
held. Tracks the start time until touched; the server stores
nothing when the two agree (issue #126). */}
<div>
<label className={labelCls}>Meeting Time</label>
<div className="sm:w-1/2 sm:pr-1.5">
<TimePicker
value={s.meeting_time || s.start_time}
onChange={v => updateSession(i, 'meeting_time', v)}
/>
</div>
</div>

<div>
<label className={labelCls}>Status</label>
<select
Expand Down
51 changes: 50 additions & 1 deletion app/(dashboard)/bookings/edit-weekly-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useRef, useState } from 'react'
import TimePicker from './time-picker'
import { resolveMeetingTime } from '@/lib/meeting-time'
import DateField from '@/app/_components/date-field'
import BookingScopeSelector, { type BookingScopeValue } from '@/app/_components/booking-scope-selector'
import { DIVISIONS, type Division, type BookingScope } from '@/lib/booking-scope'
Expand Down Expand Up @@ -33,6 +34,8 @@ interface Occurrence {
room_name: string | null
start_time: string | null
end_time: string | null
/** Overrides the series meeting time for this date; null inherits (issue #126). */
meeting_time: string | null
status: string | null
reservation_code: string | null
senate_type: string | null
Expand Down Expand Up @@ -61,6 +64,7 @@ interface EditWeeklyFormProps {
end_date: string
start_time: string
end_time: string
meeting_time: string | null
status: string
reservation_code: string | null
weekly_room_occurrences: Occurrence[]
Expand Down Expand Up @@ -118,6 +122,9 @@ export default function EditWeeklyForm({ booking, bodies, initialExpandedOcc, on
end_date: w?.end_date ?? '',
start_time: w?.start_time.slice(0, 5) ?? '',
end_time: w?.end_time.slice(0, 5) ?? '',
// Blank means "follows the start time"; the picker below fills it in from
// start_time for display, and the server collapses it back (issue #126).
meeting_time: w?.meeting_time?.slice(0, 5) ?? '',
reservation_code: w?.reservation_code ?? '',
status: w?.status ?? 'Reserved',
})
Expand Down Expand Up @@ -159,6 +166,7 @@ export default function EditWeeklyForm({ booking, bodies, initialExpandedOcc, on
room_name: null,
start_time: null,
end_time: null,
meeting_time: null,
status: null,
reservation_code: null,
senate_type: null,
Expand Down Expand Up @@ -249,6 +257,20 @@ export default function EditWeeklyForm({ booking, bodies, initialExpandedOcc, on
</div>
</div>

{/* See weekly-form.tsx for why this tracks the start time rather than sitting blank (issue #126). */}
<div>
<label className={labelCls}>Meeting Time</label>
<div className="sm:w-1/2 sm:pr-1.5">
<TimePicker
value={form.meeting_time || form.start_time}
onChange={v => setForm({ ...form, meeting_time: v })}
/>
</div>
<p className="text-xs text-[#6a96bb] mt-1">
When the meeting itself starts. Leave it on the start time unless the room is held early for setup.
</p>
</div>

<div>
<label className={labelCls}>Reservation Code</label>
<input type="text" placeholder="Optional" value={form.reservation_code} onChange={e => setForm({ ...form, reservation_code: e.target.value })} className={inputCls} />
Expand All @@ -267,7 +289,8 @@ export default function EditWeeklyForm({ booking, bodies, initialExpandedOcc, on
{getWeeklyDates(form.start_date, form.end_date).map(date => {
const occ = occurrences.find(o => o.occurrence_date === date)
// `hidden != null` because false is an override, not an absence.
const hasOverride = occ && (occ.room_name || occ.start_time || occ.end_time || occ.status
const hasOverride = occ && (occ.room_name || occ.start_time || occ.end_time
|| occ.meeting_time || occ.status
|| occ.reservation_code || occ.purpose || occ.hidden != null)
const isExpanded = expandedOcc === date

Expand Down Expand Up @@ -320,6 +343,32 @@ export default function EditWeeklyForm({ booking, bodies, initialExpandedOcc, on
</div>
</div>

{/*
Shows what this week currently resolves to -- its own meeting
time, else the series', else whichever start time applies to
it -- and writes an override the moment it is touched, exactly
as the room and time overrides above do.

Not run through meetingTimeForStorage on save: null here means
"inherit the series", not "meet at the start time", so a week
that genuinely meets at its own start time has to say so
explicitly (issue #126).
*/}
<div>
<label className={labelCls}>Meeting Time Override</label>
<div className="sm:w-1/2 sm:pr-1.5">
<TimePicker
value={resolveMeetingTime(
occ.meeting_time?.slice(0, 5),
form.meeting_time,
occ.start_time?.slice(0, 5),
form.start_time
)}
onChange={v => updateOccurrence(occ.id, 'meeting_time', v)}
/>
</div>
</div>

<div>
<label className={labelCls}>Status Override</label>
<select
Expand Down
16 changes: 16 additions & 0 deletions app/(dashboard)/bookings/one-time-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ interface OneTimeSession {
booking_date: string
start_time: string
end_time: string
/** Blank means the session meets when its reservation starts (issue #126). */
meeting_time: string
status: string
reservation_code: string
}
Expand All @@ -73,6 +75,7 @@ const emptySession = (): OneTimeSession => ({
booking_date: '',
start_time: '',
end_time: '',
meeting_time: '',
status: 'Reserved',
reservation_code: '',
})
Expand Down Expand Up @@ -259,6 +262,19 @@ export default function OneTimeForm({ bodies, semesters, onClose, onSuccess }: O
</div>
</div>

{/* When the meeting itself starts, as opposed to when the room is
held. Tracks the start time until touched; the server stores
nothing when the two agree (issue #126). */}
<div>
<label className={labelCls}>Meeting Time</label>
<div className="sm:w-1/2 sm:pr-1.5">
<TimePicker
value={s.meeting_time || s.start_time}
onChange={v => updateSession(i, 'meeting_time', v)}
/>
</div>
</div>

<div>
<label className={labelCls}>Status</label>
<select
Expand Down
16 changes: 16 additions & 0 deletions app/(dashboard)/bookings/tabling-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ interface Session {
session_date: string
start_time: string
end_time: string
/** Blank means the session meets when its reservation starts (issue #126). */
meeting_time: string
status: string
}

Expand Down Expand Up @@ -82,6 +84,7 @@ const emptySession = (): Session => ({
session_date: '',
start_time: '09:00',
end_time: '10:00',
meeting_time: '',
status: 'Reserved',
})

Expand Down Expand Up @@ -276,6 +279,19 @@ export default function TablingForm({ bodies, semesters, onClose, onSuccess }: T
</div>
</div>

{/* When the meeting itself starts, as opposed to when the room is
held. Tracks the start time until touched; the server stores
nothing when the two agree (issue #126). */}
<div>
<label className={labelCls}>Meeting Time</label>
<div className="sm:w-1/2 sm:pr-1.5">
<TimePicker
value={s.meeting_time || s.start_time}
onChange={v => updateSession(i, 'meeting_time', v)}
/>
</div>
</div>

<div>
<label className={labelCls}>Status</label>
<select
Expand Down
3 changes: 3 additions & 0 deletions app/(dashboard)/bookings/weekly-booking-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ interface WeeklyOccurrence {
room_name: string | null
start_time: string | null
end_time: string | null
/** Overrides the series meeting time for this date; null inherits (issue #126). */
meeting_time: string | null
status: string | null
reservation_code: string | null
senate_type: string | null
Expand Down Expand Up @@ -38,6 +40,7 @@ interface WeeklyBooking {
end_date: string
start_time: string
end_time: string
meeting_time: string | null
status: string
reservation_code: string | null
weekly_room_occurrences: WeeklyOccurrence[]
Expand Down
24 changes: 24 additions & 0 deletions app/(dashboard)/bookings/weekly-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export default function WeeklyForm({ bodies, semesters, onClose, onSuccess }: We
end_date: '',
start_time: '',
end_time: '',
meeting_time: '',
reservation_code: '',
status: 'Reserved',
})
Expand Down Expand Up @@ -222,6 +223,29 @@ export default function WeeklyForm({ bodies, semesters, onClose, onSuccess }: We
</div>
</div>

{/*
Meeting Time: when the body actually meets, as opposed to when the room
is held (issue #126). Tracks the start time until it is touched -- an
empty picker would invite retyping the start time by hand, and the server
collapses a meeting time equal to the start back to "inherit" anyway, so
an untouched field stores nothing and keeps following the reservation.

Its own row rather than a third column: three TimePickers abreast is what
issue #24 had to unpick on mobile.
*/}
<div>
<label className={labelCls}>Meeting Time</label>
<div className="sm:w-1/2 sm:pr-1.5">
<TimePicker
value={form.meeting_time || form.start_time}
onChange={v => setForm({ ...form, meeting_time: v })}
/>
</div>
<p className="text-xs text-[#6a96bb] mt-1">
When the meeting itself starts. Leave it on the start time unless the room is held early for setup.
</p>
</div>

<div>
<label className={labelCls}>Reservation Code</label>
<input
Expand Down
14 changes: 13 additions & 1 deletion app/(dashboard)/my-rooms/booking-detail-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,20 @@ export default function BookingDetailModal({ booking, isLeadership, onClose, onC
<span className="text-[#6a96bb] w-24 flex-shrink-0">Date</span>
<span className="text-[#f0f6ff]">{formatDate(booking.date)}</span>
</div>
{/*
Both rows, always, even when they carry the same time. This is the
detail view -- the one place someone comes to find out exactly what
was booked -- so it is worth a line to say that the meeting starts at
one time and the room is held from another, rather than collapsing
them the way the cards do and leaving the distinction unexplained
(issue #126).
*/}
<div className="flex gap-2">
<span className="text-[#6a96bb] w-24 flex-shrink-0">Time</span>
<span className="text-[#6a96bb] w-24 flex-shrink-0">Meeting Time</span>
<span className="text-[#f0f6ff] font-semibold">{formatTime(booking.meetingTime)}</span>
</div>
<div className="flex gap-2">
<span className="text-[#6a96bb] w-24 flex-shrink-0">Reserved</span>
<span className="text-[#f0f6ff]">{formatTime(booking.startTime)} – {formatTime(booking.endTime)}</span>
</div>
{booking.reservationCode && (
Expand Down
Loading
Loading