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
8 changes: 7 additions & 1 deletion app/(dashboard)/my-rooms/calendar-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useMemo, useState } from 'react'
import {
type FlatBooking,
bookingTitle,
statusBarColors,
statusTextColors,
formatTime,
Expand Down Expand Up @@ -161,7 +162,12 @@ export default function CalendarView({ bookings, onSelect, today }: CalendarView
<div key={b.id} onClick={() => onSelect(b)} className="flex items-center gap-4 px-5 py-3.5 hover:bg-[#1a4d8a] transition-colors cursor-pointer">
<div className={`w-1.5 h-8 rounded-full flex-shrink-0 ${statusBarColors[b.status] || 'bg-[#1e5080]'}`} />
<div className="flex-1 min-w-0">
<p className="font-semibold text-[#f0f6ff] truncate">{b.scopeLabel}</p>
<div className="flex items-baseline gap-2 min-w-0">
<p className="font-semibold text-[#f0f6ff] truncate">{bookingTitle(b)}</p>
{b.scopeLabel !== bookingTitle(b) && (
<span className="text-xs text-[#93b8d8] truncate">{b.scopeLabel}</span>
)}
</div>
<p className="text-sm text-[#6a96bb]">{b.location} · {formatTime(b.startTime)} – {formatTime(b.endTime)}</p>
</div>
<span className={`hidden md:inline text-xs font-semibold flex-shrink-0 ${statusTextColors[b.status] || 'text-[#93b8d8]'}`}>{b.status}</span>
Expand Down
5 changes: 3 additions & 2 deletions app/(dashboard)/my-rooms/my-rooms-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Skeleton } from '@/app/_components/skeleton'
import {
type FlatBooking,
type MyRoomsResponse,
bookingTitle,
flattenMyRooms,
isWithinDays,
statusColors,
Expand Down Expand Up @@ -313,12 +314,12 @@ export default function MyRoomsClient({
<div className={`w-1.5 h-8 rounded-full flex-shrink-0 ${statusBarColors[b.status] || 'bg-[#1e5080]'}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 min-w-0">
<p className="font-semibold text-[#f0f6ff] truncate">{b.location}</p>
<p className="font-semibold text-[#f0f6ff] truncate">{bookingTitle(b)}</p>
{b.senateType && (
<span className={`text-xs font-medium px-2 py-0.5 rounded-full flex-shrink-0 ${senateTypeBadgeColors[b.senateType] || DEFAULT_SENATE_BADGE}`}>{b.senateType}</span>
)}
</div>
<p className="text-sm text-[#6a96bb]">{formatDate(b.date)} · {formatTime(b.startTime)} – {formatTime(b.endTime)}</p>
<p className="text-sm text-[#6a96bb]">{b.location} · {formatDate(b.date)} · {formatTime(b.startTime)} – {formatTime(b.endTime)}</p>
</div>
<span className="hidden md:inline text-xs text-[#6a96bb] flex-shrink-0">{b.type === 'One-Time Room' ? 'One-Time/Multiple Room' : b.type}</span>
<span className={`hidden md:inline text-xs font-semibold flex-shrink-0 ${statusTextColors[b.status] || 'text-[#93b8d8]'}`}>{b.status}</span>
Expand Down
14 changes: 14 additions & 0 deletions app/(dashboard)/my-rooms/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ export function scopeFullOf(b: ScopedBookingRow): string[] {
).full
}

/**
* The headline for a booking row: what it is, not who runs it.
*
* Rows used to lead with the owning body (calendar day list) or the room (list
* view), which is the least distinguishing thing about them -- a body's rows all
* read the same, and so do a room's. The purpose is the title someone typed for
* this specific booking, so it leads and the rest drops to supporting text
* (issue #65). Falls back to the scope label for a booking saved without one, so
* a row is never headed by an empty string.
*/
export function bookingTitle(b: FlatBooking): string {
return b.purpose?.trim() || b.scopeLabel
}

export const SENATE_TYPES = ['Full Body', 'Weekly', 'Office Hours'] as const

export const statusColors: Record<string, string> = {
Expand Down
44 changes: 35 additions & 9 deletions lib/pending-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,20 @@ function nameOf(ref: NameRef): string {
return r?.name ?? 'Unknown'
}

/**
* What a pending action is *about*, for the one-line summary.
*
* The purpose is the human-written title of the thing being held -- "Fall
* Kickoff", "Weekly Meeting" -- and is what an admin scans the list for. The
* governing body used to be shown here instead, but a body runs many bookings,
* so repeating it told the reader nothing that separated one row from the next
* (issue #63). The body name stays as the fallback for a record with no purpose,
* and remains the whole label for membership requests, which have no title.
*/
function titleOf(purpose: string | null | undefined, ref: NameRef): string {
return purpose?.trim() || nameOf(ref)
}

function shortDate(dateStr: string | null): string {
if (!dateStr) return 'no date'
const [y, m, d] = dateStr.split('-').map(Number)
Expand Down Expand Up @@ -198,10 +212,11 @@ export function settingsFromRow(row: SettingsRow | null): PendingActionSettings
export interface BookingChildDates {
type?: string | null
is_event?: boolean | null
purpose?: string | null
bodies?: NameRef
one_time_room_bookings?: { id?: string; booking_date: string | null }[] | null
weekly_room_bookings?:
| { weekly_room_occurrences?: { id?: string; occurrence_date: string | null }[] | null }[]
| { weekly_room_occurrences?: { id?: string; occurrence_date: string | null; purpose?: string | null }[] | null }[]
| null
tabling_bookings?:
| { tabling_sessions?: { id?: string; session_date: string | null }[] | null }[]
Expand All @@ -219,6 +234,14 @@ export function sessionDatesOf(b: BookingChildDates): string[] {
return out
}

/** The purpose override on one specific weekly occurrence, or null when it inherits (issue #55). */
function occurrencePurposeById(b: BookingChildDates, childId: string): string | null {
for (const w of b.weekly_room_bookings ?? [])
for (const occ of w.weekly_room_occurrences ?? [])
if (occ.id === childId) return occ.purpose ?? null
return null
}

/** The date of one specific child row (used for occurrence-scoped cancellations). */
function childDateById(b: BookingChildDates, childId: string): string | null {
for (const o of b.one_time_room_bookings ?? []) if (o.id === childId) return o.booking_date ?? null
Expand All @@ -230,7 +253,7 @@ function childDateById(b: BookingChildDates, childId: string): string | null {
}

const BOOKING_CHILD_SELECT =
'type, is_event, bodies(name), one_time_room_bookings(id, booking_date), weekly_room_bookings(weekly_room_occurrences(id, occurrence_date)), tabling_bookings(tabling_sessions(id, session_date))'
'type, is_event, purpose, bodies(name), one_time_room_bookings(id, booking_date), weekly_room_bookings(weekly_room_occurrences(id, occurrence_date, purpose)), tabling_bookings(tabling_sessions(id, session_date))'

// ---------------------------------------------------------------------------
// Main entry
Expand All @@ -256,7 +279,7 @@ export async function fetchPendingActions(
adminSupabase.from('app_settings').select('*').eq('id', 1).maybeSingle(),
adminSupabase
.from('room_requests')
.select('id, type, bodies(name), room_request_details(start_date), tabling_request_sessions(session_date)')
.select('id, type, purpose, bodies(name), room_request_details(start_date), tabling_request_sessions(session_date)')
.eq('status', 'Pending'),
adminSupabase
.from('revision_requests')
Expand All @@ -283,6 +306,7 @@ export async function fetchPendingActions(
for (const r of (requests ?? []) as unknown as {
id: string
type: string
purpose: string | null
bodies: NameRef
room_request_details: { start_date: string | null }[] | null
tabling_request_sessions: { session_date: string | null }[] | null
Expand All @@ -304,7 +328,7 @@ export async function fetchPendingActions(
id: `request:${r.id}`,
kind: 'request',
severity,
label: `${r.type} request — ${nameOf(r.bodies)} · ${shortDate(refDate)}`,
label: `${r.type} request — ${titleOf(r.purpose, r.bodies)} · ${shortDate(refDate)}`,
originTab: 'Requests',
originId: r.id,
referenceDate: refDate,
Expand All @@ -327,7 +351,7 @@ export async function fetchPendingActions(
id: `revision:${rv.id}`,
kind: 'revision',
severity,
label: `Revision — ${nameOf(b?.bodies ?? null)} · ${shortDate(refDate)}`,
label: `Revision — ${titleOf(b?.purpose, b?.bodies ?? null)} · ${shortDate(refDate)}`,
originTab: 'Requests',
originId: rv.id,
referenceDate: refDate,
Expand All @@ -348,6 +372,8 @@ export async function fetchPendingActions(
: minDate(sessionDatesOf(b))
: null
const isEvent = !!b?.is_event
const purpose =
(b && c.occurrence_id ? occurrencePurposeById(b, c.occurrence_id) : null) ?? b?.purpose

let severity: Severity = 'regular'
if (refDate) {
Expand All @@ -361,7 +387,7 @@ export async function fetchPendingActions(
id: `cancellation:${c.id}`,
kind: 'cancellation',
severity,
label: `${isEvent ? 'Event ' : ''}Cancellation — ${nameOf(b?.bodies ?? null)} · ${shortDate(refDate)}`,
label: `${isEvent ? 'Event ' : ''}Cancellation — ${titleOf(purpose, b?.bodies ?? null)} · ${shortDate(refDate)}`,
originTab: 'Cancellations',
originId: c.id,
referenceDate: refDate,
Expand All @@ -383,14 +409,14 @@ export async function fetchPendingActions(
if (days < 0 || utcMidnight(eventDate) > triggerCutoffMs(s.eventTriggerMonths)) continue

const tracking = Array.isArray(b.event_tracking) ? b.event_tracking[0] : b.event_tracking
const bodyName = nameOf(b.bodies ?? null)
const title = titleOf(b.purpose, b.bodies ?? null)

if (!tracking?.event_management_form) {
actions.push({
id: `event-form:${b.id}:mgmt`,
kind: 'event-form',
severity: severityForRange(days, s.eventMgmt[0], s.warningLeadDays),
label: `Event Management Form — ${bodyName} · ${shortDate(eventDate)}`,
label: `Event Management Form — ${title} · ${shortDate(eventDate)}`,
originTab: 'Events',
originId: b.id,
referenceDate: eventDate,
Expand All @@ -401,7 +427,7 @@ export async function fetchPendingActions(
id: `event-form:${b.id}:engage`,
kind: 'event-form',
severity: severityForRange(days, s.eventEngage[0], s.warningLeadDays),
label: `Engage Form — ${bodyName} · ${shortDate(eventDate)}`,
label: `Engage Form — ${title} · ${shortDate(eventDate)}`,
originTab: 'Events',
originId: b.id,
referenceDate: eventDate,
Expand Down
Loading