Compare commits
16 Commits
cd22418786
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
678e1db0a3 | ||
|
|
a2c3f16dc7 | ||
|
|
44e6b2a6dc | ||
|
|
42508af610 | ||
|
|
2a1f13a5f0 | ||
|
|
c81e27c602 | ||
|
|
9fe5e60907 | ||
|
|
62ce98c80e | ||
|
|
fe3f2b0894 | ||
|
|
79f08a1208 | ||
|
|
2c73ee4d7e | ||
|
|
392dd6f0d2 | ||
|
|
1f1192fb62 | ||
|
|
31c6ae3f5c | ||
|
|
678c55a1e2 | ||
|
|
d61e518697 |
@@ -317,6 +317,7 @@ def upgrade() -> None:
|
||||
sa.Column('gift_id', sa.UUID(), nullable=False),
|
||||
sa.Column('reserved_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('notes', sa.String(), nullable=True),
|
||||
sa.Column('quantity', sa.Integer(), nullable=False, server_default='1'),
|
||||
sa.ForeignKeyConstraint(['gift_id'], ['gift_items.id'], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(['guest_id'], ['guests.id'], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint('guest_id', 'gift_id')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.dependencies.auth import get_current_active_user, get_current_user, get_optional_current_user
|
||||
@@ -1048,3 +1048,27 @@ def read_gift_purchases_by_guest(
|
||||
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||
|
||||
return gift_purchase_crud.get_gift_reservations_by_guest(db, guest_id=guest_id)
|
||||
|
||||
@router.get(
|
||||
"/reservations/guests",
|
||||
response_model=List[GiftPurchase],
|
||||
operation_id="read_guests_gift_reservations",
|
||||
)
|
||||
def read_all_guest_gift_reservations(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
event_id: Optional[str] = Query(None, description="Optional event ID to filter reservations by event"),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve all guest gift reservations, optionally filtered by event ID.
|
||||
"""
|
||||
if event_id:
|
||||
event_id = UUID(event_id)
|
||||
reservations = gift_purchase_crud.get_event_guest_gift_reservations(db=db, event_id=event_id)
|
||||
else:
|
||||
reservations = gift_purchase_crud.get_all_guest_gift_reservations(db=db)
|
||||
|
||||
if not reservations:
|
||||
reservations = []
|
||||
|
||||
return reservations
|
||||
|
||||
@@ -2,15 +2,13 @@ from datetime import datetime, timezone
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import asc, desc
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import asc, desc, select
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from app.crud.base import CRUDBase
|
||||
from app.models import gift as gift_models
|
||||
from app.models.gift import GiftItem, GiftCategory, GiftPurchase, GiftStatus, EventGiftCategory
|
||||
from app.models.guest import Guest
|
||||
from app.models.event import Event
|
||||
from app.schemas import gifts as gift_schemas
|
||||
from app.models.gift import GiftItem, GiftCategory, GiftPurchase, GiftStatus, EventGiftCategory
|
||||
from app.models.guest import Guest, GuestGifts
|
||||
from app.schemas.gifts import (
|
||||
GiftItemCreate, GiftItemUpdate,
|
||||
GiftCategoryCreate, GiftCategoryUpdate,
|
||||
@@ -94,13 +92,14 @@ class CRUDGiftItem(CRUDBase[GiftItem, GiftItemCreate, GiftItemUpdate]):
|
||||
gift: GiftItem = self.get(db, gift_id)
|
||||
if gift and gift.quantity_received < gift.quantity_requested:
|
||||
# Add to the association table using the SQLAlchemy Core Table directly
|
||||
from app.models.guest import guest_gifts
|
||||
from app.models.guest import GuestGifts
|
||||
|
||||
stmt = guest_gifts.insert().values(
|
||||
stmt = GuestGifts.insert().values(
|
||||
gift_id=gift_id,
|
||||
guest_id=guest_id,
|
||||
reserved_at=datetime.now(timezone.utc),
|
||||
notes=notes
|
||||
notes=notes,
|
||||
quantity=quantity
|
||||
)
|
||||
db.execute(stmt)
|
||||
|
||||
@@ -151,6 +150,7 @@ class CRUDGiftItem(CRUDBase[GiftItem, GiftItemCreate, GiftItemUpdate]):
|
||||
|
||||
return gift # Always return the gift object, even if no changes were made
|
||||
|
||||
|
||||
class CRUDEventGiftCategory:
|
||||
def create(self, db: Session, *, obj_in: EventGiftCategoryCreate) -> EventGiftCategory:
|
||||
"""Create a new event-category association"""
|
||||
@@ -335,13 +335,10 @@ class CRUDGiftPurchase(CRUDBase[GiftPurchase, GiftPurchaseCreate, GiftPurchaseUp
|
||||
result = []
|
||||
for gift in guest.gifts:
|
||||
# Access the association data through the relationship metadata
|
||||
from sqlalchemy import select
|
||||
from app.models.guest import guest_gifts
|
||||
|
||||
# Get the reservation data
|
||||
stmt = select(guest_gifts).where(
|
||||
(guest_gifts.c.guest_id == guest_id) &
|
||||
(guest_gifts.c.gift_id == gift.id)
|
||||
stmt = select(GuestGifts).where(
|
||||
(GuestGifts.c.guest_id == guest_id) &
|
||||
(GuestGifts.c.gift_id == gift.id)
|
||||
)
|
||||
reservation = db.execute(stmt).first()
|
||||
|
||||
@@ -359,6 +356,72 @@ class CRUDGiftPurchase(CRUDBase[GiftPurchase, GiftPurchaseCreate, GiftPurchaseUp
|
||||
|
||||
return result
|
||||
|
||||
def get_all_guest_gift_reservations(self, db: Session) -> List[GiftPurchase]:
|
||||
"""Retrieve all guest gift reservations and convert them into GiftPurchase-like objects for API compatibility."""
|
||||
|
||||
stmt = select(Guest).options(joinedload(Guest.gifts))
|
||||
guests = db.scalars(stmt).all()
|
||||
|
||||
result = []
|
||||
|
||||
for guest in guests:
|
||||
# Fetch all gifts and their reservation details per guest at once to avoid N+1 issues
|
||||
for gift in guest.gifts:
|
||||
reservation_stmt = select(GuestGifts).where(
|
||||
(GuestGifts.c.guest_id == guest.id) &
|
||||
(GuestGifts.c.gift_id == gift.id)
|
||||
)
|
||||
|
||||
reservation = db.execute(reservation_stmt).first()
|
||||
|
||||
if reservation:
|
||||
purchase = GiftPurchase(
|
||||
id=UUID('00000000-0000-0000-0000-000000000000'), # Placeholder UUID
|
||||
gift_id=gift.id,
|
||||
guest_id=guest.id,
|
||||
quantity=1, # Default
|
||||
purchased_at=reservation.reserved_at,
|
||||
notes=reservation.notes
|
||||
)
|
||||
result.append(purchase)
|
||||
|
||||
return result
|
||||
|
||||
def get_event_guest_gift_reservations(self, db: Session, event_id: UUID | str) -> List[GiftPurchase]:
|
||||
"""Retrieve all gift reservations for guests belonging to a specific event."""
|
||||
event_id = event_id if isinstance(event_id, UUID) else UUID(event_id)
|
||||
|
||||
stmt = (
|
||||
select(Guest)
|
||||
.where(Guest.event_id == event_id)
|
||||
.options(joinedload(Guest.gifts))
|
||||
)
|
||||
|
||||
# Correct: Call unique() on the RESULT, not on the stmt
|
||||
guests = db.execute(stmt).unique().scalars().all()
|
||||
|
||||
results = []
|
||||
for guest in guests:
|
||||
for gift in guest.gifts:
|
||||
reservation_stmt = select(GuestGifts).where(
|
||||
(GuestGifts.c.guest_id == guest.id) &
|
||||
(GuestGifts.c.gift_id == gift.id)
|
||||
)
|
||||
|
||||
reservation = db.execute(reservation_stmt).first()
|
||||
|
||||
if reservation:
|
||||
purchase = GiftPurchase(
|
||||
id=UUID('00000000-0000-0000-0000-000000000000'),
|
||||
gift_id=gift.id,
|
||||
guest_id=guest.id,
|
||||
quantity=1,
|
||||
purchased_at=reservation.reserved_at,
|
||||
notes=reservation.notes
|
||||
)
|
||||
results.append(purchase)
|
||||
|
||||
return results
|
||||
|
||||
# Create CRUD instances
|
||||
gift_item_crud = CRUDGiftItem(GiftItem)
|
||||
|
||||
@@ -16,7 +16,7 @@ from .event_theme import EventTheme
|
||||
from .event_media import EventMedia, MediaType, MediaPurpose
|
||||
|
||||
# Import guest and RSVP models
|
||||
from .guest import Guest, GuestStatus, guest_gifts
|
||||
from .guest import Guest, GuestStatus, GuestGifts
|
||||
from .rsvp import RSVP, RSVPStatus
|
||||
|
||||
# Import gift-related models
|
||||
|
||||
@@ -82,11 +82,12 @@ class Guest(Base, UUIDMixin, TimestampMixin):
|
||||
|
||||
|
||||
# Association table for guest gifts (many-to-many relationship)
|
||||
guest_gifts = Table(
|
||||
GuestGifts = Table(
|
||||
'guest_gifts',
|
||||
Base.metadata,
|
||||
Column('guest_id', UUID(as_uuid=True), ForeignKey('guests.id'), primary_key=True),
|
||||
Column('gift_id', UUID(as_uuid=True), ForeignKey('gift_items.id'), primary_key=True),
|
||||
Column('reserved_at', DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)),
|
||||
Column('notes', String),
|
||||
Column('quantity', Integer, default=1),
|
||||
)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel, EmailStr, ConfigDict
|
||||
from datetime import datetime
|
||||
from typing import Optional, Any, Dict
|
||||
from app.models.guest import GuestStatus
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, EmailStr, ConfigDict
|
||||
|
||||
from app.models.guest import GuestStatus
|
||||
from app.schemas.rsvp import RSVPSchema, RSVPStatus
|
||||
|
||||
|
||||
@@ -50,6 +49,8 @@ class GuestRead(GuestBase):
|
||||
is_blocked: bool
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
invitation_code: str
|
||||
rsvp: Optional[RSVPSchema] = None
|
||||
|
||||
|
||||
class GuestWithRSVPResponse(BaseModel):
|
||||
"""
|
||||
@@ -62,6 +63,7 @@ class GuestWithRSVPResponse(BaseModel):
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
def map_rsvp_status_to_guest_status(rsvp_status: RSVPStatus) -> GuestStatus:
|
||||
if rsvp_status == RSVPStatus.ATTENDING:
|
||||
return GuestStatus.CONFIRMED
|
||||
@@ -70,4 +72,4 @@ def map_rsvp_status_to_guest_status(rsvp_status: RSVPStatus) -> GuestStatus:
|
||||
elif rsvp_status == RSVPStatus.MAYBE:
|
||||
return GuestStatus.PENDING
|
||||
else:
|
||||
return GuestStatus.INVITED
|
||||
return GuestStatus.INVITED
|
||||
|
||||
@@ -17,6 +17,7 @@ import Link from "next/link";
|
||||
import { Loader2Icon, PaletteIcon } from "lucide-react";
|
||||
import { useEventThemes } from "@/context/event-theme-context";
|
||||
import { getServerFileUrl } from "@/lib/utils";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function EventThemesPage() {
|
||||
// const { data: themes, isLoading } = useQuery({
|
||||
@@ -24,8 +25,11 @@ export default function EventThemesPage() {
|
||||
// queryFn: () => listEventThemes().then(res => res.data),
|
||||
// });
|
||||
|
||||
const { themes, isLoadingThemes } = useEventThemes();
|
||||
const { themes, refetchThemes, isLoadingThemes } = useEventThemes();
|
||||
|
||||
useEffect(() => {
|
||||
refetchThemes();
|
||||
}, []);
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto py-6 px-4">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
|
||||
@@ -6,16 +6,15 @@ import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ChevronRight,
|
||||
Loader2,
|
||||
AlertTriangle,
|
||||
ChevronRight,
|
||||
Edit,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
Search,
|
||||
Filter,
|
||||
Edit,
|
||||
Trash,
|
||||
MoreHorizontal,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import { useEvents } from "@/context/event-context";
|
||||
import { useGifts } from "@/context/gift-context";
|
||||
@@ -45,9 +44,15 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { GiftStatus, GiftPriority } from "@/client/types.gen";
|
||||
import { GiftPriority, GiftPurchase, GiftStatus } from "@/client/types.gen";
|
||||
import { CategoryModal } from "@/components/gifts/category-modal";
|
||||
import { GiftModal } from "@/components/gifts/gift-modal";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { useGuests } from "@/context/guest-context";
|
||||
|
||||
export default function GiftRegistryPage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
@@ -57,14 +62,17 @@ export default function GiftRegistryPage() {
|
||||
items,
|
||||
isLoadingCategories,
|
||||
isLoadingItems,
|
||||
purchases,
|
||||
error,
|
||||
refetchCategories,
|
||||
refetchItems,
|
||||
currentEventId,
|
||||
setCurrentEventId,
|
||||
deleteItem,
|
||||
fetchGuestsGiftPurchases,
|
||||
} = useGifts();
|
||||
|
||||
const { guests } = useGuests();
|
||||
// State for modals
|
||||
const [isAddGiftModalOpen, setIsAddGiftModalOpen] = useState(false);
|
||||
const [isEditGiftModalOpen, setIsEditGiftModalOpen] = useState(false);
|
||||
@@ -79,6 +87,39 @@ export default function GiftRegistryPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [categoryFilter, setCategoryFilter] = useState<string>("all");
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||
const [reservations, setReservations] = useState<
|
||||
Record<string, GiftPurchase[]>
|
||||
>({});
|
||||
const [loadingReservations, setLoadingReservations] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadReservations = async () => {
|
||||
if (currentEventId) {
|
||||
setLoadingReservations(true);
|
||||
try {
|
||||
const data = await fetchGuestsGiftPurchases(currentEventId);
|
||||
if (data) {
|
||||
const groupedReservations = data.reduce(
|
||||
(acc, purchase) => {
|
||||
const giftId = purchase.gift_id;
|
||||
if (!acc[giftId]) acc[giftId] = [];
|
||||
acc[giftId].push(purchase);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, GiftPurchase[]>,
|
||||
);
|
||||
setReservations(groupedReservations);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Unable to fetch reservations:", err);
|
||||
} finally {
|
||||
setLoadingReservations(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadReservations();
|
||||
}, [currentEventId, fetchGuestsGiftPurchases]);
|
||||
|
||||
// Filter items based on search query and filters
|
||||
const filteredItems = items
|
||||
@@ -387,19 +428,94 @@ export default function GiftRegistryPage() {
|
||||
const category = categories?.find(
|
||||
(c) => c.id === item.category_id,
|
||||
);
|
||||
const canShowReservations = item.status
|
||||
? (
|
||||
[
|
||||
GiftStatus.RESERVED,
|
||||
GiftStatus.RECEIVED,
|
||||
] as GiftStatus[]
|
||||
).includes(item.status)
|
||||
: false;
|
||||
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
{category?.name || "Uncategorized"}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
|
||||
<TableCell className="flex items-center gap-2 font-medium">
|
||||
{/* Purchase URL clickable icon */}
|
||||
{item.purchase_url ? (
|
||||
<Link
|
||||
href={item.purchase_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 text-blue-500 hover:text-blue-600" />
|
||||
</Link>
|
||||
) : (
|
||||
<ExternalLink className="w-4 h-4 text-gray-300 cursor-not-allowed" />
|
||||
)}
|
||||
{item.name}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{item.quantity_requested || 1}</TableCell>
|
||||
<TableCell>{item.formatted_price || "-"}</TableCell>
|
||||
<TableCell>{getPriorityBadge(item.priority)}</TableCell>
|
||||
<TableCell>{getStatusBadge(item.status)}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{canShowReservations ? (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer">
|
||||
{getStatusBadge(item.status)}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="text-sm w-[220px]">
|
||||
{loadingReservations ? (
|
||||
<div className="flex items-center justify-center p-2">
|
||||
<Loader2
|
||||
className="animate-spin"
|
||||
size={16}
|
||||
/>
|
||||
<span className="ml-2">
|
||||
Loading reservations...
|
||||
</span>
|
||||
</div>
|
||||
) : reservations[item.id] &&
|
||||
reservations[item.id].length > 0 ? (
|
||||
<ul className="list-disc pl-4 py-2">
|
||||
{reservations[item.id].map(
|
||||
(purchase, index) => {
|
||||
const guest = guests?.find(
|
||||
(g) => g.id === purchase.guest_id,
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={`${purchase.id}_${index}`}>
|
||||
<strong>
|
||||
{guest?.full_name ||
|
||||
purchase.guest_id}
|
||||
</strong>
|
||||
: {purchase.quantity}
|
||||
</li>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="p-2 text-gray-500">
|
||||
No reservations available.
|
||||
</p>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
getStatusBadge(item.status)
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -428,68 +544,18 @@ export default function GiftRegistryPage() {
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
{/* Separate row for description */}
|
||||
{/*{item.description && (*/}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={7}
|
||||
className="py-1 px-4 border-t-0 bg-gray-50 dark:bg-gray-800/50 text-sm text-gray-500 dark:text-gray-400"
|
||||
className="py-1 px-4 bg-gray-50 dark:bg-gray-800/50 text-sm text-gray-500 dark:text-gray-400 h-8"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* Description - show if available */}
|
||||
{item.description && <p>{item.description}</p>}
|
||||
|
||||
{/* Status-specific information */}
|
||||
<div className="flex items-center gap-2">
|
||||
{item.status === GiftStatus.AVAILABLE && (
|
||||
<>
|
||||
{item.store_name && (
|
||||
<span>Store: {item.store_name}</span>
|
||||
)}
|
||||
|
||||
{item.purchase_url && (
|
||||
<>
|
||||
{item.store_name && <span>•</span>}
|
||||
<a
|
||||
href={item.purchase_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
Shop Link
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!item.store_name &&
|
||||
!item.purchase_url &&
|
||||
item.brand && (
|
||||
<span>Brand: {item.brand}</span>
|
||||
)}
|
||||
|
||||
{!item.store_name &&
|
||||
!item.purchase_url &&
|
||||
!item.brand && (
|
||||
<span className={"italic"}>
|
||||
No purchase information available
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{item.status === GiftStatus.RESERVED && (
|
||||
<span>Reserved</span>
|
||||
)}
|
||||
|
||||
{item.status === GiftStatus.PURCHASED && (
|
||||
<span>Purchased</span>
|
||||
)}
|
||||
|
||||
{item.status === GiftStatus.RECEIVED && (
|
||||
<span>Received</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{item.description}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/*)}*/}
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -27,11 +27,13 @@ import {
|
||||
import { useEventThemes } from "@/context/event-theme-context";
|
||||
import { getServerFileUrl } from "@/lib/utils";
|
||||
import GuestsList from "@/components/guests/guests-list";
|
||||
import { useGuests } from "@/context/guest-context";
|
||||
|
||||
export default function EventDetailPage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { event, fetchEventBySlug, isLoadingEvent, eventError } = useEvents();
|
||||
const { themes } = useEventThemes();
|
||||
const { refetchGuests } = useGuests();
|
||||
const currentTheme =
|
||||
event?.theme_id && themes
|
||||
? themes.find((theme) => theme.id === event.theme_id)
|
||||
@@ -44,6 +46,10 @@ export default function EventDetailPage() {
|
||||
fetchEventBySlug(slug);
|
||||
}, [slug, fetchEventBySlug]);
|
||||
|
||||
useEffect(() => {
|
||||
refetchGuests();
|
||||
}, []);
|
||||
|
||||
if (isLoadingEvent) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[50vh]">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter, usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import Navbar from "@/components/layout/navbar";
|
||||
import Breadcrumbs from "@/components/layout/breadcrumb";
|
||||
import { useEvents } from "@/context/event-context";
|
||||
|
||||
export default function MainLayout({
|
||||
children,
|
||||
@@ -12,12 +13,18 @@ export default function MainLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
const { refetchUpcomingEvents, refetchPublicEvents, refetchUserEvents } =
|
||||
useEvents();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isAuthenticated) {
|
||||
router.push("/login");
|
||||
} else {
|
||||
refetchUpcomingEvents();
|
||||
refetchPublicEvents();
|
||||
refetchUserEvents();
|
||||
}
|
||||
}, [isAuthenticated, isLoading, router]);
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
readGiftPurchase,
|
||||
readGiftPurchasesByGift,
|
||||
readGiftPurchasesByGuest,
|
||||
readGuestsGiftReservations,
|
||||
createEvent,
|
||||
getUserEvents,
|
||||
getUpcomingEvents,
|
||||
@@ -165,6 +166,7 @@ import type {
|
||||
ReadGiftPurchaseData,
|
||||
ReadGiftPurchasesByGiftData,
|
||||
ReadGiftPurchasesByGuestData,
|
||||
ReadGuestsGiftReservationsData,
|
||||
CreateEventData,
|
||||
CreateEventError,
|
||||
CreateEventResponse,
|
||||
@@ -1421,6 +1423,27 @@ export const readGiftPurchasesByGuestOptions = (
|
||||
});
|
||||
};
|
||||
|
||||
export const readGuestsGiftReservationsQueryKey = (
|
||||
options?: Options<ReadGuestsGiftReservationsData>,
|
||||
) => createQueryKey("readGuestsGiftReservations", options);
|
||||
|
||||
export const readGuestsGiftReservationsOptions = (
|
||||
options?: Options<ReadGuestsGiftReservationsData>,
|
||||
) => {
|
||||
return queryOptions({
|
||||
queryFn: async ({ queryKey, signal }) => {
|
||||
const { data } = await readGuestsGiftReservations({
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: readGuestsGiftReservationsQueryKey(options),
|
||||
});
|
||||
};
|
||||
|
||||
export const createEventQueryKey = (options: Options<CreateEventData>) =>
|
||||
createQueryKey("createEvent", options);
|
||||
|
||||
|
||||
@@ -2427,6 +2427,16 @@ export const GuestReadSchema = {
|
||||
type: "string",
|
||||
title: "Invitation Code",
|
||||
},
|
||||
rsvp: {
|
||||
anyOf: [
|
||||
{
|
||||
$ref: "#/components/schemas/RSVPSchema",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
required: [
|
||||
@@ -2715,6 +2725,16 @@ export const PresignedUrlResponseSchema = {
|
||||
|
||||
export const RSVPSchemaSchema = {
|
||||
properties: {
|
||||
event_id: {
|
||||
type: "string",
|
||||
format: "uuid",
|
||||
title: "Event Id",
|
||||
},
|
||||
guest_id: {
|
||||
type: "string",
|
||||
format: "uuid",
|
||||
title: "Guest Id",
|
||||
},
|
||||
status: {
|
||||
$ref: "#/components/schemas/RSVPStatus",
|
||||
},
|
||||
@@ -2771,12 +2791,30 @@ export const RSVPSchemaSchema = {
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
required: ["status", "id", "response_date", "created_at", "updated_at"],
|
||||
required: [
|
||||
"event_id",
|
||||
"guest_id",
|
||||
"status",
|
||||
"id",
|
||||
"response_date",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
],
|
||||
title: "RSVPSchema",
|
||||
} as const;
|
||||
|
||||
export const RSVPSchemaCreateSchema = {
|
||||
properties: {
|
||||
event_id: {
|
||||
type: "string",
|
||||
format: "uuid",
|
||||
title: "Event Id",
|
||||
},
|
||||
guest_id: {
|
||||
type: "string",
|
||||
format: "uuid",
|
||||
title: "Guest Id",
|
||||
},
|
||||
status: {
|
||||
$ref: "#/components/schemas/RSVPStatus",
|
||||
},
|
||||
@@ -2811,19 +2849,9 @@ export const RSVPSchemaCreateSchema = {
|
||||
additional_info: {
|
||||
title: "Additional Info",
|
||||
},
|
||||
event_id: {
|
||||
type: "string",
|
||||
format: "uuid",
|
||||
title: "Event Id",
|
||||
},
|
||||
guest_id: {
|
||||
type: "string",
|
||||
format: "uuid",
|
||||
title: "Guest Id",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
required: ["status", "event_id", "guest_id"],
|
||||
required: ["event_id", "guest_id", "status"],
|
||||
title: "RSVPSchemaCreate",
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -142,6 +142,9 @@ import type {
|
||||
ReadGiftPurchasesByGuestData,
|
||||
ReadGiftPurchasesByGuestResponse,
|
||||
ReadGiftPurchasesByGuestError,
|
||||
ReadGuestsGiftReservationsData,
|
||||
ReadGuestsGiftReservationsResponse,
|
||||
ReadGuestsGiftReservationsError,
|
||||
CreateEventData,
|
||||
CreateEventResponse,
|
||||
CreateEventError,
|
||||
@@ -1153,6 +1156,25 @@ export const readGiftPurchasesByGuest = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Read All Guest Gift Reservations
|
||||
* Retrieve all guest gift reservations, optionally filtered by event ID.
|
||||
*/
|
||||
export const readGuestsGiftReservations = <
|
||||
ThrowOnError extends boolean = false,
|
||||
>(
|
||||
options?: Options<ReadGuestsGiftReservationsData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).get<
|
||||
ReadGuestsGiftReservationsResponse,
|
||||
ReadGuestsGiftReservationsError,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/v1/events/gifts/reservations/guests",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create Event
|
||||
* Create a new event.
|
||||
|
||||
@@ -370,6 +370,7 @@ export type GuestRead = {
|
||||
actual_additional_guests: number;
|
||||
is_blocked: boolean;
|
||||
invitation_code: string;
|
||||
rsvp?: RsvpSchema | null;
|
||||
};
|
||||
|
||||
export type GuestStatus =
|
||||
@@ -467,6 +468,8 @@ export type PresignedUrlResponse = {
|
||||
};
|
||||
|
||||
export type RsvpSchema = {
|
||||
event_id: string;
|
||||
guest_id: string;
|
||||
status: RsvpStatus;
|
||||
number_of_guests?: number;
|
||||
response_message?: string | null;
|
||||
@@ -479,13 +482,13 @@ export type RsvpSchema = {
|
||||
};
|
||||
|
||||
export type RsvpSchemaCreate = {
|
||||
event_id: string;
|
||||
guest_id: string;
|
||||
status: RsvpStatus;
|
||||
number_of_guests?: number;
|
||||
response_message?: string | null;
|
||||
dietary_requirements?: string | null;
|
||||
additional_info?: unknown;
|
||||
event_id: string;
|
||||
guest_id: string;
|
||||
};
|
||||
|
||||
export type RsvpSchemaUpdate = {
|
||||
@@ -1874,6 +1877,38 @@ export type ReadGiftPurchasesByGuestResponses = {
|
||||
export type ReadGiftPurchasesByGuestResponse =
|
||||
ReadGiftPurchasesByGuestResponses[keyof ReadGiftPurchasesByGuestResponses];
|
||||
|
||||
export type ReadGuestsGiftReservationsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: {
|
||||
/**
|
||||
* Optional event ID to filter reservations by event
|
||||
*/
|
||||
event_id?: string | null;
|
||||
};
|
||||
url: "/api/v1/events/gifts/reservations/guests";
|
||||
};
|
||||
|
||||
export type ReadGuestsGiftReservationsErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ReadGuestsGiftReservationsError =
|
||||
ReadGuestsGiftReservationsErrors[keyof ReadGuestsGiftReservationsErrors];
|
||||
|
||||
export type ReadGuestsGiftReservationsResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: Array<GiftPurchase>;
|
||||
};
|
||||
|
||||
export type ReadGuestsGiftReservationsResponse =
|
||||
ReadGuestsGiftReservationsResponses[keyof ReadGuestsGiftReservationsResponses];
|
||||
|
||||
export type CreateEventData = {
|
||||
body: EventCreate;
|
||||
path?: never;
|
||||
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
import { useGuests } from "@/context/guest-context";
|
||||
import {
|
||||
EventResponse,
|
||||
EventThemeResponse,
|
||||
GuestCreate,
|
||||
GuestRead,
|
||||
GuestStatus,
|
||||
@@ -66,6 +65,12 @@ import {
|
||||
} 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 => {
|
||||
@@ -96,7 +101,7 @@ const GuestListTable = ({ event }: GuestListTableProps) => {
|
||||
full_name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
max_additional_guests: 0,
|
||||
max_additional_guests: 10,
|
||||
dietary_restrictions: "",
|
||||
notes: "",
|
||||
can_bring_guests: true,
|
||||
@@ -288,7 +293,7 @@ const GuestListTable = ({ event }: GuestListTableProps) => {
|
||||
"Phone",
|
||||
"Invitation Code",
|
||||
"Status",
|
||||
"Additional Guests",
|
||||
"Max Additional Guests",
|
||||
];
|
||||
const csvContent = [
|
||||
headers.join(","),
|
||||
@@ -343,6 +348,12 @@ const GuestListTable = ({ event }: GuestListTableProps) => {
|
||||
}
|
||||
}, [addGuestOpen, editGuestOpen, currentGuest]);
|
||||
|
||||
const confirmedGuestCount =
|
||||
guests?.filter((g) => g.status === GuestStatus.CONFIRMED).length || 0;
|
||||
const confirmedAdditionalGuestsCount =
|
||||
guests?.reduce((acc, g) => acc + (g.actual_additional_guests || 0), 0) || 0;
|
||||
const totalConfirmedGuestsCount =
|
||||
confirmedGuestCount + confirmedAdditionalGuestsCount;
|
||||
return (
|
||||
<div className="space-y-4 w-full">
|
||||
{error && (
|
||||
@@ -409,14 +420,14 @@ const GuestListTable = ({ event }: GuestListTableProps) => {
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="max_additional_guests" className="text-right">
|
||||
Additional Guests
|
||||
Max Add. Guests
|
||||
</Label>
|
||||
<Input
|
||||
id="max_additional_guests"
|
||||
name="max_additional_guests"
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.max_additional_guests || 0}
|
||||
value={formData.max_additional_guests || 10}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
/>
|
||||
@@ -522,10 +533,13 @@ const GuestListTable = ({ event }: GuestListTableProps) => {
|
||||
<TableHead>Phone</TableHead>
|
||||
<TableHead>Invitation Code</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Additional Guests</TableHead>
|
||||
<TableHead>Add. Guests</TableHead>
|
||||
<TableHead>Diet Restr.</TableHead>
|
||||
<TableHead>Notes</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{isLoadingGuests ? (
|
||||
<TableRow>
|
||||
@@ -567,6 +581,57 @@ const GuestListTable = ({ event }: GuestListTableProps) => {
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(guest.status)}</TableCell>
|
||||
<TableCell>{guest.actual_additional_guests || 0}</TableCell>
|
||||
|
||||
{/* Dietary Restrictions Column */}
|
||||
<TableCell>
|
||||
{guest.rsvp?.dietary_requirements &&
|
||||
guest.rsvp.dietary_requirements.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.rsvp.dietary_requirements}
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* Notes Column */}
|
||||
<TableCell>
|
||||
{guest.rsvp?.response_message &&
|
||||
guest.rsvp.response_message.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.rsvp.response_message}
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -626,14 +691,9 @@ const GuestListTable = ({ event }: GuestListTableProps) => {
|
||||
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}
|
||||
Guests Confirmed: {confirmedGuestCount} | Additional Guests:{" "}
|
||||
{confirmedAdditionalGuestsCount} | Total Guests:{" "}
|
||||
{totalConfirmedGuestsCount}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -687,7 +747,7 @@ const GuestListTable = ({ event }: GuestListTableProps) => {
|
||||
htmlFor="edit_max_additional_guests"
|
||||
className="text-right"
|
||||
>
|
||||
Additional Guests
|
||||
Max Add. Guests
|
||||
</Label>
|
||||
<Input
|
||||
id="edit_max_additional_guests"
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useSearchParams, useParams } from "next/navigation";
|
||||
import { Loader2, Plus, Minus } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useRSVPs } from "@/context/rsvp-context";
|
||||
|
||||
interface RSVPProps {
|
||||
eventId: string;
|
||||
@@ -32,6 +33,8 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
|
||||
findGuestByInvitationCode,
|
||||
submitGuestRsvp,
|
||||
} = useGuests();
|
||||
|
||||
const { rsvps } = useRSVPs();
|
||||
const searchParams = useSearchParams();
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { event, fetchEventBySlug, isLoadingEvent } = useEvents();
|
||||
@@ -45,6 +48,9 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
const [currentGuest, setCurrentGuest] = useState<any | null>(null);
|
||||
const [currentRSVP, setCurrentRSVP] = useState<any | null>(null);
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -58,6 +64,21 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
|
||||
}
|
||||
}, [slug, fetchEventBySlug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (rsvps && currentGuest) {
|
||||
setCurrentRSVP(rsvps.find((rsvp) => rsvp.guest_id === currentGuest?.id));
|
||||
}
|
||||
}, [rsvps, currentGuest]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentRSVP) {
|
||||
setStatus(currentRSVP.status);
|
||||
setNumberOfGuests(currentRSVP.number_of_guests);
|
||||
setResponseMessage(currentRSVP.response_message);
|
||||
setDietaryRequirements(currentRSVP.dietary_requirements);
|
||||
}
|
||||
}, [currentRSVP]);
|
||||
|
||||
// Find the theme for this event
|
||||
const eventTheme =
|
||||
event && themes?.find((theme) => theme.id === event.theme_id);
|
||||
@@ -114,7 +135,7 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
|
||||
// Find the guest with matching invitation code
|
||||
if (guests) {
|
||||
const matchingGuest = findGuestByInvitationCode(invitationCode);
|
||||
|
||||
setCurrentGuest(matchingGuest);
|
||||
if (matchingGuest) {
|
||||
console.log("matchingGuest ", matchingGuest);
|
||||
setGuestId(matchingGuest.id);
|
||||
@@ -248,13 +269,12 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
|
||||
if (success) {
|
||||
return (
|
||||
<motion.div
|
||||
className="w-full rounded-lg border shadow-sm p-8 text-center"
|
||||
className="w-full rounded-lg shadow-sm p-8 text-center"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
style={{
|
||||
backgroundColor: colors.background,
|
||||
borderColor: colors.primary,
|
||||
color: colors.text,
|
||||
}}
|
||||
>
|
||||
@@ -298,13 +318,12 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="w-full rounded-lg border shadow-sm"
|
||||
className="w-full rounded-lg shadow-sm"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={formVariants}
|
||||
style={{
|
||||
backgroundColor: colors.background,
|
||||
borderColor: colors.backgroundDark,
|
||||
color: colors.text,
|
||||
}}
|
||||
>
|
||||
@@ -466,12 +485,15 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
|
||||
<motion.div className="flex justify-end mt-6" variants={itemVariants}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full md:w-auto"
|
||||
className="w-full md:w-auto text-white transition-colors duration-300"
|
||||
disabled={isProcessing}
|
||||
style={{
|
||||
backgroundColor: colors.accent,
|
||||
color: "white",
|
||||
}}
|
||||
style={{ backgroundColor: colors.accent }}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.backgroundColor = colors.accent2)
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.backgroundColor = colors.accent)
|
||||
}
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
|
||||
@@ -32,6 +32,11 @@ interface EventsContextState {
|
||||
userEvents: PaginatedResponseEventResponse | undefined;
|
||||
upcomingEvents: PaginatedResponseEventResponse | undefined;
|
||||
publicEvents: PaginatedResponseEventResponse | undefined;
|
||||
|
||||
refetchUpcomingEvents: () => Promise<any>;
|
||||
refetchPublicEvents: () => Promise<any>;
|
||||
refetchUserEvents: () => Promise<any>;
|
||||
|
||||
isLoadingUserEvents: boolean;
|
||||
isLoadingUpcomingEvents: boolean;
|
||||
isLoadingPublicEvents: boolean;
|
||||
@@ -67,6 +72,11 @@ const defaultEventsState: EventsContextState = {
|
||||
userEvents: undefined,
|
||||
upcomingEvents: undefined,
|
||||
publicEvents: undefined,
|
||||
|
||||
refetchUpcomingEvents: async () => undefined,
|
||||
refetchPublicEvents: async () => undefined,
|
||||
refetchUserEvents: async () => undefined,
|
||||
|
||||
isLoadingUserEvents: false,
|
||||
isLoadingUpcomingEvents: false,
|
||||
isLoadingPublicEvents: false,
|
||||
@@ -148,6 +158,7 @@ export const EventsProvider: React.FC<EventsProviderProps> = ({ children }) => {
|
||||
data: userEvents,
|
||||
isLoading: isLoadingUserEvents,
|
||||
error: userEventsError,
|
||||
refetch: refetchUserEvents,
|
||||
} = useQuery({
|
||||
...getUserEventsOptions(paginationParams),
|
||||
});
|
||||
@@ -157,6 +168,7 @@ export const EventsProvider: React.FC<EventsProviderProps> = ({ children }) => {
|
||||
data: upcomingEvents,
|
||||
isLoading: isLoadingUpcomingEvents,
|
||||
error: upcomingEventsError,
|
||||
refetch: refetchUpcomingEvents,
|
||||
} = useQuery({
|
||||
...getUpcomingEventsOptions(paginationParams),
|
||||
});
|
||||
@@ -166,6 +178,7 @@ export const EventsProvider: React.FC<EventsProviderProps> = ({ children }) => {
|
||||
data: publicEvents,
|
||||
isLoading: isLoadingPublicEvents,
|
||||
error: publicEventsError,
|
||||
refetch: refetchPublicEvents,
|
||||
} = useQuery({
|
||||
...getPublicEventsOptions(paginationParams),
|
||||
});
|
||||
@@ -255,6 +268,11 @@ export const EventsProvider: React.FC<EventsProviderProps> = ({ children }) => {
|
||||
userEvents,
|
||||
upcomingEvents,
|
||||
publicEvents,
|
||||
|
||||
refetchPublicEvents,
|
||||
refetchUpcomingEvents,
|
||||
refetchUserEvents,
|
||||
|
||||
isLoadingUserEvents,
|
||||
isLoadingUpcomingEvents,
|
||||
isLoadingPublicEvents,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
readGiftPurchase,
|
||||
readGiftPurchasesByGift,
|
||||
readGiftPurchasesByGuest,
|
||||
readGuestsGiftReservations,
|
||||
} from "@/client/sdk.gen";
|
||||
import {
|
||||
GiftCategory,
|
||||
@@ -48,6 +49,10 @@ interface GiftContextState {
|
||||
refetchCategories: (eventId: string) => Promise<any>;
|
||||
|
||||
fetchCategoryById: (id: string, eventId?: string) => void;
|
||||
fetchGuestsGiftPurchases: (
|
||||
eventId: string,
|
||||
) => Promise<GiftPurchase[] | undefined>;
|
||||
|
||||
createCategory: (
|
||||
data: GiftCategoryCreate,
|
||||
) => Promise<GiftCategory | undefined>;
|
||||
@@ -147,6 +152,8 @@ const defaultGiftContextState: GiftContextState = {
|
||||
},
|
||||
|
||||
fetchCategoryById: () => {},
|
||||
fetchGuestsGiftPurchases: async () => undefined,
|
||||
|
||||
createCategory: async () => {
|
||||
throw new Error("GiftContext not initialized");
|
||||
},
|
||||
@@ -310,6 +317,20 @@ export const GiftProvider: React.FC<GiftProviderProps> = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchGuestsGiftPurchases = async (
|
||||
eventId: string,
|
||||
): Promise<GiftPurchase[] | undefined> => {
|
||||
try {
|
||||
const result = await readGuestsGiftReservations({
|
||||
query: { event_id: eventId },
|
||||
});
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching guests' gift purchases:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Create Category Mutation
|
||||
const createCategoryMutation = useMutation({
|
||||
mutationFn: (data: GiftCategoryCreate) =>
|
||||
@@ -771,7 +792,7 @@ export const GiftProvider: React.FC<GiftProviderProps> = ({ children }) => {
|
||||
isLoadingCategories,
|
||||
isLoadingCategory,
|
||||
refetchCategories,
|
||||
|
||||
fetchGuestsGiftPurchases,
|
||||
fetchCategoryById,
|
||||
createCategory: createCategoryMutation.mutateAsync,
|
||||
updateCategory: (id, data, eventId) =>
|
||||
|
||||
Reference in New Issue
Block a user