Regenerate types/database.ts from a local Supabase built from the canonical scripts/schema.sql. The running dev DB was stale (missing the 'stripe' payment_platform value, members_with_permission, spaces.host_requires_card, payments.link_status, dues_payment_methods.space_id), so it was rebuilt from schema.sql first. With real relationship metadata the 11 `as unknown as … // TODO(types)` join casts are no longer needed: - me portal, session-attendance, member-certifications, equipment-reservations: narrow the action result with `'data' in res` instead of a blanket cast. - stripe / door embeds read the now-typed relation directly. - completeSession + listEquipmentReservations route the embedded-select query through the fully-typed createClient() rather than the wide ServerSupabase gate client (the pattern documented in auth-helpers.ts); equipment disambiguates its two space_members FKs with an explicit !fkey hint. Collapse the hand-maintained governance rows/enums in types/domain/governance.ts into Tables<>/Enums<> aliases. Tighten duesPaymentMethodSchema.platform to the DUES_LINK_PLATFORMS literal union (the old `as unknown as [string,...]` erased it), which the now-correct payment_platform enum requires. The hand-added class_signup_tx / class_cancel_tx RPC entries are now generated natively. typecheck 0 · lint 0 errors · 645 tests · build ok
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useState, useCallback } from 'react'
|
||
import { toast } from 'sonner'
|
||
import { listEquipmentReservations, cancelReservation } from '@/lib/actions'
|
||
|
||
type Reservation = {
|
||
id: string
|
||
member_id: string
|
||
starts_at: string
|
||
ends_at: string
|
||
status: string
|
||
notes: string | null
|
||
space_members: { display_name: string | null; email: string | null } | null
|
||
}
|
||
|
||
function who(r: Reservation) {
|
||
return r.space_members?.display_name ?? r.space_members?.email ?? 'Member'
|
||
}
|
||
|
||
// Manager view of who reserved a piece of equipment, with a manager cancel.
|
||
// listEquipmentReservations is equipment.manage-gated server-side.
|
||
export function EquipmentReservations({ equipmentId }: { equipmentId: string }) {
|
||
const [loading, setLoading] = useState(false)
|
||
const [rows, setRows] = useState<Reservation[]>([])
|
||
const [busy, setBusy] = useState<string | null>(null)
|
||
|
||
const load = useCallback(async () => {
|
||
setLoading(true)
|
||
const res = await listEquipmentReservations({ equipmentId })
|
||
setLoading(false)
|
||
if ('error' in res && res.error) {
|
||
toast.error(res.error)
|
||
return
|
||
}
|
||
setRows(('data' in res ? res.data : []) ?? [])
|
||
}, [equipmentId])
|
||
|
||
useEffect(() => {
|
||
load()
|
||
}, [load])
|
||
|
||
async function onCancel(r: Reservation) {
|
||
setBusy(r.id)
|
||
const res = await cancelReservation({ reservationId: r.id })
|
||
setBusy(null)
|
||
if ('error' in res && res.error) {
|
||
toast.error(res.error)
|
||
return
|
||
}
|
||
setRows(prev => prev.map(x => (x.id === r.id ? { ...x, status: 'cancelled' } : x)))
|
||
toast.success('Reservation cancelled')
|
||
}
|
||
|
||
return (
|
||
<div className="mt-3 rounded border border-border bg-background p-3">
|
||
<p className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground mb-2">Reservations</p>
|
||
{loading ? (
|
||
<p className="font-mono text-[10px] text-muted-foreground">Loading…</p>
|
||
) : rows.length === 0 ? (
|
||
<p className="font-mono text-[10px] text-muted-foreground">No reservations.</p>
|
||
) : (
|
||
<ul className="divide-y divide-border">
|
||
{rows.map(r => (
|
||
<li key={r.id} className="py-2 flex items-center justify-between gap-3 flex-wrap">
|
||
<div className="min-w-0">
|
||
<span className="font-sans text-sm text-foreground">{who(r)}</span>
|
||
<span className="font-mono text-[10px] text-muted-foreground ml-2">
|
||
{new Date(r.starts_at).toLocaleString()} – {new Date(r.ends_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||
{' · '}
|
||
<span className={r.status === 'cancelled' ? 'text-red-500' : 'text-primary'}>{r.status}</span>
|
||
</span>
|
||
{r.notes && <p className="font-sans text-xs text-muted-foreground mt-0.5">{r.notes}</p>}
|
||
</div>
|
||
{r.status !== 'cancelled' && (
|
||
<button
|
||
onClick={() => onCancel(r)}
|
||
disabled={busy === r.id}
|
||
className="font-mono text-[10px] border border-border px-3 py-2 rounded hover:border-red-500 hover:text-red-500 transition disabled:opacity-60"
|
||
>
|
||
Cancel
|
||
</button>
|
||
)}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|