diff --git a/components/booking/BookingListItem.tsx b/components/booking/BookingListItem.tsx new file mode 100644 index 00000000..851330a0 --- /dev/null +++ b/components/booking/BookingListItem.tsx @@ -0,0 +1,130 @@ +import { BanIcon, CheckIcon, ClockIcon, XIcon } from "@heroicons/react/outline"; +import { BookingStatus } from "@prisma/client"; +import dayjs from "dayjs"; +import { useMutation } from "react-query"; + +import { HttpError } from "@lib/core/http/error"; +import { inferQueryOutput, trpc } from "@lib/trpc"; + +import TableActions from "@components/ui/TableActions"; + +type BookingItem = inferQueryOutput<"viewer.bookings">[number]; + +function BookingListItem(booking: BookingItem) { + const utils = trpc.useContext(); + const mutation = useMutation( + async (confirm: boolean) => { + const res = await fetch("/api/book/confirm", { + method: "PATCH", + body: JSON.stringify({ id: booking.id, confirmed: confirm }), + headers: { + "Content-Type": "application/json", + }, + }); + if (!res.ok) { + throw new HttpError({ statusCode: res.status }); + } + }, + { + async onSettled() { + await utils.invalidateQuery(["viewer.bookings"]); + }, + } + ); + const isUpcoming = new Date(booking.endTime) >= new Date(); + const isCancelled = booking.status === BookingStatus.CANCELLED; + + const pendingActions = [ + { + id: "reject", + label: "Reject", + onClick: () => mutation.mutate(false), + icon: BanIcon, + disabled: mutation.isLoading, + }, + { + id: "confirm", + label: "Confirm", + onClick: () => mutation.mutate(true), + icon: CheckIcon, + disabled: mutation.isLoading, + color: "primary", + }, + ]; + + const bookedActions = [ + { + id: "cancel", + label: "Cancel", + href: `/cancel/${booking.uid}`, + icon: XIcon, + }, + { + id: "reschedule", + label: "Reschedule", + href: `/reschedule/${booking.uid}`, + icon: ClockIcon, + }, + ]; + + const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY"); + + return ( + + +
{startTime}
+ {!booking.confirmed && !booking.rejected && ( + + Unconfirmed + + )} + + +
+ {dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")} +
+ + +
+ {!booking.confirmed && !booking.rejected && ( + + Unconfirmed + + )} +
+ {startTime}:{" "} + + {dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")} + +
+
+
+ {booking.eventType?.team && {booking.eventType.team.name}: } + {booking.title} +
+ {booking.description && ( +
+ "{booking.description}" +
+ )} + {booking.attendees.length !== 0 && ( +
+ {booking.attendees[0].email} +
+ )} + + + + {isUpcoming && !isCancelled ? ( + <> + {!booking.confirmed && !booking.rejected && } + {booking.confirmed && !booking.rejected && } + {!booking.confirmed && booking.rejected &&
Rejected
} + + ) : null} + + + ); +} + +export default BookingListItem; diff --git a/components/ui/TableActions.tsx b/components/ui/TableActions.tsx index 2e5eccef..cccb53d3 100644 --- a/components/ui/TableActions.tsx +++ b/components/ui/TableActions.tsx @@ -12,6 +12,7 @@ type ActionType = { icon: SVGComponent; label: string; disabled?: boolean; + color?: "primary" | "secondary"; } & ({ href?: never; onClick: () => any } | { href: string; onClick?: never }); interface Props { @@ -30,7 +31,7 @@ const TableActions: FC = ({ actions }) => { onClick={action.onClick} StartIcon={action.icon} disabled={action.disabled} - color="secondary"> + color={action.color || "secondary"}> {action.label} ))} diff --git a/pages/bookings/[status].tsx b/pages/bookings/[status].tsx index 4424935e..080ecf85 100644 --- a/pages/bookings/[status].tsx +++ b/pages/bookings/[status].tsx @@ -1,140 +1,22 @@ -// TODO: replace headlessui with radix-ui -import { BanIcon, CalendarIcon, CheckIcon, ClockIcon, XIcon } from "@heroicons/react/outline"; -import { BookingStatus } from "@prisma/client"; -import dayjs from "dayjs"; +import { CalendarIcon } from "@heroicons/react/outline"; import { useRouter } from "next/router"; -import { useMutation } from "react-query"; -import { HttpError } from "@lib/core/http/error"; -import { inferQueryOutput, trpc } from "@lib/trpc"; +import { inferQueryInput, trpc } from "@lib/trpc"; import BookingsShell from "@components/BookingsShell"; import EmptyScreen from "@components/EmptyScreen"; import Loader from "@components/Loader"; import Shell from "@components/Shell"; +import BookingListItem from "@components/booking/BookingListItem"; import { Alert } from "@components/ui/Alert"; -import TableActions from "@components/ui/TableActions"; -type BookingItem = inferQueryOutput<"viewer.bookings">[number]; - -function BookingListItem(booking: BookingItem) { - const utils = trpc.useContext(); - const mutation = useMutation( - async (confirm: boolean) => { - const res = await fetch("/api/book/confirm", { - method: "PATCH", - body: JSON.stringify({ id: booking.id, confirmed: confirm }), - headers: { - "Content-Type": "application/json", - }, - }); - if (!res.ok) { - throw new HttpError({ statusCode: res.status }); - } - }, - { - async onSettled() { - await utils.invalidateQuery(["viewer.bookings"]); - }, - } - ); - const isUpcoming = new Date(booking.endTime) >= new Date(); - const isCancelled = booking.status === BookingStatus.CANCELLED; - - const pendingActions = [ - { - id: "confirm", - label: "Confirm", - onClick: () => mutation.mutate(true), - icon: CheckIcon, - disabled: mutation.isLoading, - }, - { - id: "reject", - label: "Reject", - onClick: () => mutation.mutate(false), - icon: BanIcon, - disabled: mutation.isLoading, - }, - ]; - - const bookedActions = [ - { - id: "cancel", - label: "Cancel", - href: `/cancel/${booking.uid}`, - icon: XIcon, - }, - { - id: "reschedule", - label: "Reschedule", - href: `/reschedule/${booking.uid}`, - icon: ClockIcon, - }, - ]; - - return ( - - - {!booking.confirmed && !booking.rejected && ( - - Unconfirmed - - )} -
- {booking.eventType?.team && {booking.eventType.team.name}: } - {booking.title} -
-
-
- {dayjs(booking.startTime).format("D MMMM YYYY")}:{" "} - - {dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")} - -
-
- {booking.description && ( -
- "{booking.description}" -
- )} - {booking.attendees.length !== 0 && ( -
- {booking.attendees[0].email} -
- )} - - -
{dayjs(booking.startTime).format("D MMMM YYYY")}
-
- {dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")} -
- - - {isUpcoming && !isCancelled ? ( - <> - {!booking.confirmed && !booking.rejected && } - {booking.confirmed && !booking.rejected && } - {!booking.confirmed && booking.rejected &&
Rejected
} - - ) : null} - - - ); -} +type BookingListingStatus = inferQueryInput<"viewer.bookings">["status"]; export default function Bookings() { const router = useRouter(); - const query = trpc.useQuery(["viewer.bookings"]); - const filtersByStatus = { - upcoming: (booking: BookingItem) => - new Date(booking.endTime) >= new Date() && booking.status !== BookingStatus.CANCELLED, - past: (booking: BookingItem) => new Date(booking.endTime) < new Date(), - cancelled: (booking: BookingItem) => booking.status === BookingStatus.CANCELLED, - } as const; - const filterKey = (router.query?.status as string as keyof typeof filtersByStatus) || "upcoming"; - const appliedFilter = filtersByStatus[filterKey]; - const bookings = query.data?.filter(appliedFilter); + const status = router.query?.status as BookingListingStatus; + const query = trpc.useQuery(["viewer.bookings", { status }]); + const bookings = query.data; return ( diff --git a/server/routers/viewer.tsx b/server/routers/viewer.tsx index 6b9a6ad5..8a75d9ca 100644 --- a/server/routers/viewer.tsx +++ b/server/routers/viewer.tsx @@ -1,4 +1,4 @@ -import { Prisma } from "@prisma/client"; +import { Prisma, BookingStatus } from "@prisma/client"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -20,8 +20,25 @@ export const viewerRouter = createProtectedRouter() }, }) .query("bookings", { - async resolve({ ctx }) { + input: z.object({ + status: z.enum(["upcoming", "past", "cancelled"]).optional(), + }), + async resolve({ ctx, input }) { const { prisma, user } = ctx; + const bookingListingByStatus = input.status || "upcoming"; + const bookingListingFilters: Record = { + upcoming: [{ endTime: { gte: new Date() } }], + past: [{ endTime: { lte: new Date() } }], + cancelled: [{ status: { equals: BookingStatus.CANCELLED } }], + }; + const bookingListingOrderby: Record = { + upcoming: { startTime: "desc" }, + past: { startTime: "asc" }, + cancelled: { startTime: "asc" }, + }; + const passedBookingsFilter = bookingListingFilters[bookingListingByStatus]; + const orderBy = bookingListingOrderby[bookingListingByStatus]; + const bookingsQuery = await prisma.booking.findMany({ where: { OR: [ @@ -36,6 +53,7 @@ export const viewerRouter = createProtectedRouter() }, }, ], + AND: passedBookingsFilter, }, select: { uid: true, @@ -58,9 +76,7 @@ export const viewerRouter = createProtectedRouter() }, status: true, }, - orderBy: { - startTime: "asc", - }, + orderBy, }); const bookings = bookingsQuery.reverse().map((booking) => {