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
11 changes: 11 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "chambers-dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 3000
}
]
}
11 changes: 11 additions & 0 deletions app/(dashboard)/administrator/fulfill-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import { useEffect, useState } from 'react'
import BookingModal from './booking-modal'
import ScopeLabel from '@/app/_components/scope-label'
import type { BookingScope, Division } from '@/lib/booking-scope'

interface Booking {
id: string
Expand All @@ -15,6 +17,10 @@ interface FulfillModalProps {
type: string
purpose: string
body_id: string
bodyName: string
scope: BookingScope
division: Division | null
linkedBodies: { id: string; name: string }[]
}
onClose: () => void
onSuccess: () => void
Expand Down Expand Up @@ -79,6 +85,11 @@ export default function FulfillModal({ request, onClose, onSuccess }: FulfillMod
<p className="text-xs font-medium text-[#93b8d8] uppercase tracking-wide">Request</p>
<p className="text-sm font-semibold text-[#f0f6ff]">{request.purpose}</p>
<p className="text-xs text-[#93b8d8]">{request.type}</p>
<ScopeLabel
row={{ body_id: request.body_id, scope: request.scope, division: request.division, bodies: { name: request.bodyName } }}
linkedBodies={request.linkedBodies}
className="text-xs text-[#93b8d8]"
/>
</div>

{/* Booking dropdown */}
Expand Down
33 changes: 27 additions & 6 deletions app/(dashboard)/administrator/one-time-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import { useState, useEffect } from 'react'
import TimePicker from './time-picker'
import BookingScopeSelector, { type BookingScopeValue } from '@/app/_components/booking-scope-selector'
import { DIVISIONS, type Division } from '@/lib/booking-scope'
import ScopeLabel from '@/app/_components/scope-label'
import { DIVISIONS, type Division, type BookingScope } from '@/lib/booking-scope'

const STATUSES = [
'Reserved',
Expand Down Expand Up @@ -38,7 +39,10 @@ interface PendingRequest {
status: string
created_at: string
body_id: string
scope: BookingScope
division: Division | null
bodies: { name: string } | null
room_request_bodies: { body_id: string; bodies: { name: string } | null }[] | null
room_request_details: Array<{
start_date: string | null
end_date: string | null
Expand Down Expand Up @@ -92,6 +96,7 @@ export default function OneTimeForm({ bodies, semesters, onClose, onSuccess }: O
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const [pendingRequests, setPendingRequests] = useState<PendingRequest[]>([])
const [requestTypeFilter, setRequestTypeFilter] = useState<'all' | BookingScope>('all')

useEffect(() => {
fetch('/api/administrator/requests')
Expand All @@ -104,9 +109,9 @@ export default function OneTimeForm({ bodies, semesters, onClose, onSuccess }: O
.catch(() => {})
}, [])

const visibleRequests = scopeValue.body_id
? pendingRequests.filter(r => r.body_id === scopeValue.body_id)
: pendingRequests
const visibleRequests = pendingRequests
.filter(r => !scopeValue.body_id || r.body_id === scopeValue.body_id)
.filter(r => requestTypeFilter === 'all' || r.scope === requestTypeFilter)

const updateSession = (index: number, field: keyof OneTimeSession, value: string) => {
setSessions(prev => prev.map((s, i) => i === index ? { ...s, [field]: value } : s))
Expand Down Expand Up @@ -306,15 +311,31 @@ export default function OneTimeForm({ bodies, semesters, onClose, onSuccess }: O

<div className="w-96 shrink-0">
<div className="rounded-xl border border-[#1e5080] bg-[#0f2a4a] p-4 h-full">
<p className="text-xs font-semibold text-[#93b8d8] uppercase tracking-wide mb-3">Open Requests</p>
<div className="flex items-center justify-between gap-2 mb-3">
<p className="text-xs font-semibold text-[#93b8d8] uppercase tracking-wide">Open Requests</p>
<select
value={requestTypeFilter}
onChange={e => setRequestTypeFilter(e.target.value as 'all' | BookingScope)}
className="text-xs bg-[#0a1f38] border border-[#1e5080] rounded-lg px-2 py-1 text-[#93b8d8] focus:outline-none focus:ring-2 focus:ring-[#c8102e]/30 focus:border-[#c8102e]"
>
<option value="all">All Types</option>
<option value="single">Single Body</option>
<option value="divisional">Divisional</option>
<option value="multi">Multi-Body</option>
</select>
</div>
{visibleRequests.length === 0 ? (
<p className="text-xs text-[#6a96bb]">No open requests</p>
) : (
<div className="space-y-2">
{visibleRequests.map(r => (
<div key={r.id} className="rounded-lg bg-[#0a1f38] border border-[#1e5080]/60 px-3 py-2.5">
<div className="flex items-baseline justify-between gap-2 mb-0.5">
<span className="text-sm font-semibold text-[#f0f6ff]">{r.bodies?.name ?? '—'}</span>
<ScopeLabel
row={r}
linkedBodies={(r.room_request_bodies ?? []).map(x => ({ id: x.body_id, name: x.bodies?.name ?? '' }))}
className="text-sm font-semibold text-[#f0f6ff]"
/>
<span className="text-xs text-[#6a96bb] shrink-0">{new Date(r.created_at).toLocaleDateString()}</span>
</div>
<p className="text-xs text-[#93b8d8] mb-1.5">{r.purpose}</p>
Expand Down
61 changes: 57 additions & 4 deletions app/(dashboard)/administrator/requests-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { useEffect, useState } from 'react'
import FulfillModal from './fulfill-modal'
import DenyModal from './deny-modal'
import { Skeleton } from '@/app/_components/skeleton'
import ScopeLabel from '@/app/_components/scope-label'
import type { BookingScope, Division } from '@/lib/booking-scope'

function RequestsTabSkeleton() {
const card = (wide: boolean) => (
Expand Down Expand Up @@ -62,8 +64,11 @@ interface RoomRequest {
status: RequestStatus
notes: string | null
created_at: string
scope: BookingScope
division: Division | null
bodies: { name: string } | null
users: { full_name: string } | null
room_request_bodies: { body_id: string; bodies: { name: string } | null }[] | null
room_request_details: {
room_name: string | null
start_date: string
Expand Down Expand Up @@ -113,7 +118,12 @@ export default function RequestsTab({ onCountChange }: RequestsTabProps) {
type: string
purpose: string
body_id: string
bodyName: string
scope: BookingScope
division: Division | null
linkedBodies: { id: string; name: string }[]
} | null>(null)
const [typeFilter, setTypeFilter] = useState<'all' | BookingScope>('all')

const fetchRequests = async () => {
const [reqRes, revRes] = await Promise.all([
Expand All @@ -135,6 +145,15 @@ export default function RequestsTab({ onCountChange }: RequestsTabProps) {

if (requests.length === 0 && revisions.length === 0) return <div className="text-[#6a96bb] text-sm">No requests found.</div>

const filteredRequests = requests.filter(r => typeFilter === 'all' || r.scope === typeFilter)

const TYPE_FILTERS: { value: 'all' | BookingScope; label: string }[] = [
{ value: 'all', label: 'All' },
{ value: 'single', label: 'Single Body' },
{ value: 'divisional', label: 'Divisional' },
{ value: 'multi', label: 'Multi-Body' },
]

return (
<div className="space-y-6">
{revisions.length > 0 && (
Expand Down Expand Up @@ -175,13 +194,38 @@ export default function RequestsTab({ onCountChange }: RequestsTabProps) {

{requests.length > 0 && (
<div className="space-y-3">
{revisions.length > 0 && <h3 className="text-xs font-semibold uppercase tracking-widest text-[#6a96bb]">Room Requests</h3>}
{requests.map(r => (
<div className="flex items-center justify-between flex-wrap gap-3">
{revisions.length > 0 && (
<h3 className="text-xs font-semibold uppercase tracking-widest text-[#6a96bb]">Room Requests</h3>
)}
<div className="flex gap-1 flex-wrap ml-auto">
{TYPE_FILTERS.map(f => (
<button
key={f.value}
onClick={() => setTypeFilter(f.value)}
className={`px-2.5 py-1 text-xs rounded-md font-medium transition-colors ${
typeFilter === f.value
? 'bg-[#c8102e] text-white'
: 'bg-[#0f2a4a] border border-[#1e5080] text-[#93b8d8] hover:text-[#f0f6ff] hover:border-[#93b8d8]'
}`}
>
{f.label}
</button>
))}
</div>
</div>
{filteredRequests.length === 0 ? (
<p className="text-sm text-[#6a96bb]">No requests match this filter.</p>
) : filteredRequests.map(r => (
<div key={r.id} className="border border-[#1e5080] rounded-xl p-5 bg-[#184073] shadow-sm space-y-3">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<span className="font-semibold text-[#f0f6ff]">{r.bodies?.name || 'Unknown Body'}</span>
<ScopeLabel
row={r}
linkedBodies={(r.room_request_bodies ?? []).map(x => ({ id: x.body_id, name: x.bodies?.name ?? '' }))}
className="font-semibold text-[#f0f6ff]"
/>
<span className="mx-2 text-[#1e5080]">·</span>
<span className="text-sm text-[#93b8d8]">{r.type === 'One-Time Room' ? 'One-Time/Multiple Room' : r.type}</span>
</div>
Expand Down Expand Up @@ -245,7 +289,16 @@ export default function RequestsTab({ onCountChange }: RequestsTabProps) {
) : (
<div className="flex gap-2 pt-1">
<button
onClick={() => setFulfillingRequest({ id: r.id, type: r.type, purpose: r.purpose, body_id: r.body_id })}
onClick={() => setFulfillingRequest({
id: r.id,
type: r.type,
purpose: r.purpose,
body_id: r.body_id,
bodyName: r.bodies?.name ?? 'Unknown',
scope: r.scope,
division: r.division,
linkedBodies: (r.room_request_bodies ?? []).map(x => ({ id: x.body_id, name: x.bodies?.name ?? '' })),
})}
className="px-3 py-1 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700"
>
Fulfill
Expand Down
8 changes: 4 additions & 4 deletions app/(dashboard)/administrator/sga-spaces-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ function AdminBookingsPanel({ spaces }: { spaces: Space[] }) {

return (
<div className="space-y-2">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center justify-between gap-4 flex-wrap">
<p className="text-xs text-[#93b8d8]">{RANGE_DESCRIPTIONS[range]} Force-cancelling sends email to creator and all attendees.</p>
<div className="flex gap-1 shrink-0">
{(['week', 'month', 'semester', 'all'] as BookingRange[]).map(r => (
Expand Down Expand Up @@ -424,7 +424,7 @@ function AdminBlackoutsPanel({ spaces }: { spaces: Space[] }) {
</select>
</div>

<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className={labelCls}>Start Date</label>
<input type="date" value={form.start_date} onChange={e => setForm(f => ({ ...f, start_date: e.target.value }))} className={inputCls} required />
Expand Down Expand Up @@ -464,7 +464,7 @@ function AdminBlackoutsPanel({ spaces }: { spaces: Space[] }) {
) : blackouts.length === 0 ? (
<div className="text-sm text-[#93b8d8]">No blackouts configured.</div>
) : (
<div className="rounded-xl border border-[#1e5080] overflow-hidden">
<div className="rounded-xl border border-[#1e5080] overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1e5080] text-[#93b8d8] text-xs">
Expand Down Expand Up @@ -523,7 +523,7 @@ function AdminBlackoutsPanel({ spaces }: { spaces: Space[] }) {
{spaces.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className={labelCls}>Start Date</label>
<input type="date" value={editForm.start_date} onChange={e => setEditForm(f => f && ({ ...f, start_date: e.target.value }))} className={inputCls} required />
Expand Down
33 changes: 27 additions & 6 deletions app/(dashboard)/administrator/tabling-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import { useState, useEffect } from 'react'
import TimePicker from './time-picker'
import BookingScopeSelector, { type BookingScopeValue } from '@/app/_components/booking-scope-selector'
import { DIVISIONS, type Division } from '@/lib/booking-scope'
import ScopeLabel from '@/app/_components/scope-label'
import { DIVISIONS, type Division, type BookingScope } from '@/lib/booking-scope'

const STATUSES = [
'Reserved',
Expand Down Expand Up @@ -46,7 +47,10 @@ interface PendingRequest {
status: string
created_at: string
body_id: string
scope: BookingScope
division: Division | null
bodies: { name: string } | null
room_request_bodies: { body_id: string; bodies: { name: string } | null }[] | null
room_request_details: Array<{
start_date: string | null
end_date: string | null
Expand Down Expand Up @@ -96,6 +100,7 @@ export default function TablingForm({ bodies, semesters, onClose, onSuccess }: T
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const [pendingRequests, setPendingRequests] = useState<PendingRequest[]>([])
const [requestTypeFilter, setRequestTypeFilter] = useState<'all' | BookingScope>('all')

useEffect(() => {
fetch('/api/administrator/requests')
Expand All @@ -108,9 +113,9 @@ export default function TablingForm({ bodies, semesters, onClose, onSuccess }: T
.catch(() => {})
}, [])

const visibleRequests = scopeValue.body_id
? pendingRequests.filter(r => r.body_id === scopeValue.body_id)
: pendingRequests
const visibleRequests = pendingRequests
.filter(r => !scopeValue.body_id || r.body_id === scopeValue.body_id)
.filter(r => requestTypeFilter === 'all' || r.scope === requestTypeFilter)

const updateSession = (index: number, field: keyof Session, value: string) => {
setSessions(prev => prev.map((s, i) => i === index ? { ...s, [field]: value } : s))
Expand Down Expand Up @@ -312,15 +317,31 @@ export default function TablingForm({ bodies, semesters, onClose, onSuccess }: T

<div className="w-96 shrink-0">
<div className="rounded-xl border border-[#1e5080] bg-[#0f2a4a] p-4 h-full">
<p className="text-xs font-semibold text-[#93b8d8] uppercase tracking-wide mb-3">Open Requests</p>
<div className="flex items-center justify-between gap-2 mb-3">
<p className="text-xs font-semibold text-[#93b8d8] uppercase tracking-wide">Open Requests</p>
<select
value={requestTypeFilter}
onChange={e => setRequestTypeFilter(e.target.value as 'all' | BookingScope)}
className="text-xs bg-[#0a1f38] border border-[#1e5080] rounded-lg px-2 py-1 text-[#93b8d8] focus:outline-none focus:ring-2 focus:ring-[#c8102e]/30 focus:border-[#c8102e]"
>
<option value="all">All Types</option>
<option value="single">Single Body</option>
<option value="divisional">Divisional</option>
<option value="multi">Multi-Body</option>
</select>
</div>
{visibleRequests.length === 0 ? (
<p className="text-xs text-[#6a96bb]">No open requests</p>
) : (
<div className="space-y-2">
{visibleRequests.map(r => (
<div key={r.id} className="rounded-lg bg-[#0a1f38] border border-[#1e5080]/60 px-3 py-2.5">
<div className="flex items-baseline justify-between gap-2 mb-0.5">
<span className="text-sm font-semibold text-[#f0f6ff]">{r.bodies?.name ?? '—'}</span>
<ScopeLabel
row={r}
linkedBodies={(r.room_request_bodies ?? []).map(x => ({ id: x.body_id, name: x.bodies?.name ?? '' }))}
className="text-sm font-semibold text-[#f0f6ff]"
/>
<span className="text-xs text-[#6a96bb] shrink-0">{new Date(r.created_at).toLocaleDateString()}</span>
</div>
<p className="text-xs text-[#93b8d8] mb-1.5">{r.purpose}</p>
Expand Down
Loading
Loading