Feat/event waitlist auto promotion - #256
Conversation
Enable out-of-capacity registrations via an event waitlist and auto-promote users when spots open, while exposing waitlist demand in attendee and admin event views. Made-with: Cursor
WalkthroughA new event waitlist system is introduced with database schema changes, server-side capacity and promotion logic, and updated UI components to allow users to register or join waitlists when events reach capacity. Changes
Sequence DiagramsequenceDiagram
participant User
participant UI as Event UI Component
participant RegisterAction as registerEvent Action
participant DB as Database Transaction
participant CapacityAction as promoteNextFromWaitlist
participant NotifyAction as notifyAdmins
User->>UI: Click "Inscribirme"
UI->>RegisterAction: registerEvent(eventId)
RegisterAction->>DB: Start Serializable Transaction
DB->>DB: Check event capacity
alt Capacity Available
DB->>DB: Create eventRegistration
DB->>DB: Cancel any pending waitlist entry
DB-->>RegisterAction: { status: 'registered', registrationId }
RegisterAction->>NotifyAction: emit event_registration_created
else Capacity Full
DB->>DB: Create eventWaitlistEntry
DB->>DB: Calculate waitlist position
DB-->>RegisterAction: { status: 'waitlisted', waitlistId, position }
RegisterAction->>NotifyAction: emit event_registered_to_waitlist
end
RegisterAction-->>UI: RegistrationResult
UI->>User: Show status-specific toast & redirect
User->>UI: Delete Registration
UI->>RegisterAction: deleteRegistration(registrationId)
RegisterAction->>DB: Remove eventRegistration
RegisterAction->>CapacityAction: promoteNextFromWaitlist(eventId)
CapacityAction->>DB: Start Serializable Transaction
alt Waitlist Exists
DB->>DB: Get next waitlisted user
DB->>DB: Create/reactivate registration
DB->>DB: Mark waitlist entry as promoted
DB-->>CapacityAction: { registrationId, userId, userName }
CapacityAction-->>RegisterAction: Promotion details
RegisterAction->>NotifyAction: emit event_waitlist_promoted
end
RegisterAction-->>UI: { success: true }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/meetup/page.tsx (1)
67-86:⚠️ Potential issue | 🟡 MinorKeep the metadata titles aligned.
titleandtwitter.titlewere updated, butopenGraph.titlestill uses the old format on Line 74. Shares from social crawlers will keep showing a different title than the page itself.Suggested fix
return { title: `${nextMeetup.name} (PCN)`, @@ openGraph: { - title: `${nextMeetup.name} - Meetup - PCN`, + title: `${nextMeetup.name} (PCN)`,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/meetup/page.tsx` around lines 67 - 86, Update the Open Graph title to match the page and Twitter title format: change the openGraph.title expression in the returned metadata object so it uses the same template as title and twitter.title (i.e. `${nextMeetup.name} (PCN)`), ensuring openGraph.title, title, and twitter.title are identical and use nextMeetup.name consistently.
🧹 Nitpick comments (1)
prisma/schema.prisma (1)
219-223: Add a composite index for the waitlist hot path.The new queries in
src/actions/events/event-capacity.tsandsrc/actions/events/register-event.tsconsistently filter byeventId,cancelledAt, andpromotedAt, then sort/count bycreatedAt. Single-column indexes won’t help that pattern much once waitlists grow.Suggested change
model EventWaitlistEntry { … @@unique([eventId, userId]) + @@index([eventId, cancelledAt, promotedAt, createdAt]) @@index([eventId]) @@index([userId]) @@index([cancelledAt]) @@index([promotedAt]) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@prisma/schema.prisma` around lines 219 - 223, Add a composite index to the Prisma model indexes to support the waitlist hot path used in src/actions/events/event-capacity.ts and src/actions/events/register-event.ts: create an @@index that includes eventId, cancelledAt, promotedAt and createdAt (e.g., @@index([eventId, cancelledAt, promotedAt, createdAt])) alongside the existing @@unique([eventId, userId]) and single-column @@index entries so queries filtering by eventId/cancelledAt/promotedAt and ordering/counting by createdAt will use the composite index.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@prisma/migrations/20260410120000_add_event_waitlist/migration.sql`:
- Around line 26-27: The current unique index
EventWaitlistEntry_eventId_userId_key prevents legitimate re-joins because it
enforces uniqueness across all historical rows; replace it with a partial unique
index that only covers active rows (e.g., unique on eventId,userId WHERE
cancelledAt IS NULL AND promotedAt IS NULL) or alternatively remove the global
unique constraint and implement reactivation of historical rows in the
registration flow; update the migration to drop the existing
EventWaitlistEntry_eventId_userId_key and create the scoped unique index, and
adjust Prisma-side logic (registration/reactivation code) to match the new “one
active waitlist row per user/event” semantics.
In `@src/actions/events/cancel-registration.ts`:
- Around line 92-115: The cancellation currently calls
prisma.eventRegistration.update before calling promoteNextFromWaitlist(),
risking a freed seat without a successful promotion; change this to perform the
cancellation and the waitlist promotion in the same Prisma transaction (use
prisma.$transaction with an async callback or refactor promoteNextFromWaitlist
to accept a transactional Prisma client), run prisma.eventRegistration.update
and the promotion logic using that same transactional client, and only after the
transaction successfully commits call notifyAdmins for the promoted user (keep
function names: prisma.eventRegistration.update, promoteNextFromWaitlist or a
new promoteNextFromWaitlistTx that accepts a client, and notifyAdmins).
In `@src/actions/events/delete-registration.ts`:
- Around line 44-58: The notifyAdmins call can throw and must not abort the
delete flow; wrap the notifyAdmins(...) invocation (the block handling promoted
from promoteNextFromWaitlist(registration.eventId)) in a try/catch, so any
exception is caught and handled (log the error and continue) instead of
rethrowing; ensure the code still returns success for the deletion path even if
notifyAdmins fails, referencing promoteNextFromWaitlist, promoted, notifyAdmins
and event in your fix.
In `@src/actions/events/event-capacity.ts`:
- Around line 130-137: The code cancels a stale waitlist entry when
existingRegistration is present (within the block using
tx.eventWaitlistEntry.update on nextWaitlistEntry) but then returns null, which
stops processing further waitlist entries and leaves capacity unfilled; change
the control flow so that after marking nextWaitlistEntry.cancelledAt you do not
return but instead continue processing the loop/logic that promotes the next
eligible waitlist entry (i.e., remove the early return and either continue to
the next iteration or let the promotion logic that follows run), ensuring the
rest of the function (promotion code that reads nextWaitlistEntry and creates a
registration) can run for subsequent entries.
In `@src/actions/events/register-event.ts`:
- Around line 168-177: Count the waitlist position using the same sort key as
getWaitlistPosition by adding an id tie-breaker to the where clause: instead of
only counting entries with createdAt <= waitlistCreatedAt, count entries where
createdAt < waitlistCreatedAt OR (createdAt = waitlistCreatedAt AND id < = <this
entry's id>), so use the current waitlist entry's id (e.g. waitlistEntryId / id)
and waitlistCreatedAt in tx.eventWaitlistEntry.count to ensure stable FIFO
ordering that matches getWaitlistPosition.
In `@src/app/`(platform)/eventos/[id]/inscripciones/page.tsx:
- Around line 81-98: The admin waitlist ordering differs from the promotion
ordering; update the waitlist query used to build activeWaitlistEntries so it
orders identically to promoteNextFromWaitlist() by using orderBy: [{ createdAt:
'asc' }, { id: 'asc' }] in the prisma.eventWaitlistEntry.findMany call (keep the
same where and include clauses and the existing cancelledAt/promotedAt
filtering), ensuring the admin table position matches the actual promotion
order.
In `@src/components/events/event-detail-client.tsx`:
- Line 46: The component currently only flips justWaitlistedLocally and doesn't
persist the returned waitlist position, so the UI keeps showing the stale server
prop waitlistPosition; add a local state like waitlistPositionLocal
(useState<number | null>(null)) and on successful waitlist join in the handlers
referenced (the functions that call setJustWaitlistedLocally at the blocks
around lines 46, ~80-89, ~114-124, and ~163-170) set waitlistPositionLocal to
the position returned by the server response; then update rendering to prefer
waitlistPositionLocal if non-null (falling back to props.waitlistPosition) so
the “Tu posición actual…” message updates immediately.
In `@src/components/events/register-event-button.tsx`:
- Line 14: Update the onSuccess prop and its usage to forward the waitlist
position returned by registerEvent: change the prop signature (onSuccess) to
accept either a payload object or an extra optional parameter that includes both
status and position (e.g., { status: 'registered' | 'waitlisted', position?:
number }) and, inside the register-event-button component where registerEvent()
is called, pass the returned position when status === 'waitlisted' (use the
position property from registerEvent's response). Ensure all calls to onSuccess
(and its type) are updated so callers can read position without requiring a
refresh.
---
Outside diff comments:
In `@src/app/meetup/page.tsx`:
- Around line 67-86: Update the Open Graph title to match the page and Twitter
title format: change the openGraph.title expression in the returned metadata
object so it uses the same template as title and twitter.title (i.e.
`${nextMeetup.name} (PCN)`), ensuring openGraph.title, title, and twitter.title
are identical and use nextMeetup.name consistently.
---
Nitpick comments:
In `@prisma/schema.prisma`:
- Around line 219-223: Add a composite index to the Prisma model indexes to
support the waitlist hot path used in src/actions/events/event-capacity.ts and
src/actions/events/register-event.ts: create an @@index that includes eventId,
cancelledAt, promotedAt and createdAt (e.g., @@index([eventId, cancelledAt,
promotedAt, createdAt])) alongside the existing @@unique([eventId, userId]) and
single-column @@index entries so queries filtering by
eventId/cancelledAt/promotedAt and ordering/counting by createdAt will use the
composite index.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 4c41aeea-46a7-4ffd-975f-d78985207183
📒 Files selected for processing (15)
prisma/migrations/20260410120000_add_event_waitlist/migration.sqlprisma/schema.prismasrc/actions/events/cancel-registration.tssrc/actions/events/check-event-capacity.tssrc/actions/events/delete-registration.tssrc/actions/events/event-capacity.tssrc/actions/events/register-event.tssrc/app/(platform)/eventos/[id]/inscripciones/page.tsxsrc/app/(platform)/eventos/[id]/page.tsxsrc/app/(platform)/testimonios/[id]/page.tsxsrc/app/meetup/page.tsxsrc/components/events/cancel-registration-button.tsxsrc/components/events/event-detail-client.tsxsrc/components/events/register-event-button.tsxsrc/components/home/sponsors-section.tsx
| -- CreateIndex | ||
| CREATE UNIQUE INDEX "EventWaitlistEntry_eventId_userId_key" ON "EventWaitlistEntry"("eventId", "userId"); |
There was a problem hiding this comment.
This unique index blocks legitimate re-joins after cancel/promotion.
Because waitlist rows are kept as history via cancelledAt/promotedAt, UNIQUE(eventId, userId) means a user can never join the waitlist again for the same event after leaving it or after being promoted once. That’s stricter than “no duplicate active waitlist entry”.
If the intended rule is “one active waitlist row per user/event”, this needs either:
- reusing/reactivating the historical row in the registration flow, or
- a partial unique index scoped to active rows only, with the Prisma-side logic adjusted to match.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@prisma/migrations/20260410120000_add_event_waitlist/migration.sql` around
lines 26 - 27, The current unique index EventWaitlistEntry_eventId_userId_key
prevents legitimate re-joins because it enforces uniqueness across all
historical rows; replace it with a partial unique index that only covers active
rows (e.g., unique on eventId,userId WHERE cancelledAt IS NULL AND promotedAt IS
NULL) or alternatively remove the global unique constraint and implement
reactivation of historical rows in the registration flow; update the migration
to drop the existing EventWaitlistEntry_eventId_userId_key and create the scoped
unique index, and adjust Prisma-side logic (registration/reactivation code) to
match the new “one active waitlist row per user/event” semantics.
| if (registration) { | ||
| await prisma.eventRegistration.update({ | ||
| where: { id: registration.id }, | ||
| data: { | ||
| cancelledAt: new Date(), | ||
| }, | ||
| }); | ||
|
|
||
| const promoted = await promoteNextFromWaitlist(eventId); | ||
| if (promoted) { | ||
| await notifyAdmins({ | ||
| type: 'event_waitlist_promoted', | ||
| title: 'Promoción automática desde lista de espera', | ||
| message: `${promoted.userName} obtuvo un cupo en "${event.name}"`, | ||
| metadata: { | ||
| eventId, | ||
| eventName: event.name, | ||
| registrationId: promoted.registrationId, | ||
| userId: promoted.userId, | ||
| userName: promoted.userName, | ||
| userEmail: promoted.userEmail, | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Make cancellation and auto-promotion atomic.
Line 93 commits the cancellation before Line 100 calls promoteNextFromWaitlist(), and that helper runs in a separate transaction. If promotion aborts after the cancellation succeeds, the seat stays freed with nobody promoted, which breaks the auto-promotion contract of this PR. Based on learnings: "The events system handles invalid/non-existent IDs through two layers: 1. Prisma's findUnique returns null for invalid IDs 2. The EventDetailPage component displays a user-friendly message 'No se encontró el evento solicitado' when the event is null".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/actions/events/cancel-registration.ts` around lines 92 - 115, The
cancellation currently calls prisma.eventRegistration.update before calling
promoteNextFromWaitlist(), risking a freed seat without a successful promotion;
change this to perform the cancellation and the waitlist promotion in the same
Prisma transaction (use prisma.$transaction with an async callback or refactor
promoteNextFromWaitlist to accept a transactional Prisma client), run
prisma.eventRegistration.update and the promotion logic using that same
transactional client, and only after the transaction successfully commits call
notifyAdmins for the promoted user (keep function names:
prisma.eventRegistration.update, promoteNextFromWaitlist or a new
promoteNextFromWaitlistTx that accepts a client, and notifyAdmins).
| const promoted = await promoteNextFromWaitlist(registration.eventId); | ||
| if (promoted && event) { | ||
| await notifyAdmins({ | ||
| type: 'event_waitlist_promoted', | ||
| title: 'Promoción automática desde lista de espera', | ||
| message: `${promoted.userName} obtuvo un cupo en "${event.name}"`, | ||
| metadata: { | ||
| eventId: registration.eventId, | ||
| eventName: event.name, | ||
| registrationId: promoted.registrationId, | ||
| userId: promoted.userId, | ||
| userName: promoted.userName, | ||
| userEmail: promoted.userEmail, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Don’t fail the delete flow on notification errors.
At this point the registration is already deleted and the promotion may already be committed. If notifyAdmins() throws, the action returns an error even though the core mutation succeeded.
Suggested fix
const promoted = await promoteNextFromWaitlist(registration.eventId);
if (promoted && event) {
- await notifyAdmins({
- type: 'event_waitlist_promoted',
- title: 'Promoción automática desde lista de espera',
- message: `${promoted.userName} obtuvo un cupo en "${event.name}"`,
- metadata: {
- eventId: registration.eventId,
- eventName: event.name,
- registrationId: promoted.registrationId,
- userId: promoted.userId,
- userName: promoted.userName,
- userEmail: promoted.userEmail,
- },
- });
+ try {
+ await notifyAdmins({
+ type: 'event_waitlist_promoted',
+ title: 'Promoción automática desde lista de espera',
+ message: `${promoted.userName} obtuvo un cupo en "${event.name}"`,
+ metadata: {
+ eventId: registration.eventId,
+ eventName: event.name,
+ registrationId: promoted.registrationId,
+ userId: promoted.userId,
+ userName: promoted.userName,
+ userEmail: promoted.userEmail,
+ },
+ });
+ } catch (error) {
+ console.error('Failed to notify admins about waitlist promotion', error);
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const promoted = await promoteNextFromWaitlist(registration.eventId); | |
| if (promoted && event) { | |
| await notifyAdmins({ | |
| type: 'event_waitlist_promoted', | |
| title: 'Promoción automática desde lista de espera', | |
| message: `${promoted.userName} obtuvo un cupo en "${event.name}"`, | |
| metadata: { | |
| eventId: registration.eventId, | |
| eventName: event.name, | |
| registrationId: promoted.registrationId, | |
| userId: promoted.userId, | |
| userName: promoted.userName, | |
| userEmail: promoted.userEmail, | |
| }, | |
| }); | |
| const promoted = await promoteNextFromWaitlist(registration.eventId); | |
| if (promoted && event) { | |
| try { | |
| await notifyAdmins({ | |
| type: 'event_waitlist_promoted', | |
| title: 'Promoción automática desde lista de espera', | |
| message: `${promoted.userName} obtuvo un cupo en "${event.name}"`, | |
| metadata: { | |
| eventId: registration.eventId, | |
| eventName: event.name, | |
| registrationId: promoted.registrationId, | |
| userId: promoted.userId, | |
| userName: promoted.userName, | |
| userEmail: promoted.userEmail, | |
| }, | |
| }); | |
| } catch (error) { | |
| console.error('Failed to notify admins about waitlist promotion', error); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/actions/events/delete-registration.ts` around lines 44 - 58, The
notifyAdmins call can throw and must not abort the delete flow; wrap the
notifyAdmins(...) invocation (the block handling promoted from
promoteNextFromWaitlist(registration.eventId)) in a try/catch, so any exception
is caught and handled (log the error and continue) instead of rethrowing; ensure
the code still returns success for the deletion path even if notifyAdmins fails,
referencing promoteNextFromWaitlist, promoted, notifyAdmins and event in your
fix.
| if (existingRegistration && existingRegistration.cancelledAt === null) { | ||
| await tx.eventWaitlistEntry.update({ | ||
| where: { id: nextWaitlistEntry.id }, | ||
| data: { | ||
| cancelledAt: new Date(), | ||
| }, | ||
| }); | ||
| return null; |
There was a problem hiding this comment.
Don’t stop promotion after removing a stale waitlist row.
If the first waitlisted user already has an active registration, Lines 130-137 cancel that waitlist entry and return null. When capacity is still available, the next eligible person never gets promoted, so the event can keep an empty seat until some later action retries promotion.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/actions/events/event-capacity.ts` around lines 130 - 137, The code
cancels a stale waitlist entry when existingRegistration is present (within the
block using tx.eventWaitlistEntry.update on nextWaitlistEntry) but then returns
null, which stops processing further waitlist entries and leaves capacity
unfilled; change the control flow so that after marking
nextWaitlistEntry.cancelledAt you do not return but instead continue processing
the loop/logic that promotes the next eligible waitlist entry (i.e., remove the
early return and either continue to the next iteration or let the promotion
logic that follows run), ensuring the rest of the function (promotion code that
reads nextWaitlistEntry and creates a registration) can run for subsequent
entries.
| const position = await tx.eventWaitlistEntry.count({ | ||
| where: { | ||
| eventId, | ||
| cancelledAt: null, | ||
| promotedAt: null, | ||
| createdAt: { | ||
| lte: waitlistCreatedAt, | ||
| }, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Count waitlist position with the same sort key used elsewhere.
getWaitlistPosition() orders by createdAt, id, but this count only uses createdAt <= waitlistCreatedAt. If two entries share the same timestamp, both can get the same returned position, so the immediate response can disagree with the actual FIFO order after reload.
Suggested change
const position = await tx.eventWaitlistEntry.count({
where: {
eventId,
cancelledAt: null,
promotedAt: null,
- createdAt: {
- lte: waitlistCreatedAt,
- },
+ OR: [
+ { createdAt: { lt: waitlistCreatedAt } },
+ {
+ createdAt: waitlistCreatedAt,
+ id: { lte: waitlistId },
+ },
+ ],
},
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const position = await tx.eventWaitlistEntry.count({ | |
| where: { | |
| eventId, | |
| cancelledAt: null, | |
| promotedAt: null, | |
| createdAt: { | |
| lte: waitlistCreatedAt, | |
| }, | |
| }, | |
| }); | |
| const position = await tx.eventWaitlistEntry.count({ | |
| where: { | |
| eventId, | |
| cancelledAt: null, | |
| promotedAt: null, | |
| OR: [ | |
| { createdAt: { lt: waitlistCreatedAt } }, | |
| { | |
| createdAt: waitlistCreatedAt, | |
| id: { lte: waitlistId }, | |
| }, | |
| ], | |
| }, | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/actions/events/register-event.ts` around lines 168 - 177, Count the
waitlist position using the same sort key as getWaitlistPosition by adding an id
tie-breaker to the where clause: instead of only counting entries with createdAt
<= waitlistCreatedAt, count entries where createdAt < waitlistCreatedAt OR
(createdAt = waitlistCreatedAt AND id < = <this entry's id>), so use the current
waitlist entry's id (e.g. waitlistEntryId / id) and waitlistCreatedAt in
tx.eventWaitlistEntry.count to ensure stable FIFO ordering that matches
getWaitlistPosition.
| prisma.eventWaitlistEntry.findMany({ | ||
| where: { | ||
| eventId: id, | ||
| }, | ||
| include: { | ||
| user: true, | ||
| }, | ||
| orderBy: { | ||
| createdAt: 'asc', | ||
| }, | ||
| }), | ||
| ]); | ||
|
|
||
| const activeRegistrations = registrations.filter((r) => r.cancelledAt === null); | ||
| const cancelledRegistrations = registrations.filter((r) => r.cancelledAt !== null); | ||
| const activeWaitlistEntries = waitlistEntries.filter( | ||
| (entry) => entry.cancelledAt === null && entry.promotedAt === null, | ||
| ); |
There was a problem hiding this comment.
Match the admin waitlist order to the promotion order.
The table position is derived from this query order, but promoteNextFromWaitlist() uses createdAt ASC, id ASC. With same-timestamp entries, the admin page can show #1/#2 in a different order than the user who will actually be promoted next.
Suggested fix
prisma.eventWaitlistEntry.findMany({
where: {
eventId: id,
+ cancelledAt: null,
+ promotedAt: null,
},
include: {
user: true,
},
- orderBy: {
- createdAt: 'asc',
- },
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
}),
]);
@@
- const activeWaitlistEntries = waitlistEntries.filter(
- (entry) => entry.cancelledAt === null && entry.promotedAt === null,
- );
+ const activeWaitlistEntries = waitlistEntries;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`(platform)/eventos/[id]/inscripciones/page.tsx around lines 81 - 98,
The admin waitlist ordering differs from the promotion ordering; update the
waitlist query used to build activeWaitlistEntries so it orders identically to
promoteNextFromWaitlist() by using orderBy: [{ createdAt: 'asc' }, { id: 'asc'
}] in the prisma.eventWaitlistEntry.findMany call (keep the same where and
include clauses and the existing cancelledAt/promotedAt filtering), ensuring the
admin table position matches the actual promotion order.
| const [isAutoRegistering, setIsAutoRegistering] = useState(false); | ||
| // Estado local para saber si ya se registró en esta sesión | ||
| const [justRegisteredLocally, setJustRegisteredLocally] = useState(false); | ||
| const [justWaitlistedLocally, setJustWaitlistedLocally] = useState(false); |
There was a problem hiding this comment.
Persist the returned waitlist position in local state.
Both waitlist success paths only flip a boolean. After a successful waitlist join, this component still renders the server prop waitlistPosition, so the new “Tu posición actual…” message stays blank/stale until a refresh.
Suggested change
const [justRegisteredLocally, setJustRegisteredLocally] = useState(false);
const [justWaitlistedLocally, setJustWaitlistedLocally] = useState(false);
+ const [localWaitlistPosition, setLocalWaitlistPosition] = useState(waitlistPosition);
…
if (result.status === 'registered') {
setJustRegisteredLocally(true);
setShowSuccessDialog(true);
setJustWaitlistedLocally(false);
+ setLocalWaitlistPosition(null);
} else {
setJustWaitlistedLocally(true);
+ setLocalWaitlistPosition(result.position);
toast.success('Te sumaste a la lista de espera de este evento.');
}
…
- setJustWaitlistedLocally(true);
+ setJustWaitlistedLocally(true);
+ setLocalWaitlistPosition(result.position);
toast.success('Te sumaste a la lista de espera de este evento.');
…
- {waitlistPosition ? ` Tu posición actual es #${waitlistPosition}.` : ''}
+ {localWaitlistPosition ? ` Tu posición actual es #${localWaitlistPosition}.` : ''}Also applies to: 80-89, 114-124, 163-170
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/events/event-detail-client.tsx` at line 46, The component
currently only flips justWaitlistedLocally and doesn't persist the returned
waitlist position, so the UI keeps showing the stale server prop
waitlistPosition; add a local state like waitlistPositionLocal (useState<number
| null>(null)) and on successful waitlist join in the handlers referenced (the
functions that call setJustWaitlistedLocally at the blocks around lines 46,
~80-89, ~114-124, and ~163-170) set waitlistPositionLocal to the position
returned by the server response; then update rendering to prefer
waitlistPositionLocal if non-null (falling back to props.waitlistPosition) so
the “Tu posición actual…” message updates immediately.
| isAuthenticated: boolean; | ||
| capacityAvailable: boolean; | ||
| onSuccess?: () => void; | ||
| onSuccess?: (status: 'registered' | 'waitlisted') => void; |
There was a problem hiding this comment.
Preserve the waitlist payload in onSuccess.
registerEvent() now returns position for waitlisted users, but this prop only forwards status. That prevents callers from rendering the promised waitlist position without an extra refresh.
Suggested change
- onSuccess?: (status: 'registered' | 'waitlisted') => void;
+ onSuccess?: (result: { status: 'registered'; registrationId: string } | { status: 'waitlisted'; waitlistId: string; position: number }) => void;
…
- onSuccess(result.status);
+ onSuccess(result);Also applies to: 42-42
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/events/register-event-button.tsx` at line 14, Update the
onSuccess prop and its usage to forward the waitlist position returned by
registerEvent: change the prop signature (onSuccess) to accept either a payload
object or an extra optional parameter that includes both status and position
(e.g., { status: 'registered' | 'waitlisted', position?: number }) and, inside
the register-event-button component where registerEvent() is called, pass the
returned position when status === 'waitlisted' (use the position property from
registerEvent's response). Ensure all calls to onSuccess (and its type) are
updated so callers can read position without requiring a refresh.
Resumen
Se implementó soporte de inscripciones fuera de cupo mediante lista de espera para eventos con capacidad limitada, incluyendo promoción automática del siguiente usuario cuando se libera un lugar.
Cambios principales
EventWaitlistEntryen Prisma y su migración.src/actions/events/event-capacity.ts.registerEventpara:cancelRegistrationydeleteRegistrationpara:Inscribirme/Sumarme a lista de espera),Test plan
capacitybaja (ej: 1 o 2).Evidencia (UI)
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes