confirming event gives no visual (#803)

This commit is contained in:
Alex Johansson 2021-09-28 10:16:02 +01:00 committed by GitHub
parent dd9f801872
commit 7779c098dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 283 additions and 302 deletions

View File

@ -5,9 +5,11 @@ import { DotsHorizontalIcon } from "@heroicons/react/solid";
import { BookingStatus } from "@prisma/client"; import { BookingStatus } from "@prisma/client";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Fragment } from "react"; import { Fragment } from "react";
import { useMutation } from "react-query";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
import { trpc } from "@lib/trpc"; import { HttpError } from "@lib/core/http/error";
import { inferQueryOutput, trpc } from "@lib/trpc";
import EmptyScreen from "@components/EmptyScreen"; import EmptyScreen from "@components/EmptyScreen";
import Loader from "@components/Loader"; import Loader from "@components/Loader";
@ -15,11 +17,12 @@ import Shell from "@components/Shell";
import { Alert } from "@components/ui/Alert"; import { Alert } from "@components/ui/Alert";
import { Button } from "@components/ui/Button"; import { Button } from "@components/ui/Button";
export default function Bookings() { type BookingItem = inferQueryOutput<"viewer.bookings">[number];
const query = trpc.useQuery(["viewer.bookings"]);
const bookings = query.data;
async function confirmBookingHandler(booking: { id: number }, confirm: boolean) { function BookingListItem(booking: BookingItem) {
const utils = trpc.useContext();
const mutation = useMutation(
async (confirm: boolean) => {
const res = await fetch("/api/book/confirm", { const res = await fetch("/api/book/confirm", {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ id: booking.id, confirmed: confirm }), body: JSON.stringify({ id: booking.id, confirmed: confirm }),
@ -27,35 +30,18 @@ export default function Bookings() {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}); });
if (res.ok) { if (!res.ok) {
await query.refetch(); throw new HttpError({ statusCode: res.status });
} }
},
{
async onSettled() {
await utils.invalidateQuery(["viewer.bookings"]);
},
} }
);
return ( return (
<Shell heading="Bookings" subtitle="See upcoming and past events booked through your event type links."> <tr>
<div className="-mx-4 sm:mx-auto flex flex-col">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
{query.status === "error" && (
<Alert severity="error" title="Something went wrong" message={query.error.message} />
)}
{query.status === "loading" && <Loader />}
{bookings &&
(bookings.length === 0 ? (
<EmptyScreen
Icon={CalendarIcon}
headline="No upcoming bookings, yet"
description="You have no upcoming bookings. As soon as someone books a time with you it will show up here."
/>
) : (
<div className="border border-gray-200 overflow-hidden border-b rounded-sm">
<table className="min-w-full divide-y divide-gray-200">
<tbody className="bg-white divide-y divide-gray-200" data-testid="bookings">
{bookings
.filter((booking) => booking.status !== BookingStatus.CANCELLED)
.map((booking) => (
<tr key={booking.id}>
<td className={"px-6 py-4" + (booking.rejected ? " line-through" : "")}> <td className={"px-6 py-4" + (booking.rejected ? " line-through" : "")}>
{!booking.confirmed && !booking.rejected && ( {!booking.confirmed && !booking.rejected && (
<span className="mb-2 inline-flex items-center px-1.5 py-0.5 rounded-sm text-xs font-medium bg-yellow-100 text-yellow-800"> <span className="mb-2 inline-flex items-center px-1.5 py-0.5 rounded-sm text-xs font-medium bg-yellow-100 text-yellow-800">
@ -70,26 +56,20 @@ export default function Bookings() {
<div className="text-sm text-gray-900"> <div className="text-sm text-gray-900">
{dayjs(booking.startTime).format("D MMMM YYYY")}:{" "} {dayjs(booking.startTime).format("D MMMM YYYY")}:{" "}
<small className="text-sm text-gray-500"> <small className="text-sm text-gray-500">
{dayjs(booking.startTime).format("HH:mm")} -{" "} {dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
{dayjs(booking.endTime).format("HH:mm")}
</small> </small>
</div> </div>
</div> </div>
{booking.attendees.length !== 0 && ( {booking.attendees.length !== 0 && (
<div className="text-sm text-blue-500"> <div className="text-sm text-blue-500">
<a href={"mailto:" + booking.attendees[0].email}> <a href={"mailto:" + booking.attendees[0].email}>{booking.attendees[0].email}</a>
{booking.attendees[0].email}
</a>
</div> </div>
)} )}
</td> </td>
<td className="hidden sm:table-cell px-6 py-4 whitespace-nowrap"> <td className="hidden sm:table-cell px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900"> <div className="text-sm text-gray-900">{dayjs(booking.startTime).format("D MMMM YYYY")}</div>
{dayjs(booking.startTime).format("D MMMM YYYY")}
</div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
{dayjs(booking.startTime).format("HH:mm")} -{" "} {dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
{dayjs(booking.endTime).format("HH:mm")}
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
@ -97,15 +77,17 @@ export default function Bookings() {
<> <>
<div className="space-x-2 hidden lg:block"> <div className="space-x-2 hidden lg:block">
<Button <Button
onClick={() => confirmBookingHandler(booking, true)} onClick={() => mutation.mutate(true)}
StartIcon={CheckIcon} StartIcon={CheckIcon}
color="secondary"> color="secondary"
disabled={mutation.isLoading}>
Confirm Confirm
</Button> </Button>
<Button <Button
onClick={() => confirmBookingHandler(booking, false)} onClick={() => mutation.mutate(false)}
StartIcon={BanIcon} StartIcon={BanIcon}
color="secondary"> color="secondary"
disabled={mutation.isLoading}>
Reject Reject
</Button> </Button>
</div> </div>
@ -134,11 +116,9 @@ export default function Bookings() {
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<span <span
onClick={() => confirmBookingHandler(booking, true)} onClick={() => mutation.mutate(true)}
className={classNames( className={classNames(
active active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700",
? "bg-neutral-100 text-neutral-900"
: "text-neutral-700",
"group flex items-center px-4 py-2 text-sm font-medium" "group flex items-center px-4 py-2 text-sm font-medium"
)}> )}>
<CheckIcon <CheckIcon
@ -152,11 +132,9 @@ export default function Bookings() {
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<span <span
onClick={() => confirmBookingHandler(booking, false)} onClick={() => mutation.mutate(false)}
className={classNames( className={classNames(
active active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700",
? "bg-neutral-100 text-neutral-900"
: "text-neutral-700",
"group flex items-center px-4 py-2 text-sm w-full font-medium" "group flex items-center px-4 py-2 text-sm w-full font-medium"
)}> )}>
<BanIcon <BanIcon
@ -185,10 +163,7 @@ export default function Bookings() {
color="secondary"> color="secondary">
Cancel Cancel
</Button> </Button>
<Button <Button href={"reschedule/" + booking.uid} StartIcon={ClockIcon} color="secondary">
href={"reschedule/" + booking.uid}
StartIcon={ClockIcon}
color="secondary">
Reschedule Reschedule
</Button> </Button>
</div> </div>
@ -218,15 +193,9 @@ export default function Bookings() {
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<a <a
href={ href={process.env.NEXT_PUBLIC_APP_URL + "/../cancel/" + booking.uid}
process.env.NEXT_PUBLIC_APP_URL +
"/../cancel/" +
booking.uid
}
className={classNames( className={classNames(
active active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700",
? "bg-neutral-100 text-neutral-900"
: "text-neutral-700",
"group flex items-center px-4 py-2 text-sm font-medium" "group flex items-center px-4 py-2 text-sm font-medium"
)}> )}>
<XIcon <XIcon
@ -240,15 +209,9 @@ export default function Bookings() {
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<a <a
href={ href={process.env.NEXT_PUBLIC_APP_URL + "/../reschedule/" + booking.uid}
process.env.NEXT_PUBLIC_APP_URL +
"/../reschedule/" +
booking.uid
}
className={classNames( className={classNames(
active active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700",
? "bg-neutral-100 text-neutral-900"
: "text-neutral-700",
"group flex items-center px-4 py-2 text-sm w-full font-medium" "group flex items-center px-4 py-2 text-sm w-full font-medium"
)}> )}>
<ClockIcon <ClockIcon
@ -267,11 +230,40 @@ export default function Bookings() {
</Menu> </Menu>
</> </>
)} )}
{!booking.confirmed && booking.rejected && ( {!booking.confirmed && booking.rejected && <div className="text-sm text-gray-500">Rejected</div>}
<div className="text-sm text-gray-500">Rejected</div>
)}
</td> </td>
</tr> </tr>
);
}
export default function Bookings() {
const query = trpc.useQuery(["viewer.bookings"]);
const bookings = query.data;
return (
<Shell heading="Bookings" subtitle="See upcoming and past events booked through your event type links.">
<div className="-mx-4 sm:mx-auto flex flex-col">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
{query.status === "error" && (
<Alert severity="error" title="Something went wrong" message={query.error.message} />
)}
{query.status === "loading" && <Loader />}
{bookings &&
(bookings.length === 0 ? (
<EmptyScreen
Icon={CalendarIcon}
headline="No upcoming bookings, yet"
description="You have no upcoming bookings. As soon as someone books a time with you it will show up here."
/>
) : (
<div className="border border-gray-200 overflow-hidden border-b rounded-sm">
<table className="min-w-full divide-y divide-gray-200">
<tbody className="bg-white divide-y divide-gray-200" data-testid="bookings">
{bookings
.filter((booking) => booking.status !== BookingStatus.CANCELLED)
.map((booking) => (
<BookingListItem key={booking.id} {...booking} />
))} ))}
</tbody> </tbody>
</table> </table>

View File

@ -6,57 +6,13 @@ import { hashPassword } from "../lib/auth";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
async function createBookingForEventType(opts: {
uid: string;
title: string;
slug: string;
startTime: Date | string;
endTime: Date | string;
userEmail: string;
}) {
const eventType = await prisma.eventType.findFirst({
where: {
slug: opts.slug,
},
});
if (!eventType) {
// should not happen
throw new Error("Eventtype missing");
}
const bookingData: Prisma.BookingCreateArgs["data"] = {
uid: opts.uid,
title: opts.title,
startTime: opts.startTime,
endTime: opts.endTime,
user: {
connect: {
email: opts.userEmail,
},
},
attendees: {
create: {
email: opts.userEmail,
name: "Some name",
timeZone: "Europe/London",
},
},
eventType: {
connect: {
id: eventType.id,
},
},
};
await prisma.booking.create({
data: bookingData,
});
}
async function createUserAndEventType(opts: { async function createUserAndEventType(opts: {
user: { email: string; password: string; username: string; plan: UserPlan; name: string }; user: { email: string; password: string; username: string; plan: UserPlan; name: string };
eventTypes: Array<Prisma.EventTypeCreateArgs["data"]>; eventTypes: Array<
Prisma.EventTypeCreateInput & {
_bookings?: Prisma.BookingCreateInput[];
}
>;
}) { }) {
const userData: Prisma.UserCreateArgs["data"] = { const userData: Prisma.UserCreateArgs["data"] = {
...opts.user, ...opts.user,
@ -73,8 +29,8 @@ async function createUserAndEventType(opts: {
console.log( console.log(
`👤 Upserted '${opts.user.username}' with email "${opts.user.email}" & password "${opts.user.password}". Booking page 👉 http://localhost:3000/${opts.user.username}` `👤 Upserted '${opts.user.username}' with email "${opts.user.email}" & password "${opts.user.password}". Booking page 👉 http://localhost:3000/${opts.user.username}`
); );
for (const rawData of opts.eventTypes) { for (const eventTypeInput of opts.eventTypes) {
const eventTypeData: Prisma.EventTypeCreateArgs["data"] = { ...rawData }; const { _bookings: bookingInputs = [], ...eventTypeData } = eventTypeInput;
eventTypeData.userId = user.id; eventTypeData.userId = user.id;
eventTypeData.users = { connect: { id: user.id } }; eventTypeData.users = { connect: { id: user.id } };
@ -93,21 +49,48 @@ async function createUserAndEventType(opts: {
}); });
if (eventType) { if (eventType) {
await prisma.eventType.update({ console.log(
where: { `\t📆 Event type ${eventTypeData.slug} already seems seeded - http://localhost:3000/${user.username}/${eventTypeData.slug}`
id: eventType.id, );
}, continue;
data: eventTypeData,
});
} else {
await prisma.eventType.create({
data: eventTypeData,
});
} }
const { id } = await prisma.eventType.create({
data: eventTypeData,
});
console.log( console.log(
`\t📆 Event type ${eventTypeData.slug}, length ${eventTypeData.length}: http://localhost:3000/${user.username}/${eventTypeData.slug}` `\t📆 Event type ${eventTypeData.slug}, length ${eventTypeData.length}min - http://localhost:3000/${user.username}/${eventTypeData.slug}`
); );
for (const bookingInput of bookingInputs) {
await prisma.booking.create({
data: {
...bookingInput,
user: {
connect: {
email: opts.user.email,
},
},
attendees: {
create: {
email: opts.user.email,
name: opts.user.name,
timeZone: "Europe/London",
},
},
eventType: {
connect: {
id,
},
},
confirmed: bookingInput.confirmed,
},
});
console.log(
`\t\t☎ Created booking ${bookingInput.title} at ${new Date(
bookingInput.startTime
).toLocaleDateString()}`
);
}
} }
} }
@ -170,6 +153,21 @@ async function main() {
title: "30min", title: "30min",
slug: "30min", slug: "30min",
length: 30, length: 30,
_bookings: [
{
uid: uuid(),
title: "30min",
startTime: dayjs().add(1, "day").toDate(),
endTime: dayjs().add(1, "day").add(30, "minutes").toDate(),
},
{
uid: uuid(),
title: "30min",
startTime: dayjs().add(2, "day").toDate(),
endTime: dayjs().add(2, "day").add(30, "minutes").toDate(),
confirmed: false,
},
],
}, },
{ {
title: "60min", title: "60min",
@ -179,15 +177,6 @@ async function main() {
], ],
}); });
await createBookingForEventType({
title: "30min",
slug: "30min",
startTime: dayjs().add(1, "day").toDate(),
endTime: dayjs().add(1, "day").add(60, "minutes").toDate(),
uid: uuid(),
userEmail: "pro@example.com",
});
await createUserAndEventType({ await createUserAndEventType({
user: { user: {
email: "trial@example.com", email: "trial@example.com",