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 dayjs from "dayjs";
import { Fragment } from "react";
import { useMutation } from "react-query";
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 Loader from "@components/Loader";
@ -15,11 +17,12 @@ import Shell from "@components/Shell";
import { Alert } from "@components/ui/Alert";
import { Button } from "@components/ui/Button";
export default function Bookings() {
const query = trpc.useQuery(["viewer.bookings"]);
const bookings = query.data;
type BookingItem = inferQueryOutput<"viewer.bookings">[number];
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", {
method: "PATCH",
body: JSON.stringify({ id: booking.id, confirmed: confirm }),
@ -27,35 +30,18 @@ export default function Bookings() {
"Content-Type": "application/json",
},
});
if (res.ok) {
await query.refetch();
if (!res.ok) {
throw new HttpError({ statusCode: res.status });
}
},
{
async onSettled() {
await utils.invalidateQuery(["viewer.bookings"]);
},
}
);
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) => (
<tr key={booking.id}>
<tr>
<td className={"px-6 py-4" + (booking.rejected ? " line-through" : "")}>
{!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">
@ -70,26 +56,20 @@ export default function Bookings() {
<div className="text-sm text-gray-900">
{dayjs(booking.startTime).format("D MMMM YYYY")}:{" "}
<small className="text-sm text-gray-500">
{dayjs(booking.startTime).format("HH:mm")} -{" "}
{dayjs(booking.endTime).format("HH:mm")}
{dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
</small>
</div>
</div>
{booking.attendees.length !== 0 && (
<div className="text-sm text-blue-500">
<a href={"mailto:" + booking.attendees[0].email}>
{booking.attendees[0].email}
</a>
<a href={"mailto:" + booking.attendees[0].email}>{booking.attendees[0].email}</a>
</div>
)}
</td>
<td className="hidden sm:table-cell px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{dayjs(booking.startTime).format("D MMMM YYYY")}
</div>
<div className="text-sm text-gray-900">{dayjs(booking.startTime).format("D MMMM YYYY")}</div>
<div className="text-sm text-gray-500">
{dayjs(booking.startTime).format("HH:mm")} -{" "}
{dayjs(booking.endTime).format("HH:mm")}
{dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
</div>
</td>
<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">
<Button
onClick={() => confirmBookingHandler(booking, true)}
onClick={() => mutation.mutate(true)}
StartIcon={CheckIcon}
color="secondary">
color="secondary"
disabled={mutation.isLoading}>
Confirm
</Button>
<Button
onClick={() => confirmBookingHandler(booking, false)}
onClick={() => mutation.mutate(false)}
StartIcon={BanIcon}
color="secondary">
color="secondary"
disabled={mutation.isLoading}>
Reject
</Button>
</div>
@ -134,11 +116,9 @@ export default function Bookings() {
<Menu.Item>
{({ active }) => (
<span
onClick={() => confirmBookingHandler(booking, true)}
onClick={() => mutation.mutate(true)}
className={classNames(
active
? "bg-neutral-100 text-neutral-900"
: "text-neutral-700",
active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700",
"group flex items-center px-4 py-2 text-sm font-medium"
)}>
<CheckIcon
@ -152,11 +132,9 @@ export default function Bookings() {
<Menu.Item>
{({ active }) => (
<span
onClick={() => confirmBookingHandler(booking, false)}
onClick={() => mutation.mutate(false)}
className={classNames(
active
? "bg-neutral-100 text-neutral-900"
: "text-neutral-700",
active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700",
"group flex items-center px-4 py-2 text-sm w-full font-medium"
)}>
<BanIcon
@ -185,10 +163,7 @@ export default function Bookings() {
color="secondary">
Cancel
</Button>
<Button
href={"reschedule/" + booking.uid}
StartIcon={ClockIcon}
color="secondary">
<Button href={"reschedule/" + booking.uid} StartIcon={ClockIcon} color="secondary">
Reschedule
</Button>
</div>
@ -218,15 +193,9 @@ export default function Bookings() {
<Menu.Item>
{({ active }) => (
<a
href={
process.env.NEXT_PUBLIC_APP_URL +
"/../cancel/" +
booking.uid
}
href={process.env.NEXT_PUBLIC_APP_URL + "/../cancel/" + booking.uid}
className={classNames(
active
? "bg-neutral-100 text-neutral-900"
: "text-neutral-700",
active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700",
"group flex items-center px-4 py-2 text-sm font-medium"
)}>
<XIcon
@ -240,15 +209,9 @@ export default function Bookings() {
<Menu.Item>
{({ active }) => (
<a
href={
process.env.NEXT_PUBLIC_APP_URL +
"/../reschedule/" +
booking.uid
}
href={process.env.NEXT_PUBLIC_APP_URL + "/../reschedule/" + booking.uid}
className={classNames(
active
? "bg-neutral-100 text-neutral-900"
: "text-neutral-700",
active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700",
"group flex items-center px-4 py-2 text-sm w-full font-medium"
)}>
<ClockIcon
@ -267,11 +230,40 @@ export default function Bookings() {
</Menu>
</>
)}
{!booking.confirmed && booking.rejected && (
<div className="text-sm text-gray-500">Rejected</div>
)}
{!booking.confirmed && booking.rejected && <div className="text-sm text-gray-500">Rejected</div>}
</td>
</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>
</table>

View File

@ -6,57 +6,13 @@ import { hashPassword } from "../lib/auth";
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: {
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"] = {
...opts.user,
@ -73,8 +29,8 @@ async function createUserAndEventType(opts: {
console.log(
`👤 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) {
const eventTypeData: Prisma.EventTypeCreateArgs["data"] = { ...rawData };
for (const eventTypeInput of opts.eventTypes) {
const { _bookings: bookingInputs = [], ...eventTypeData } = eventTypeInput;
eventTypeData.userId = user.id;
eventTypeData.users = { connect: { id: user.id } };
@ -93,21 +49,48 @@ async function createUserAndEventType(opts: {
});
if (eventType) {
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: eventTypeData,
});
} else {
await prisma.eventType.create({
data: eventTypeData,
});
console.log(
`\t📆 Event type ${eventTypeData.slug} already seems seeded - http://localhost:3000/${user.username}/${eventTypeData.slug}`
);
continue;
}
const { id } = await prisma.eventType.create({
data: eventTypeData,
});
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",
slug: "30min",
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",
@ -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({
user: {
email: "trial@example.com",