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
114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState, useCallback } from 'react'
|
|
import { toast } from 'sonner'
|
|
import { listSessionSignups, markAttendance, completeSession } from '@/lib/actions'
|
|
import { SIGNUP_STATUS_LABEL } from '@/lib/classes-logic'
|
|
|
|
type Signup = {
|
|
id: string
|
|
member_id: string
|
|
status: string
|
|
attended: boolean
|
|
signed_up_at: string
|
|
space_members: { display_name: string | null; email: string | null } | null
|
|
}
|
|
|
|
export function SessionAttendance({
|
|
sessionId,
|
|
sessionStatus,
|
|
onCompleted,
|
|
}: {
|
|
sessionId: string
|
|
sessionStatus: string
|
|
onCompleted: () => void
|
|
}) {
|
|
const [loading, setLoading] = useState(false)
|
|
const [rows, setRows] = useState<Signup[]>([])
|
|
const [busy, setBusy] = useState(false)
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true)
|
|
const res = await listSessionSignups({ sessionId })
|
|
setLoading(false)
|
|
if ('error' in res && res.error) {
|
|
toast.error(res.error)
|
|
return
|
|
}
|
|
setRows(('data' in res ? res.data : []) ?? [])
|
|
}, [sessionId])
|
|
|
|
useEffect(() => {
|
|
load()
|
|
}, [load])
|
|
|
|
async function toggle(s: Signup) {
|
|
const res = await markAttendance({ signupId: s.id, attended: !s.attended })
|
|
if ('error' in res && res.error) {
|
|
toast.error(res.error)
|
|
return
|
|
}
|
|
setRows(prev => prev.map(r => (r.id === s.id ? { ...r, attended: !r.attended } : r)))
|
|
}
|
|
|
|
async function onComplete() {
|
|
setBusy(true)
|
|
const res = await completeSession({ sessionId })
|
|
setBusy(false)
|
|
if ('error' in res && res.error) {
|
|
toast.error(res.error)
|
|
return
|
|
}
|
|
const d = (res as { data: { certificatesIssued: number; certificatesSkipped: boolean } }).data
|
|
if (d.certificatesSkipped) {
|
|
toast.success('Session completed. Certificates were NOT issued (requires the certifications.grant permission).')
|
|
} else if (d.certificatesIssued > 0) {
|
|
toast.success(`Session completed. ${d.certificatesIssued} certificate(s) issued.`)
|
|
} else {
|
|
toast.success('Session completed.')
|
|
}
|
|
onCompleted()
|
|
}
|
|
|
|
return (
|
|
<div className="mt-3 rounded border border-border bg-background p-3">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<p className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">Attendees</p>
|
|
{sessionStatus === 'scheduled' && (
|
|
<button
|
|
onClick={onComplete}
|
|
disabled={busy}
|
|
className="font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-primary hover:text-primary transition disabled:opacity-60"
|
|
>
|
|
Complete session
|
|
</button>
|
|
)}
|
|
</div>
|
|
{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 signups yet.</p>
|
|
) : (
|
|
<ul className="divide-y divide-border">
|
|
{rows.map(s => (
|
|
<li key={s.id} className="py-2 flex items-center justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<span className="font-sans text-sm text-foreground">
|
|
{s.space_members?.display_name ?? s.space_members?.email ?? 'Member'}
|
|
</span>
|
|
<span className="font-mono text-[10px] text-muted-foreground ml-2">
|
|
{SIGNUP_STATUS_LABEL[s.status] ?? s.status}
|
|
</span>
|
|
</div>
|
|
<label className="flex items-center gap-1.5 font-mono text-[10px] text-muted-foreground cursor-pointer">
|
|
<input type="checkbox" checked={s.attended} onChange={() => toggle(s)} />
|
|
attended
|
|
</label>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|