diff --git a/src/app/api/admin/rsvp/on-behalf/route.ts b/src/app/api/admin/rsvp/on-behalf/route.ts index 06ffc52..66b12bf 100644 --- a/src/app/api/admin/rsvp/on-behalf/route.ts +++ b/src/app/api/admin/rsvp/on-behalf/route.ts @@ -71,11 +71,15 @@ export async function POST(request: NextRequest) { // Build update data const updateData: { isAttending: boolean; + rsvpStatus: 'ACCEPTED' | 'DECLINED'; + rsvpCompletedAt?: Date; dietaryRestrictions?: string | null; notes?: string | null; rsvpTimestamp?: Date; } = { isAttending: attending, + rsvpStatus: attending ? 'ACCEPTED' : 'DECLINED', + rsvpCompletedAt: new Date(), rsvpTimestamp: new Date(), }; @@ -119,6 +123,26 @@ export async function POST(request: NextRequest) { invalidateCacheByTag(CACHE_TAGS.RSVP), ]); + // Emit real-time event for admin dashboard updates + try { + const { RealtimeManager } = await import('@/lib/realtime'); + await RealtimeManager.sendEvent({ + type: 'rsvp', + action: 'update', + data: { + guestId: updatedGuest.id, + guestName: updatedGuest.name, + isAttending: attending, + rsvpStatus: attending ? 'ACCEPTED' : 'DECLINED', + rsvpTimestamp: updatedGuest.rsvpTimestamp?.toISOString(), + }, + timestamp: new Date().toISOString(), + }); + } catch (error) { + // Don't fail the RSVP if real-time event fails + console.error('Failed to emit real-time RSVP event:', error); + } + logger.info('Admin submitted RSVP on behalf of guest', { requestId, adminId: session.adminId, diff --git a/src/app/api/rsvp/submit/route.ts b/src/app/api/rsvp/submit/route.ts index 86f5e51..8fe49f7 100644 --- a/src/app/api/rsvp/submit/route.ts +++ b/src/app/api/rsvp/submit/route.ts @@ -532,7 +532,10 @@ export async function POST(request: NextRequest) { updatedGuest = await prisma.guest.update({ where: { id: guest.id }, data: { + // Update both legacy and new RSVP status fields isAttending: attending, + rsvpStatus: attending ? 'ACCEPTED' : 'DECLINED', + rsvpCompletedAt: new Date(), // Mark RSVP as completed dietaryRestrictions: sanitizedDietary, notes: sanitizedNotes || guest.notes, rsvpTimestamp: new Date(), @@ -560,19 +563,39 @@ export async function POST(request: NextRequest) { }); } catch (error) { // Fallback: update only legacy/core columns that exist in older schemas. - updatedGuest = await prisma.guest.update({ - where: { id: guest.id }, - data: { - isAttending: attending, - rsvpTimestamp: new Date(), - confirmationCode, - dietaryRestrictions: sanitizedDietary, - notes: sanitizedNotes || null, - phone: phone || guest.phone || null, - email: email || guest.email || null, - }, - select: updateSelect, - }); + // Try to include rsvpStatus if possible, but don't fail if it doesn't exist + try { + updatedGuest = await prisma.guest.update({ + where: { id: guest.id }, + data: { + isAttending: attending, + rsvpStatus: attending ? 'ACCEPTED' : 'DECLINED', + rsvpCompletedAt: new Date(), + rsvpTimestamp: new Date(), + confirmationCode, + dietaryRestrictions: sanitizedDietary, + notes: sanitizedNotes || null, + phone: phone || guest.phone || null, + email: email || guest.email || null, + }, + select: updateSelect, + }); + } catch (fallbackError) { + // If rsvpStatus field doesn't exist, fall back to legacy-only update + updatedGuest = await prisma.guest.update({ + where: { id: guest.id }, + data: { + isAttending: attending, + rsvpTimestamp: new Date(), + confirmationCode, + dietaryRestrictions: sanitizedDietary, + notes: sanitizedNotes || null, + phone: phone || guest.phone || null, + email: email || guest.email || null, + }, + select: updateSelect, + }); + } // Keep original error in logs, but don't fail the RSVP. logger.warn("RSVP submit: fell back to legacy guest update (schema drift)", { requestId, @@ -798,6 +821,7 @@ export async function POST(request: NextRequest) { guestId: updatedGuest.id, guestName: updatedGuest.name, isAttending: attending, + rsvpStatus: attending ? 'ACCEPTED' : 'DECLINED', // Include rsvpStatus for proper syncing rsvpTimestamp: updatedGuest.rsvpTimestamp?.toISOString(), dietaryRestrictions: sanitizedDietary, socialOptIn: socialOptIn || false, diff --git a/src/app/api/rsvp/update/route.ts b/src/app/api/rsvp/update/route.ts index 86be80d..bd76f0f 100644 --- a/src/app/api/rsvp/update/route.ts +++ b/src/app/api/rsvp/update/route.ts @@ -145,7 +145,27 @@ export async function PATCH(request: NextRequest) { // Invalidate specific guest cache if possible ]); - // 6. Audit Log + // 6. Emit real-time event for admin dashboard updates + try { + const { RealtimeManager } = await import('@/lib/realtime'); + await RealtimeManager.sendEvent({ + type: 'rsvp', + action: 'update', + data: { + guestId: updatedGuest.id, + guestName: updatedGuest.name || guest.name, + isAttending: updateData.isAttending ?? updatedGuest.isAttending, + rsvpStatus: updateData.rsvpStatus || updatedGuest.rsvpStatus || 'PENDING', + rsvpTimestamp: updatedGuest.rsvpTimestamp?.toISOString(), + }, + timestamp: new Date().toISOString(), + }); + } catch (error) { + // Don't fail the RSVP update if real-time event fails + console.error('Failed to emit real-time RSVP update event:', error); + } + + // 7. Audit Log audit.dataAccessed('guest', 'rsvp', 'update', guest.id); return NextResponse.json({ diff --git a/src/app/wedding/page.tsx b/src/app/wedding/page.tsx index a874b5e..d85d268 100644 --- a/src/app/wedding/page.tsx +++ b/src/app/wedding/page.tsx @@ -1521,6 +1521,7 @@ export default function WeddingInfoHub() { const navSections = [ { id: "home", label: "Home", icon: Heart }, + { id: "gifts", label: "Gifts", icon: Gift }, { id: "story", label: "Story", icon: BookOpen }, // Act II - The Plan { id: "itinerary", label: "Schedule", icon: Clock }, @@ -1536,7 +1537,6 @@ export default function WeddingInfoHub() { { id: "guestbook", label: "Guestbook", icon: MessageCircle }, { id: "tribute", label: "Tributes", icon: Flame }, // Act VI - Extras - { id: "gifts", label: "Gifts", icon: Gift }, { id: "faq", label: "FAQ", icon: HelpCircle }, ] as const; @@ -1742,10 +1742,10 @@ export default function WeddingInfoHub() { {/* Explore Scroll Hint */} { - const storySection = document.getElementById("story"); - if (storySection) { + const giftsSection = document.getElementById("gifts"); + if (giftsSection) { const offset = 80; // Account for floating nav - const elementTop = storySection.getBoundingClientRect().top + window.pageYOffset; + const elementTop = giftsSection.getBoundingClientRect().top + window.pageYOffset; const offsetPosition = elementTop - offset; window.scrollTo({ top: Math.max(0, offsetPosition), @@ -1778,6 +1778,45 @@ export default function WeddingInfoHub() { }} /> + {/* GIFTS with narrative - Moved before story for easy access */} +
+ + + {/* Gifts narrative */} + +

+ "Your presence at our celebration is the greatest gift we could ask for. + As we begin our life together, a contribution to our wishing well would + help us build our dreams." +

+

+ For friends and family who may not be able to attend, we've made our gift registries + and wishing well details available below. Your love and support mean the world to us. +

+
+ + {/* Wishing Well Component - Unlocked for everyone */} + + + +
+ {/* STORY with enhanced narrative storytelling */}
- {/* GIFTS with narrative */} -
- - - {/* Gifts narrative */} - -

- "Your presence at our celebration is the greatest gift we could ask for. - As we begin our life together, a contribution to our wishing well would - help us build our dreams." -

-

- For friends and family who may not be able to attend, we've made our gift registries - and wishing well details available below. Your love and support mean the world to us. -

-
- - {/* Wishing Well Component - Unlocked for everyone */} - - - -
- {/* FAQ - Premium Enhanced */}
diff --git a/src/components/features/admin/admin-dashboard.tsx b/src/components/features/admin/admin-dashboard.tsx index 6b090e5..906cf11 100644 --- a/src/components/features/admin/admin-dashboard.tsx +++ b/src/components/features/admin/admin-dashboard.tsx @@ -678,32 +678,51 @@ export function AdminDashboard({ useAdminRealtime({ onRSVPUpdate: (event) => { if (event.action === "create" || event.action === "update") { - // Refresh RSVP data with error handling - fetch('/api/admin/rsvp', { credentials: 'include' }) - .then(async res => { - if (!res.ok) { - if (res.status === 401) { - console.warn('[AdminDashboard] Unauthorized for RSVP fetch'); - return; + // Refresh both RSVP data and guest list to sync status + Promise.all([ + // Refresh RSVP data + fetch('/api/admin/rsvp', { credentials: 'include' }) + .then(async res => { + if (!res.ok) { + if (res.status === 401) { + console.warn('[AdminDashboard] Unauthorized for RSVP fetch'); + return null; + } + console.warn(`[AdminDashboard] RSVP fetch returned ${res.status}`); + return null; } - console.warn(`[AdminDashboard] RSVP fetch returned ${res.status}`); - return; - } - return res.json(); - }) - .then(data => { - if (data && data.success) { - setRsvpData(data); - toast({ - title: "New RSVP", - description: "RSVP data has been updated", - variant: "success", - }); - } - }) - .catch(error => { - console.warn('[AdminDashboard] Failed to refresh RSVP data:', error); - }); + return res.json(); + }) + .then(data => { + if (data && data.success) { + setRsvpData(data); + } + }) + .catch(error => { + console.warn('[AdminDashboard] Failed to refresh RSVP data:', error); + }), + // Refresh guest list to sync RSVP status - CRITICAL for Master Guest List sync + refreshGuestList(), + ]).then(() => { + // Only show toast for create actions to avoid spam on updates + if (event.action === "create") { + toast({ + title: "New RSVP", + description: "Guest list and RSVP data have been refreshed", + variant: "success", + }); + } + }).catch(error => { + console.warn('[AdminDashboard] Failed to refresh data after RSVP update:', error); + }); + } + }, + onGuestUpdate: (event) => { + // Also refresh guest list when guest data is updated directly + if (event.action === "update") { + refreshGuestList().catch(error => { + console.warn('[AdminDashboard] Failed to refresh guest list after guest update:', error); + }); } }, onCheckinUpdate: (event) => {