Compare commits
9 Commits
cd22418786
...
62ce98c80e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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')
|
||||
|
||||
@@ -1048,3 +1048,21 @@ 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(
|
||||
"/purchases/guest/all",
|
||||
response_model=List[GiftPurchase],
|
||||
operation_id="read_guests_gift_purchases"
|
||||
)
|
||||
def read_all_guest_gift_reservations(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve all guest gift reservations.
|
||||
"""
|
||||
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,37 @@ 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
|
||||
|
||||
|
||||
# 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),
|
||||
)
|
||||
|
||||
@@ -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,14 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { GiftStatus, GiftPriority } from "@/client/types.gen";
|
||||
import { GiftPriority, 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";
|
||||
|
||||
export default function GiftRegistryPage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
@@ -57,6 +61,7 @@ export default function GiftRegistryPage() {
|
||||
items,
|
||||
isLoadingCategories,
|
||||
isLoadingItems,
|
||||
purchases,
|
||||
error,
|
||||
refetchCategories,
|
||||
refetchItems,
|
||||
@@ -387,19 +392,70 @@ 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">*/}
|
||||
{/* {item.reservations &&*/}
|
||||
{/* item.reservations.length > 0 ? (*/}
|
||||
{/* <ul className="list-disc">*/}
|
||||
{/* {item.reservations.map((res) => (*/}
|
||||
{/* <li key={res.guest_id}>*/}
|
||||
{/* {res.guest_name}: {res.quantity}*/}
|
||||
{/* </li>*/}
|
||||
{/* ))}*/}
|
||||
{/* </ul>*/}
|
||||
{/* ) : (*/}
|
||||
{/* <p>No reservations available.</p>*/}
|
||||
{/* )}*/}
|
||||
{/*</PopoverContent>*/}
|
||||
</Popover>
|
||||
) : (
|
||||
getStatusBadge(item.status)
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -428,68 +484,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,
|
||||
readGuestsGiftPurchases,
|
||||
createEvent,
|
||||
getUserEvents,
|
||||
getUpcomingEvents,
|
||||
@@ -165,6 +166,7 @@ import type {
|
||||
ReadGiftPurchaseData,
|
||||
ReadGiftPurchasesByGiftData,
|
||||
ReadGiftPurchasesByGuestData,
|
||||
ReadGuestsGiftPurchasesData,
|
||||
CreateEventData,
|
||||
CreateEventError,
|
||||
CreateEventResponse,
|
||||
@@ -1421,6 +1423,27 @@ export const readGiftPurchasesByGuestOptions = (
|
||||
});
|
||||
};
|
||||
|
||||
export const readGuestsGiftPurchasesQueryKey = (
|
||||
options?: Options<ReadGuestsGiftPurchasesData>,
|
||||
) => createQueryKey("readGuestsGiftPurchases", options);
|
||||
|
||||
export const readGuestsGiftPurchasesOptions = (
|
||||
options?: Options<ReadGuestsGiftPurchasesData>,
|
||||
) => {
|
||||
return queryOptions({
|
||||
queryFn: async ({ queryKey, signal }) => {
|
||||
const { data } = await readGuestsGiftPurchases({
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: readGuestsGiftPurchasesQueryKey(options),
|
||||
});
|
||||
};
|
||||
|
||||
export const createEventQueryKey = (options: Options<CreateEventData>) =>
|
||||
createQueryKey("createEvent", options);
|
||||
|
||||
|
||||
@@ -2715,6 +2715,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 +2781,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 +2839,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,8 @@ import type {
|
||||
ReadGiftPurchasesByGuestData,
|
||||
ReadGiftPurchasesByGuestResponse,
|
||||
ReadGiftPurchasesByGuestError,
|
||||
ReadGuestsGiftPurchasesData,
|
||||
ReadGuestsGiftPurchasesResponse,
|
||||
CreateEventData,
|
||||
CreateEventResponse,
|
||||
CreateEventError,
|
||||
@@ -1153,6 +1155,23 @@ export const readGiftPurchasesByGuest = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Read All Guest Gift Reservations
|
||||
* Retrieve all guest gift reservations.
|
||||
*/
|
||||
export const readGuestsGiftPurchases = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<ReadGuestsGiftPurchasesData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).get<
|
||||
ReadGuestsGiftPurchasesResponse,
|
||||
unknown,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/v1/events/gifts/purchases/guest/all",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create Event
|
||||
* Create a new event.
|
||||
|
||||
@@ -467,6 +467,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 +481,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 +1876,23 @@ export type ReadGiftPurchasesByGuestResponses = {
|
||||
export type ReadGiftPurchasesByGuestResponse =
|
||||
ReadGiftPurchasesByGuestResponses[keyof ReadGiftPurchasesByGuestResponses];
|
||||
|
||||
export type ReadGuestsGiftPurchasesData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: "/api/v1/events/gifts/purchases/guest/all";
|
||||
};
|
||||
|
||||
export type ReadGuestsGiftPurchasesResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: Array<GiftPurchase>;
|
||||
};
|
||||
|
||||
export type ReadGuestsGiftPurchasesResponse =
|
||||
ReadGuestsGiftPurchasesResponses[keyof ReadGuestsGiftPurchasesResponses];
|
||||
|
||||
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 => {
|
||||
@@ -522,10 +527,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 +575,54 @@ const GuestListTable = ({ event }: GuestListTableProps) => {
|
||||
</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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user