diff --git a/frontend/src/app/(main)/dashboard/events/[slug]/page.tsx b/frontend/src/app/(main)/dashboard/events/[slug]/page.tsx index 1caa0c1..cee8b8a 100644 --- a/frontend/src/app/(main)/dashboard/events/[slug]/page.tsx +++ b/frontend/src/app/(main)/dashboard/events/[slug]/page.tsx @@ -26,6 +26,7 @@ import { } from "lucide-react"; import { useEventThemes } from "@/context/event-theme-context"; import { getServerFileUrl } from "@/lib/utils"; +import GuestsList from "@/components/guests/guests-list"; export default function EventDetailPage() { const { slug } = useParams<{ slug: string }>(); @@ -87,7 +88,7 @@ export default function EventDetailPage() { } return ( -
+

{event.title}

@@ -347,6 +348,7 @@ export default function EventDetailPage() { )}
+
); } diff --git a/frontend/src/components/guests/guests-list.tsx b/frontend/src/components/guests/guests-list.tsx index 52ac338..4e0198b 100644 --- a/frontend/src/components/guests/guests-list.tsx +++ b/frontend/src/components/guests/guests-list.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Table, TableBody, @@ -11,6 +11,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; @@ -18,142 +19,461 @@ 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 { MoreHorizontal, Plus, Search, Filter, Send, Copy } from "lucide-react"; +import { + AlertTriangle, + Copy, + Download, + Filter, + MoreHorizontal, + Plus, + Search, + Send, +} from "lucide-react"; +import { useGuests } from "@/context/guest-context"; +import { + EventResponse, + EventThemeResponse, + 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"; -const GuestListTable = () => { - const [open, setOpen] = useState(false); +// 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); - // Mock data - const guests = [ - { - id: "1", - fullName: "John Smith", - email: "john.smith@example.com", - phone: "+1 555-123-4567", - invitationCode: "JSMITH21", - status: "CONFIRMED", - additionalGuests: 2, - }, - { - id: "2", - fullName: "Emma Johnson", - email: "emma.j@example.com", - phone: "+1 555-987-6543", - invitationCode: "EJOHN45", - status: "INVITED", - additionalGuests: 0, - }, - { - id: "3", - fullName: "Michael Brown", - email: "mbrown@example.com", - phone: "+1 555-555-5555", - invitationCode: "MBROWN3", - status: "DECLINED", - additionalGuests: 0, - }, - { - id: "4", - fullName: "Olivia Davis", - email: "olivia.d@example.com", - phone: "+1 555-111-2222", - invitationCode: "ODAVIS7", - status: "PENDING", - additionalGuests: 1, - }, - { - id: "5", - fullName: "William Wilson", - email: "will.w@example.com", - phone: "+1 555-333-4444", - invitationCode: "WWILSON", - status: "CONFIRMED", - additionalGuests: 3, - }, - ]; + // Form state + const initialState = { + full_name: "", + email: "", + phone: "", + max_additional_guests: 0, + dietary_restrictions: "", + notes: "", + can_bring_guests: true, + }; + const [formData, setFormData] = + useState>(initialState); - const getStatusBadge = (status) => { - const statusStyles = { - INVITED: "bg-blue-100 text-blue-800", - PENDING: "bg-yellow-100 text-yellow-800", - CONFIRMED: "bg-green-100 text-green-800", - DECLINED: "bg-red-100 text-red-800", - WAITLISTED: "bg-purple-100 text-purple-800", - CANCELLED: "bg-gray-100 text-gray-800", - }; + // Access guest context + const { + guests, + isLoadingGuests, + error, + createGuest, + updateGuest, + deleteGuest, + refetchGuests, + currentGuestId, + setCurrentGuestId, + } = useGuests(); + const { user } = useAuth(); - return {status}; + // 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 })); }; - const filteredGuests = guests.filter( - (guest) => - guest.fullName.toLowerCase().includes(searchQuery.toLowerCase()) || - guest.email.toLowerCase().includes(searchQuery.toLowerCase()) || - guest.invitationCode.toLowerCase().includes(searchQuery.toLowerCase()), - ); + // 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); + setCurrentGuestId(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) => { + navigator.clipboard.writeText(text); + toast("Invitation code copied to clipboard"); + }; + + // 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(() => { + setFormData(currentGuest || initialState); + }, [currentGuest, addGuestOpen, editGuestOpen]); return (
+ {error && ( +
+ + Error loading guests: {error.message} +
+ )} +

Guest List

- - - - - - - Add New Guest - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- +
+ + + -
- -
+ + + + Add New Guest + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +