791 lines
26 KiB
TypeScript
791 lines
26 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,
|
|
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";
|
|
import { generateInviteLink } from "@/lib/utils";
|
|
|
|
// 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>Additional Guests</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>
|
|
<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;
|