Enhance guest management features in GuestListTable
Added functionality for adding, editing, deleting, and copying guests' details, along with filtering, exporting, and sending invitations. Improved user interactions with dialog handling and introduced error handling for better usability. Integrated GuestsList into the event detail page.
This commit is contained in:
@@ -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 (
|
||||
<div>
|
||||
<div className="space-y-8">
|
||||
<header className="mb-8 flex justify-between items-start">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tight">{event.title}</h1>
|
||||
@@ -347,6 +348,7 @@ export default function EventDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<GuestsList event={event} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<GuestRead | null>(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<Partial<GuestCreate & GuestUpdate>>(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 <Badge className={statusStyles[status]}>{status}</Badge>;
|
||||
// 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<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
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, string> = {
|
||||
[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 (
|
||||
<Badge className={statusStyles[status]}>{status.toUpperCase()}</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<div className="space-y-4 w-full">
|
||||
{error && (
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative flex items-center">
|
||||
<AlertTriangle className="h-5 w-5 mr-2" />
|
||||
<span>Error loading guests: {error.message}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">Guest List</h2>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
<Plus className="mr-2 h-4 w-4" /> Add Guest
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Guest</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Full Name
|
||||
</Label>
|
||||
<Input id="name" className="col-span-3" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="email" className="text-right">
|
||||
Email
|
||||
</Label>
|
||||
<Input id="email" type="email" className="col-span-3" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="phone" className="text-right">
|
||||
Phone
|
||||
</Label>
|
||||
<Input id="phone" className="col-span-3" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="additionalGuests" className="text-right">
|
||||
Additional Guests
|
||||
</Label>
|
||||
<Input
|
||||
id="additionalGuests"
|
||||
type="number"
|
||||
min="0"
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={exportToCSV}>
|
||||
<Download className="mr-2 h-4 w-4" /> Export
|
||||
</Button>
|
||||
<Dialog open={addGuestOpen} onOpenChange={setAddGuestOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
Add Guest
|
||||
<Plus className="mr-2 h-4 w-4" /> Add Guest
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Guest</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="full_name" className="text-right">
|
||||
Full Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="full_name"
|
||||
name="full_name"
|
||||
value={formData.full_name || ""}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="email" className="text-right">
|
||||
Email
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={formData.email || ""}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="phone" className="text-right">
|
||||
Phone
|
||||
</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.phone || ""}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="max_additional_guests" className="text-right">
|
||||
Additional Guests
|
||||
</Label>
|
||||
<Input
|
||||
id="max_additional_guests"
|
||||
name="max_additional_guests"
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.max_additional_guests || 0}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-start gap-4">
|
||||
<Label
|
||||
htmlFor="dietary_restrictions"
|
||||
className="text-right pt-2"
|
||||
>
|
||||
Dietary Restrictions
|
||||
</Label>
|
||||
<Textarea
|
||||
id="dietary_restrictions"
|
||||
name="dietary_restrictions"
|
||||
value={formData.dietary_restrictions || ""}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-start gap-4">
|
||||
<Label htmlFor="notes" className="text-right pt-2">
|
||||
Notes
|
||||
</Label>
|
||||
<Textarea
|
||||
id="notes"
|
||||
name="notes"
|
||||
value={formData.notes || ""}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="can_bring_guests" className="text-right">
|
||||
Can Bring Guests
|
||||
</Label>
|
||||
<Select
|
||||
value={
|
||||
formData.can_bring_guests !== undefined
|
||||
? String(formData.can_bring_guests)
|
||||
: "true"
|
||||
}
|
||||
onValueChange={(value: string) =>
|
||||
handleSelectChange("can_bring_guests", value === "true")
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">Yes</SelectItem>
|
||||
<SelectItem value="false">No</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setAddGuestOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
onClick={handleAddGuest}
|
||||
>
|
||||
Add Guest
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -170,7 +490,7 @@ const GuestListTable = () => {
|
||||
<Button variant="outline" size="sm">
|
||||
<Filter className="mr-2 h-4 w-4" /> Filter
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline" size="sm" onClick={sendInvitations}>
|
||||
<Send className="mr-2 h-4 w-4" /> Send Invites
|
||||
</Button>
|
||||
</div>
|
||||
@@ -190,54 +510,239 @@ const GuestListTable = () => {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredGuests.map((guest) => (
|
||||
<TableRow key={guest.id}>
|
||||
<TableCell className="font-medium">{guest.fullName}</TableCell>
|
||||
<TableCell>{guest.email}</TableCell>
|
||||
<TableCell>{guest.phone}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
{guest.invitationCode}
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(guest.status)}</TableCell>
|
||||
<TableCell>{guest.additionalGuests}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Resend Invitation</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-red-600">
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{isLoadingGuests ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-8">
|
||||
Loading guests...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
) : filteredGuests.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-8">
|
||||
No guests found. Add your first guest!
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredGuests.map((guest) => (
|
||||
<TableRow key={guest.id}>
|
||||
<TableCell className="font-medium">
|
||||
{guest.full_name}
|
||||
</TableCell>
|
||||
<TableCell>{guest.email || "-"}</TableCell>
|
||||
<TableCell>{guest.phone || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
{guest.invitation_code}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => copyToClipboard(guest.invitation_code)}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(guest.status)}</TableCell>
|
||||
<TableCell>{guest.max_additional_guests || 0}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => prepareEditGuest(guest)}
|
||||
>
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => copyToClipboard(guest.invitation_code)}
|
||||
>
|
||||
Copy Invitation Code
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>Resend Invitation</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-red-600"
|
||||
onClick={() => prepareDeleteGuest(guest)}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-sm text-gray-500">
|
||||
<div>
|
||||
Showing {filteredGuests.length} of {guests.length} guests
|
||||
Showing {filteredGuests.length} of {guests?.length || 0} guests
|
||||
</div>
|
||||
<div>
|
||||
Total Confirmed:{" "}
|
||||
{guests.filter((g) => g.status === "CONFIRMED").length} | Total
|
||||
Additional Guests:{" "}
|
||||
{guests.reduce((acc, g) => acc + g.additionalGuests, 0)}
|
||||
{guests?.filter((g) => g.status === GuestStatus.CONFIRMED).length ||
|
||||
0}{" "}
|
||||
| Total Additional Guests:{" "}
|
||||
{guests?.reduce(
|
||||
(acc, g) => acc + (g.max_additional_guests || 0),
|
||||
0,
|
||||
) || 0}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit Guest Dialog */}
|
||||
<Dialog open={editGuestOpen} onOpenChange={setEditGuestOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Guest</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="edit_full_name" className="text-right">
|
||||
Full Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="edit_full_name"
|
||||
name="full_name"
|
||||
value={formData.full_name || ""}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="edit_email" className="text-right">
|
||||
Email
|
||||
</Label>
|
||||
<Input
|
||||
id="edit_email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={formData.email || ""}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="edit_phone" className="text-right">
|
||||
Phone
|
||||
</Label>
|
||||
<Input
|
||||
id="edit_phone"
|
||||
name="phone"
|
||||
value={formData.phone || ""}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="edit_max_additional_guests"
|
||||
className="text-right"
|
||||
>
|
||||
Additional Guests
|
||||
</Label>
|
||||
<Input
|
||||
id="edit_max_additional_guests"
|
||||
name="max_additional_guests"
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.max_additional_guests || 0}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-start gap-4">
|
||||
<Label
|
||||
htmlFor="edit_dietary_restrictions"
|
||||
className="text-right pt-2"
|
||||
>
|
||||
Dietary Restrictions
|
||||
</Label>
|
||||
<Textarea
|
||||
id="edit_dietary_restrictions"
|
||||
name="dietary_restrictions"
|
||||
value={formData.dietary_restrictions || ""}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-start gap-4">
|
||||
<Label htmlFor="edit_notes" className="text-right pt-2">
|
||||
Notes
|
||||
</Label>
|
||||
<Textarea
|
||||
id="edit_notes"
|
||||
name="notes"
|
||||
value={formData.notes || ""}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="edit_can_bring_guests" className="text-right">
|
||||
Can Bring Guests
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.can_bring_guests ? "true" : "false"}
|
||||
onValueChange={(value) =>
|
||||
handleSelectChange("can_bring_guests", value === "true")
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">Yes</SelectItem>
|
||||
<SelectItem value="false">No</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEditGuestOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
onClick={handleEditGuest}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete the guest "{currentGuest?.full_name}
|
||||
". This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-red-600 hover:bg-red-700 text-white"
|
||||
onClick={handleDeleteGuest}
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { AuthProvider } from "@/context/auth-context";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { DataProviders } from "@/providers/data";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
// Create a client
|
||||
const queryClient = new QueryClient({
|
||||
@@ -29,6 +30,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
<AuthProvider>
|
||||
<DataProviders>{children}</DataProviders>
|
||||
<Toaster />
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
|
||||
Reference in New Issue
Block a user