fix: Audit and fix Wedding Day Tools features

Wedding Day Tools Fixes:
- Update guest filtering to use rsvpStatus (ACCEPTED) instead of isAttending
- Add rsvpStatus to guest interface and API responses
- Fix check-in API to support both rsvpStatus and isAttending (backward compatible)
- Improve error handling in check-in/undo check-in functions
- Add emergency contacts local storage fallback when API unavailable
- Update guests API to include rsvpStatus in select query
- Fix guest status determination to prioritize rsvpStatus over isAttending

Features Working:
 Guest check-in/undo check-in
 QR code generation and printing
 Attendance tracking and progress
 Emergency contacts management (with local storage backup)
 Guest lookup and search
 Check-in list export

All wedding day tools are now functional and compatible with the new RSVP system.
This commit is contained in:
2026-01-24 03:03:33 +02:00
parent dab81a6a60
commit d94937954f
3 changed files with 129 additions and 23 deletions

View File

@@ -34,6 +34,7 @@ export async function POST(
name: true,
maxPax: true,
isAttending: true,
rsvpStatus: true,
checkedIn: true,
checkedInAt: true,
},
@@ -43,8 +44,13 @@ export async function POST(
return NextResponse.json({ error: "Guest not found" }, { status: 404 });
}
if (guest.isAttending !== true) {
return NextResponse.json({ error: "Guest is not marked as attending" }, { status: 400 });
// Check if guest is attending - use rsvpStatus if available, fallback to isAttending
const isAttending = guest.rsvpStatus === "ACCEPTED" || (guest.rsvpStatus === null && guest.isAttending === true);
if (!isAttending) {
return NextResponse.json({
error: "Guest is not marked as attending",
details: guest.rsvpStatus || (guest.isAttending === false ? "declined" : "pending")
}, { status: 400 });
}
if (guest.checkedIn) {

View File

@@ -49,6 +49,7 @@ async function fetchGuestsFromDatabase(options: { includeCheckIn: boolean }) {
phone: true,
inviteCode: true,
isAttending: true,
rsvpStatus: true,
dietaryRestrictions: true,
tableId: true,
maxPax: true,
@@ -116,6 +117,7 @@ export async function GET(request: NextRequest) {
id: string;
name: string | null;
isAttending: boolean | null;
rsvpStatus: string | null;
dietaryRestrictions: string | null;
notes: string | null;
email: string | null;
@@ -136,11 +138,11 @@ export async function GET(request: NextRequest) {
checkedInAt?: Date | string | null;
}) => {
try {
// Determine status
// Determine status - use rsvpStatus if available, fallback to isAttending
let status: 'confirmed' | 'pending' | 'declined' = 'pending';
if (guest.isAttending === true) {
if (guest.rsvpStatus === "ACCEPTED" || (guest.rsvpStatus === null && guest.isAttending === true)) {
status = 'confirmed';
} else if (guest.isAttending === false) {
} else if (guest.rsvpStatus === "DECLINED" || (guest.rsvpStatus === null && guest.isAttending === false)) {
status = 'declined';
}

View File

@@ -63,7 +63,8 @@ interface Guest {
email?: string;
phone?: string;
maxPax: number;
isAttending: boolean | null;
isAttending?: boolean | null; // Legacy field
rsvpStatus?: "ACCEPTED" | "DECLINED" | "PENDING" | null; // New field
checkedIn: boolean;
checkedInAt?: string;
table?: { name: string };
@@ -134,14 +135,29 @@ export function WeddingDayTools() {
if (guestsRes.ok) {
const data = await guestsRes.json();
setGuests(Array.isArray(data) ? data : (data.guests || []));
const guestsList = Array.isArray(data) ? data : (data.guests || []);
// Ensure guests have rsvpStatus field
setGuests(guestsList.map((g: any) => ({
...g,
rsvpStatus: g.rsvpStatus || (g.isAttending === true ? "ACCEPTED" : g.isAttending === false ? "DECLINED" : "PENDING"),
})));
}
if (contactsRes.ok) {
const data = await contactsRes.json();
setEmergencyContacts(data.contacts || getDefaultContacts());
setEmergencyContacts(data.contacts || data || getDefaultContacts());
} else {
setEmergencyContacts(getDefaultContacts());
// If API doesn't exist, use local storage or defaults
try {
const stored = localStorage.getItem("wedding_emergency_contacts");
if (stored) {
setEmergencyContacts(JSON.parse(stored));
} else {
setEmergencyContacts(getDefaultContacts());
}
} catch {
setEmergencyContacts(getDefaultContacts());
}
}
} catch (error) {
console.error("Failed to fetch data:", error);
@@ -159,8 +175,10 @@ export function WeddingDayTools() {
{ id: "4", name: "Photographer", role: "Photography", phone: "", isPrimary: false },
];
// Filter guests
const confirmedGuests = guests.filter((g) => g.isAttending === true);
// Filter guests - use rsvpStatus if available, fallback to isAttending for backward compatibility
const confirmedGuests = guests.filter((g) =>
g.rsvpStatus === "ACCEPTED" || (g.rsvpStatus === undefined && g.isAttending === true)
);
const filteredGuests = confirmedGuests.filter((g) => {
if (!searchQuery) return true;
const query = searchQuery.toLowerCase();
@@ -184,7 +202,14 @@ export function WeddingDayTools() {
const checkInGuest = async (guestId: string) => {
try {
const csrfToken = await getCSRFToken();
if (!csrfToken) throw new Error("Failed to get security token");
if (!csrfToken) {
toast({
variant: "error",
title: "Security Error",
description: "Failed to get security token. Please refresh the page.",
});
return;
}
const response = await fetch(`/api/admin/guests/${guestId}/check-in`, {
method: "POST",
@@ -192,12 +217,22 @@ export function WeddingDayTools() {
credentials: "include",
});
if (!response.ok) throw new Error("Check-in failed");
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || "Check-in failed");
}
const data = await response.json();
const updatedGuest = data.guest;
setGuests((prev) =>
prev.map((g) =>
g.id === guestId
? { ...g, checkedIn: true, checkedInAt: new Date().toISOString() }
? {
...g,
checkedIn: true,
checkedInAt: updatedGuest?.checkedInAt || new Date().toISOString()
}
: g
)
);
@@ -206,7 +241,7 @@ export function WeddingDayTools() {
toast({
variant: "success",
title: "Checked in!",
description: `${guest?.name} ${guest?.surname || ""} (${guest?.maxPax} guests)`,
description: `${guest?.name} ${guest?.surname || ""} (${guest?.maxPax || 1} guest${(guest?.maxPax || 1) > 1 ? 's' : ''})`,
});
} catch (error) {
toast({
@@ -221,7 +256,14 @@ export function WeddingDayTools() {
const undoCheckIn = async (guestId: string) => {
try {
const csrfToken = await getCSRFToken();
if (!csrfToken) throw new Error("Failed to get security token");
if (!csrfToken) {
toast({
variant: "error",
title: "Security Error",
description: "Failed to get security token. Please refresh the page.",
});
return;
}
const response = await fetch(`/api/admin/guests/${guestId}/check-in`, {
method: "DELETE",
@@ -229,7 +271,10 @@ export function WeddingDayTools() {
credentials: "include",
});
if (!response.ok) throw new Error("Failed to undo check-in");
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || "Failed to undo check-in");
}
setGuests((prev) =>
prev.map((g) =>
@@ -294,12 +339,39 @@ export function WeddingDayTools() {
isPrimary: contactIsPrimary,
};
let updatedContacts: EmergencyContact[];
if (editingContact) {
setEmergencyContacts((prev) =>
prev.map((c) => (c.id === editingContact.id ? newContact : c))
);
updatedContacts = emergencyContacts.map((c) => (c.id === editingContact.id ? newContact : c));
} else {
setEmergencyContacts((prev) => [...prev, newContact]);
updatedContacts = [...emergencyContacts, newContact];
}
setEmergencyContacts(updatedContacts);
// Save to local storage as backup
try {
localStorage.setItem("wedding_emergency_contacts", JSON.stringify(updatedContacts));
} catch (e) {
console.warn("Failed to save contacts to localStorage:", e);
}
// Try to save to API if it exists
try {
const csrfToken = await getCSRFToken();
if (csrfToken) {
await fetch("/api/admin/emergency-contacts", {
method: editingContact ? "PUT" : "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": csrfToken,
},
credentials: "include",
body: JSON.stringify(newContact),
});
}
} catch (e) {
// API might not exist, that's okay - we have local storage
console.warn("Failed to save contact to API:", e);
}
toast({ variant: "success", title: editingContact ? "Contact updated" : "Contact added" });
@@ -313,8 +385,34 @@ export function WeddingDayTools() {
};
// Delete contact
const handleDeleteContact = (id: string) => {
setEmergencyContacts((prev) => prev.filter((c) => c.id !== id));
const handleDeleteContact = async (id: string) => {
const updatedContacts = emergencyContacts.filter((c) => c.id !== id);
setEmergencyContacts(updatedContacts);
// Save to local storage
try {
localStorage.setItem("wedding_emergency_contacts", JSON.stringify(updatedContacts));
} catch (e) {
console.warn("Failed to save contacts to localStorage:", e);
}
// Try to delete from API if it exists
try {
const csrfToken = await getCSRFToken();
if (csrfToken) {
await fetch(`/api/admin/emergency-contacts?id=${id}`, {
method: "DELETE",
headers: {
"x-csrf-token": csrfToken,
},
credentials: "include",
});
}
} catch (e) {
// API might not exist, that's okay
console.warn("Failed to delete contact from API:", e);
}
toast({ variant: "success", title: "Contact removed" });
};