feat: add group waitlist functionality

- Introduced GroupWaitlistEntry model in Prisma schema to manage waitlisted users for groups.
- Implemented API route to add users to the group waitlist with validation.
- Enhanced attendee creation logic to handle group capacity and waitlisting scenarios.
- Added new problem builders for handling waitlist-related errors.
- Updated frontend to support waitlist interactions, including modals for capacity warnings.
- Created tests for waitlist functionality, ensuring proper handling of full groups and existing waitlist entries.
This commit is contained in:
Jose Selesan
2026-09-22 16:28:54 -03:00
parent a937c827bb
commit 785d54df7e
16 changed files with 848 additions and 31 deletions

View File

@@ -4,6 +4,7 @@ import { useParams, Link } from '@tanstack/react-router';
import {
CalendarClock,
CheckCircle2,
Clock,
GraduationCap,
Loader2,
Moon,
@@ -32,6 +33,9 @@ export function JoinGroupView() {
phone: string | null;
} | null>(null);
// Waitlist state (group is full)
const [waitlistInfo, setWaitlistInfo] = useState<{ message: string } | null>(null);
const inviteQuery = useQuery({
queryKey: ['public-invite', token],
queryFn: () => getInviteInfo(token),
@@ -49,9 +53,15 @@ export function JoinGroupView() {
}),
onSuccess: (data) => {
setErrorMessage(null);
if (data.status === 'waitlisted') {
setRegisteredAttendee(null);
setWaitlistInfo({ message: data.message });
return;
}
setWaitlistInfo(null);
setRegisteredAttendee({
fullName: data.attendee.fullName,
phone: data.attendee.phone,
fullName: data.attendee!.fullName,
phone: data.attendee!.phone,
});
},
onError: (error: unknown) => {
@@ -163,8 +173,32 @@ export function JoinGroupView() {
</div>
) : null}
{/* Waitlist Confirmation (group full) */}
{waitlistInfo && group ? (
<div className="rounded-2xl border border-accent/30 bg-surface p-6 sm:p-8 text-center shadow-xl space-y-5 animate-step-enter">
<div className="size-16 rounded-full bg-accent-soft text-accent flex items-center justify-center mx-auto">
<Clock className="size-10" />
</div>
<div>
<Badge variant="neutral" className="mb-2">
Lista de Espera
</Badge>
<h1 className="text-2xl font-bold text-primary">Cupo completo</h1>
<p className="mt-2 text-sm text-foreground/70 leading-relaxed">
{waitlistInfo.message}
</p>
</div>
<p className="text-xs text-foreground/60">
El profesor se pondrá en contacto contigo cuando haya un lugar disponible en{' '}
<strong className="text-primary font-semibold">{group.name}</strong>.
</p>
</div>
) : null}
{/* Inscription Form */}
{!registeredAttendee && group ? (
{!registeredAttendee && !waitlistInfo && group ? (
<div className="rounded-2xl border border-border bg-surface p-6 sm:p-8 shadow-xl space-y-6">
{/* Group Info Header */}
<div className="border-b border-border pb-5">