Enhanced the guest list table by adding new columns for dietary restrictions and notes. These columns display content using interactive popovers when data is available, improving data accessibility and user experience. Updated existing table structure to accommodate these enhancements.
847 lines
28 KiB
TypeScript
847 lines
28 KiB
TypeScript
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<GuestRead | null>(null);
|
|
|
|
// 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);
|
|
|
|
// 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<HTMLInputElement | HTMLTextAreaElement>,
|
|
) => {
|
|
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, 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, 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 (
|
|
<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>
|
|
<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">
|
|
<Plus className="mr-2 h-4 w-4" /> Add Guest
|
|
</Button>
|
|
</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">
|
|
<div className="relative w-64">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500" />
|
|
<Input
|
|
placeholder="Search guests..."
|
|
className="pl-8"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm">
|
|
<Filter className="mr-2 h-4 w-4" /> Filter
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={sendInvitations}>
|
|
<Send className="mr-2 h-4 w-4" /> Send Invites
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Email</TableHead>
|
|
<TableHead>Phone</TableHead>
|
|
<TableHead>Invitation Code</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Add. Guests</TableHead>
|
|
<TableHead>Diet Restr.</TableHead>
|
|
<TableHead>Notes</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
|
|
<TableBody>
|
|
{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,
|
|
"Invitation code copied to clipboard",
|
|
)
|
|
}
|
|
>
|
|
<Copy className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>{getStatusBadge(guest.status)}</TableCell>
|
|
<TableCell>{guest.actual_additional_guests || 0}</TableCell>
|
|
|
|
{/* Dietary Restrictions Column */}
|
|
<TableCell>
|
|
{guest.dietary_restrictions &&
|
|
guest.dietary_restrictions.length > 0 ? (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6"
|
|
>
|
|
<Utensils className="h-4 w-4 text-green-600 cursor-pointer" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="max-w-xs">
|
|
<p className="text-sm">
|
|
{guest.dietary_restrictions}
|
|
</p>
|
|
</PopoverContent>
|
|
</Popover>
|
|
) : (
|
|
"-"
|
|
)}
|
|
</TableCell>
|
|
|
|
{/* Notes Column */}
|
|
<TableCell>
|
|
{guest.notes && guest.notes.length > 0 ? (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6"
|
|
>
|
|
<StickyNote className="h-4 w-4 text-blue-600 cursor-pointer" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="max-w-xs">
|
|
<p className="text-sm">{guest.notes}</p>
|
|
</PopoverContent>
|
|
</Popover>
|
|
) : (
|
|
"-"
|
|
)}
|
|
</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,
|
|
"Invitation code copied to clipboard",
|
|
)
|
|
}
|
|
>
|
|
Copy Invitation Code
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() =>
|
|
copyToClipboard(
|
|
generateInviteLink(
|
|
event.slug,
|
|
guest.invitation_code,
|
|
),
|
|
"Invitation link copied to clipboard",
|
|
)
|
|
}
|
|
>
|
|
Copy Invitation Link
|
|
</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 || 0} guests
|
|
</div>
|
|
<div>
|
|
Total Confirmed:{" "}
|
|
{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>
|
|
);
|
|
};
|
|
|
|
export default GuestListTable;
|