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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,10 @@ next-env.d.ts

# Supabase CLI scratch
supabase/.temp/

# `supabase db dump` output, written to the repo root. Never commit these: a real
# roles.sql can contain role passwords, and data.sql is a full copy of production.
# Schema history belongs in supabase/migrations/ instead.
/data.sql
/roles.sql
/schema.sql
40 changes: 20 additions & 20 deletions app/(dashboard)/administrator/bookings-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import EditWeeklyForm from './edit-weekly-form'
import EditTablingForm from './edit-tabling-form'
import WeeklyBookingGrid from './weekly-booking-grid'
import { Skeleton } from '@/app/_components/skeleton'
import ScopeLabel from '@/app/_components/scope-label'
import type { Division, BookingScope } from '@/lib/booking-scope'

type BookingSubTab = 'One-Time Rooms' | 'Weekly Rooms' | 'Tables'

interface Body {
id: string
name: string
division: Division
}

interface Semester {
Expand All @@ -25,14 +28,25 @@ interface Semester {
is_active: boolean
}

interface OneTimeBooking {
/** Flattens the embedded booking_bodies rows into the shape ScopeLabel expects. */
const linkedBodiesOf = (b: BookingBase) =>
(b.booking_bodies ?? []).map(x => ({ id: x.body_id, name: x.bodies?.name ?? '' }))

/** The columns every booking row carries, regardless of type. */
interface BookingBase {
id: string
body_id: string
purpose: string
is_event: boolean
hidden: boolean
bodies: { name: string } | null
creator_role: string | null
scope: BookingScope
division: Division | null
booking_bodies: { body_id: string; bodies: { name: string } | null }[] | null
}

interface OneTimeBooking extends BookingBase {
one_time_room_bookings: {
id: string
room_name: string
Expand All @@ -44,14 +58,7 @@ interface OneTimeBooking {
}[] | null
}

interface WeeklyBooking {
id: string
body_id: string
purpose: string
is_event: boolean
hidden: boolean
bodies: { name: string } | null
creator_role: string | null
interface WeeklyBooking extends BookingBase {
weekly_room_bookings: {
id: string
room_name: string
Expand All @@ -74,14 +81,7 @@ interface WeeklyBooking {
}[] | null
}

interface TablingBooking {
id: string
body_id: string
purpose: string
is_event: boolean
hidden: boolean
bodies: { name: string } | null
creator_role: string | null
interface TablingBooking extends BookingBase {
tabling_bookings: {
id: string
reservation_code: string | null
Expand Down Expand Up @@ -401,7 +401,7 @@ export default function BookingsTab() {
<div key={b.id} className="border border-[#1e5080] rounded-xl p-5 bg-[#184073] shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="font-semibold text-[#f0f6ff]">{b.bodies?.name}</p>
<ScopeLabel row={b} linkedBodies={linkedBodiesOf(b)} className="block font-semibold text-[#f0f6ff]" />
<p className="text-sm text-[#93b8d8]">{b.purpose}</p>
</div>
<div className="flex items-center gap-3">
Expand Down Expand Up @@ -482,7 +482,7 @@ export default function BookingsTab() {
<div key={b.id} className="border border-[#1e5080] rounded-xl p-5 bg-[#184073] shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="font-semibold text-[#f0f6ff]">{b.bodies?.name}</p>
<ScopeLabel row={b} linkedBodies={linkedBodiesOf(b)} className="block font-semibold text-[#f0f6ff]" />
<p className="text-sm text-[#93b8d8]">{b.purpose}</p>
</div>
<div className="flex items-center gap-3">
Expand Down Expand Up @@ -541,7 +541,7 @@ export default function BookingsTab() {
return (
<div key={b.id} className="border border-[#1e5080] rounded-xl p-5 bg-[#184073] shadow-sm">
<div className="flex items-center justify-between">
<p className="font-semibold text-[#f0f6ff]">{b.bodies?.name}</p>
<ScopeLabel row={b} linkedBodies={linkedBodiesOf(b)} className="block font-semibold text-[#f0f6ff]" />
<div className="flex items-center gap-3">
<span className="hidden md:inline"><AdminRoleBadge role={b.creator_role} /></span>
{b.is_event && (
Expand Down
46 changes: 30 additions & 16 deletions app/(dashboard)/administrator/edit-one-time-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import { useState } from 'react'
import TimePicker from './time-picker'
import BookingScopeSelector, { type BookingScopeValue } from '@/app/_components/booking-scope-selector'
import { DIVISIONS, type Division, type BookingScope } from '@/lib/booking-scope'

const STATUSES = [
'Reserved',
Expand All @@ -20,6 +22,7 @@ const STATUSES = [
interface Body {
id: string
name: string
division: Division
}

interface OneTimeSession {
Expand All @@ -45,6 +48,9 @@ interface EditOneTimeFormProps {
id: string
body_id: string
purpose: string
scope: BookingScope
division: Division | null
booking_bodies: { body_id: string; bodies: { name: string } | null }[] | null
one_time_room_bookings: {
id: string
room_name: string
Expand All @@ -65,10 +71,16 @@ const labelCls = "block text-xs font-medium text-[#93b8d8] mb-1"

export default function EditOneTimeForm({ booking, bodies, onClose, onSuccess }: EditOneTimeFormProps) {
const [form, setForm] = useState({
body_id: booking.body_id,
purpose: booking.purpose,
})

const [scopeValue, setScopeValue] = useState<BookingScopeValue>({
scope: booking.scope ?? 'single',
body_id: booking.body_id,
division: booking.division ?? null,
body_ids: (booking.booking_bodies ?? []).map(b => b.body_id),
})

const [sessions, setSessions] = useState<OneTimeSession[]>(
booking.one_time_room_bookings?.map(d => ({
room_name: d.room_name ?? '',
Expand All @@ -88,10 +100,18 @@ export default function EditOneTimeForm({ booking, bodies, onClose, onSuccess }:
}

const handleSubmit = async () => {
if (!form.body_id || !form.purpose) {
if (!scopeValue.body_id || !form.purpose) {
setError('Please fill out all required fields.')
return
}
if (scopeValue.scope === 'divisional' && !scopeValue.division) {
setError('Please select a division.')
return
}
if (scopeValue.scope === 'multi' && scopeValue.body_ids.filter(id => id !== scopeValue.body_id).length === 0) {
setError('Select at least one other body for a multi-body booking.')
return
}
for (const s of sessions) {
if (!s.booking_date || !s.start_time || !s.end_time) {
setError('Please fill out all session fields.')
Expand All @@ -105,7 +125,7 @@ export default function EditOneTimeForm({ booking, bodies, onClose, onSuccess }:
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
booking_id: booking.id,
body_id: form.body_id,
...scopeValue,
purpose: form.purpose,
sessions,
}),
Expand All @@ -123,19 +143,13 @@ export default function EditOneTimeForm({ booking, bodies, onClose, onSuccess }:

return (
<div className="space-y-3">
<div>
<label className={labelCls}>Body *</label>
<select
value={form.body_id}
onChange={e => setForm({ ...form, body_id: e.target.value })}
className={inputCls}
>
<option value="">Select Body</option>
{bodies.map(b => (
<option key={b.id} value={b.id}>{b.name}</option>
))}
</select>
</div>
<BookingScopeSelector
value={scopeValue}
onChange={setScopeValue}
ownerBodies={bodies}
allBodies={bodies}
allowedDivisions={[...DIVISIONS]}
/>

<div>
<label className={labelCls}>Purpose *</label>
Expand Down
39 changes: 30 additions & 9 deletions app/(dashboard)/administrator/edit-tabling-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import { useState } from 'react'
import TimePicker from './time-picker'
import BookingScopeSelector, { type BookingScopeValue } from '@/app/_components/booking-scope-selector'
import { DIVISIONS, type Division, type BookingScope } from '@/lib/booking-scope'

const STATUSES = [
'Reserved',
Expand All @@ -20,6 +22,7 @@ const STATUSES = [
interface Body {
id: string
name: string
division: Division
}

interface Session {
Expand All @@ -38,6 +41,9 @@ interface EditTablingFormProps {
id: string
body_id: string
purpose: string
scope: BookingScope
division: Division | null
booking_bodies: { body_id: string; bodies: { name: string } | null }[] | null
tabling_bookings: {
id: string
reservation_code: string | null
Expand Down Expand Up @@ -65,8 +71,14 @@ const emptySession = (): Session => ({
export default function EditTablingForm({ booking, bodies, onClose, onSuccess }: EditTablingFormProps) {
const t = booking.tabling_bookings?.[0]

const [form, setForm] = useState({
const [scopeValue, setScopeValue] = useState<BookingScopeValue>({
scope: booking.scope ?? 'single',
body_id: booking.body_id,
division: booking.division ?? null,
body_ids: (booking.booking_bodies ?? []).map(b => b.body_id),
})

const [form, setForm] = useState({
purpose: booking.purpose,
reservation_code: t?.reservation_code ?? '',
})
Expand All @@ -92,10 +104,18 @@ export default function EditTablingForm({ booking, bodies, onClose, onSuccess }:
}

const handleSubmit = async () => {
if (!form.body_id || !form.purpose) {
if (!scopeValue.body_id || !form.purpose) {
setError('Please fill out all required fields.')
return
}
if (scopeValue.scope === 'divisional' && !scopeValue.division) {
setError('Please select a division.')
return
}
if (scopeValue.scope === 'multi' && scopeValue.body_ids.filter(id => id !== scopeValue.body_id).length === 0) {
setError('Select at least one other body for a multi-body booking.')
return
}

for (const s of sessions) {
if (!s.location || !s.session_date || !s.start_time || !s.end_time) {
Expand All @@ -112,6 +132,7 @@ export default function EditTablingForm({ booking, bodies, onClose, onSuccess }:
booking_id: booking.id,
tabling_id: t.id,
...form,
...scopeValue,
sessions,
}),
})
Expand All @@ -128,13 +149,13 @@ export default function EditTablingForm({ booking, bodies, onClose, onSuccess }:

return (
<div className="space-y-4">
<div>
<label className={labelCls}>Body *</label>
<select value={form.body_id} onChange={e => setForm({ ...form, body_id: e.target.value })} className={inputCls}>
<option value="">Select Body</option>
{bodies.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
</select>
</div>
<BookingScopeSelector
value={scopeValue}
onChange={setScopeValue}
ownerBodies={bodies}
allBodies={bodies}
allowedDivisions={[...DIVISIONS]}
/>

<div>
<label className={labelCls}>Purpose *</label>
Expand Down
Loading
Loading