import React, { useEffect, useState } from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { AlertTriangle, Copy, Download, Filter, MoreHorizontal, Plus, Search, Send, } from "lucide-react"; import { useGuests } from "@/context/guest-context"; import { EventResponse, GuestCreate, GuestRead, GuestStatus, GuestUpdate, } from "@/client/types.gen"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { toast } from "sonner"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { useAuth } from "@/context/auth-context"; import { generateInviteLink } from "@/lib/utils"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { StickyNote, Utensils } from "lucide-react"; // Helper to generate a random invitation code const generateInvitationCode = (fullName: string): string => { const namePart = fullName .replace(/[^A-Za-z]/g, "") .substring(0, 6) .toUpperCase(); const randomPart = Math.floor(Math.random() * 1000) .toString() .padStart(5, "0"); return `${namePart}${randomPart}`; }; type GuestListTableProps = { event: EventResponse; }; const GuestListTable = ({ event }: GuestListTableProps) => { // State const [addGuestOpen, setAddGuestOpen] = useState(false); const [editGuestOpen, setEditGuestOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [currentGuest, setCurrentGuest] = useState(null); // Form state const initialState = { full_name: "", email: "", phone: "", max_additional_guests: 0, dietary_restrictions: "", notes: "", can_bring_guests: true, }; const [formData, setFormData] = useState>(initialState); // Access guest context const { guests, isLoadingGuests, error, createGuest, updateGuest, deleteGuest, refetchGuests, currentGuestId, setCurrentGuestId, } = useGuests(); const { user } = useAuth(); // Filter guests by search query const filteredGuests = guests?.filter( (guest) => guest.full_name.toLowerCase().includes(searchQuery.toLowerCase()) || (guest.email?.toLowerCase() || "").includes( searchQuery.toLowerCase(), ) || guest.invitation_code.toLowerCase().includes(searchQuery.toLowerCase()), ) || []; // Handle form input changes const handleInputChange = ( e: React.ChangeEvent, ) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); }; // Handle select changes const handleSelectChange = ( name: string, value: string | number | boolean, ) => { setFormData((prev) => ({ ...prev, [name]: value })); }; // Handle add guest form submission const handleAddGuest = async () => { try { if (!formData.full_name) { toast.error("Full name is required"); return; } // Get the event ID from the first guest or use a default // In a real implementation, you would probably pass this as a prop const eventId = event.id; // This should be replaced with the actual event ID // Get the current user ID // In a real implementation, you would get this from an auth context const invitedBy = user?.id || "admin"; // This should be replaced with the current user ID const newGuest: GuestCreate = { event_id: eventId, invited_by: invitedBy, full_name: formData.full_name || "", email: formData.email || null, phone: formData.phone || null, max_additional_guests: typeof formData.max_additional_guests === "number" ? formData.max_additional_guests : 0, dietary_restrictions: formData.dietary_restrictions || null, notes: formData.notes || null, can_bring_guests: formData.can_bring_guests || true, invitation_code: generateInvitationCode(formData.full_name || ""), }; await createGuest(newGuest); toast.success("Guest added successfully"); setAddGuestOpen(false); setFormData(initialState); } catch (error) { console.error("Error adding guest:", error); toast.error("Failed to add guest"); } }; // Handle edit guest const handleEditGuest = async () => { try { if (!currentGuest) return; const updatedGuest: GuestUpdate = { full_name: formData.full_name || null, email: formData.email || null, phone: formData.phone || null, max_additional_guests: typeof formData.max_additional_guests === "number" ? formData.max_additional_guests : null, dietary_restrictions: formData.dietary_restrictions || null, notes: formData.notes || null, can_bring_guests: formData.can_bring_guests || null, }; await updateGuest(currentGuest.id, updatedGuest); toast.success("Guest updated successfully"); setEditGuestOpen(false); } catch (error) { console.error("Error updating guest:", error); toast.error("Failed to update guest"); } }; // Handle delete guest const handleDeleteGuest = async () => { try { if (!currentGuest) return; await deleteGuest(currentGuest.id); setFormData(initialState); setCurrentGuest(null); toast.success("Guest deleted successfully"); setDeleteDialogOpen(false); } catch (error) { console.error("Error deleting guest:", error); toast.error("Failed to delete guest"); } }; // Prepare to edit a guest const prepareEditGuest = (guest: GuestRead) => { setCurrentGuest(guest); setFormData({ full_name: guest.full_name, email: guest.email || "", phone: guest.phone || "", max_additional_guests: guest.max_additional_guests || 0, dietary_restrictions: guest.dietary_restrictions || "", notes: guest.notes || "", can_bring_guests: guest.can_bring_guests || false, }); setEditGuestOpen(true); }; // Prepare to delete a guest const prepareDeleteGuest = (guest: GuestRead) => { setCurrentGuest(guest); setDeleteDialogOpen(true); }; // Helper to get status badge const getStatusBadge = (status: GuestStatus) => { const statusStyles: Record = { [GuestStatus.INVITED]: "bg-blue-100 text-blue-800", [GuestStatus.PENDING]: "bg-yellow-100 text-yellow-800", [GuestStatus.CONFIRMED]: "bg-green-100 text-green-800", [GuestStatus.DECLINED]: "bg-red-100 text-red-800", [GuestStatus.WAITLISTED]: "bg-purple-100 text-purple-800", [GuestStatus.CANCELLED]: "bg-gray-100 text-gray-800", }; return ( {status.toUpperCase()} ); }; // Copy invitation code to clipboard const copyToClipboard = (text: string, message: string) => { navigator.clipboard.writeText(text); toast(message); }; // Export guest list to CSV const exportToCSV = () => { if (!guests || guests.length === 0) return; const headers = [ "Name", "Email", "Phone", "Invitation Code", "Status", "Additional Guests", ]; const csvContent = [ headers.join(","), ...guests.map((guest) => [ `"${guest.full_name}"`, `"${guest.email || ""}"`, `"${guest.phone || ""}"`, `"${guest.invitation_code}"`, `"${guest.status}"`, guest.max_additional_guests || 0, ].join(","), ), ].join("\n"); const blob = new Blob([csvContent], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.setAttribute("href", url); a.setAttribute("download", "guest-list.csv"); a.click(); }; // Send invitations (stub - in a real app, this would integrate with your email service) const sendInvitations = () => { toast("Sending Invitations", { description: "This feature would send invitations to all uninvited guests", action: { label: "Cancel", onClick: () => console.log("Cancelled sending invitations"), }, }); }; useEffect(() => { // When add dialog opens, always reset to initial state if (addGuestOpen) { setFormData(initialState); setCurrentGuest(null); } // When edit dialog opens, form data should reflect the current guest else if (editGuestOpen && currentGuest) { setFormData({ full_name: currentGuest.full_name, email: currentGuest.email || "", phone: currentGuest.phone || "", max_additional_guests: currentGuest.max_additional_guests || 0, dietary_restrictions: currentGuest.dietary_restrictions || "", notes: currentGuest.notes || "", can_bring_guests: currentGuest.can_bring_guests || false, }); } }, [addGuestOpen, editGuestOpen, currentGuest]); return (
{error && (
Error loading guests: {error.message}
)}

Guest List

Add New Guest