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 && (
+
+ )}
+ |
+
+
+ {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 && (
-
- )}
- |
-
- {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) => {