Skip to content

Commit

Permalink
Merge pull request #1621 from rockingrohit9639/feature/booking-client…
Browse files Browse the repository at this point in the history
…-side-modal

feat(create-booking): create client side modal for new booking
  • Loading branch information
DonKoko authored Feb 6, 2025
2 parents 67d5650 + 8f0969c commit 68f3f75
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 30 deletions.
80 changes: 80 additions & 0 deletions app/components/booking/create-booking-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { cloneElement, useState } from "react";
import type { TeamMember } from "@prisma/client";
import { useLoaderData } from "@remix-run/react";
import { CalendarRangeIcon } from "lucide-react";
import { useSearchParams } from "~/hooks/search-params";
import { getBookingDefaultStartEndTimes } from "~/utils/date-fns";
import { tw } from "~/utils/tw";
import { BookingForm } from "./form";
import { Dialog, DialogPortal } from "../layout/dialog";

type CreateBookingDialogProps = {
className?: string;
trigger: React.ReactElement<{ onClick: () => void }>;
};

export default function CreateBookingDialog({
className,
trigger,
}: CreateBookingDialogProps) {
const { teamMembers, isSelfServiceOrBase } = useLoaderData<{
teamMembers: TeamMember[];
isSelfServiceOrBase: boolean;
}>();

// The loader already takes care of returning only the current user so we just get the first and only element in the array
const custodianRef = isSelfServiceOrBase ? teamMembers[0]?.id : undefined;

const [isDialogOpen, setIsDialogOpen] = useState(false);
const [searchParams] = useSearchParams();

const assetIds = searchParams.getAll("assetId");

const { startDate, endDate } = getBookingDefaultStartEndTimes();

function openDialog() {
setIsDialogOpen(true);
}

function closeDialog() {
setIsDialogOpen(false);
}

return (
<>
{cloneElement(trigger, { onClick: openDialog })}

<DialogPortal>
<Dialog
className={tw("w-[480px] overflow-auto md:h-screen", className)}
open={isDialogOpen}
onClose={closeDialog}
title={
<div className="mt-4 inline-flex items-center justify-center rounded-full border-4 border-solid border-primary-50 bg-primary-100 p-1.5 text-primary">
<CalendarRangeIcon />
</div>
}
>
<div className="px-6 py-4">
<div className="mb-5">
<h4>Create new booking</h4>
<p>
Choose a name for your booking, select a start and end time and
choose the custodian. Based on the selected information, asset
availability will be determined.
</p>
</div>

<BookingForm
startDate={startDate}
endDate={endDate}
assetIds={assetIds.length ? assetIds : undefined}
custodianRef={custodianRef}
action="/bookings/new"
/>
</div>
</Dialog>
</DialogPortal>
</>
);
}
9 changes: 8 additions & 1 deletion app/components/booking/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ type BookingFormData = {
bookingFlags?: BookingFlags;
assetIds?: string[] | null;
description?: string | null;

/**
* In case if the form is rendered outside of /edit or /new booking,
* then we can pass `action` to submit form
*/
action?: string;
};

export function BookingForm({
Expand All @@ -136,6 +142,7 @@ export function BookingForm({
bookingFlags,
assetIds,
description,
action,
}: BookingFormData) {
const navigation = useNavigation();
const { teamMembers } = useLoaderData<typeof loader>();
Expand Down Expand Up @@ -191,7 +198,7 @@ export function BookingForm({

return (
<div>
<Form ref={zo.ref} method="post">
<Form ref={zo.ref} method="post" action={action}>
{/* Hidden input for expired state. Helps is know what status we should set on the server, when the booking is getting checked out */}
{isExpired && <input type="hidden" name="isExpired" value="true" />}

Expand Down
39 changes: 39 additions & 0 deletions app/components/calendar/title-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useMemo } from "react";
import { useLoaderData } from "@remix-run/react";
import { useViewportHeight } from "~/hooks/use-viewport-height";

type TitleContainerProps = {
className?: string;
calendarTitle?: string;
calendarSubtitle?: string;
calendarView: string;
};

export default function TitleContainer({
className,
calendarTitle,
calendarSubtitle,
calendarView,
}: TitleContainerProps) {
const { title } = useLoaderData<{ title: string }>();
const { isMd } = useViewportHeight();

const titleToRender = useMemo(() => {
if (calendarTitle) {
return calendarTitle;
}

return title;
}, [calendarTitle, title]);

return (
<div className={className}>
<div className="text-left font-sans text-lg font-semibold leading-[20px] ">
{titleToRender}
</div>
{!isMd || calendarView == "timeGridWeek" ? (
<div className="text-gray-600">{calendarSubtitle}</div>
) : null}
</div>
);
}
22 changes: 13 additions & 9 deletions app/routes/_layout+/bookings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Link, Outlet, useMatches, useNavigate } from "@remix-run/react";
import { ChevronRight } from "lucide-react";
import { AvailabilityBadge } from "~/components/booking/availability-label";
import BulkActionsDropdown from "~/components/booking/bulk-actions-dropdown";
import CreateBookingDialog from "~/components/booking/create-booking-dialog";
import { StatusFilter } from "~/components/booking/status-filter";
import DynamicDropdown from "~/components/dynamic-dropdown/dynamic-dropdown";
import { ErrorContent } from "~/components/errors";
Expand Down Expand Up @@ -185,6 +186,7 @@ export async function loader({ context, request }: LoaderFunctionArgs) {
perPage,
modelName,
...teamMembersData,
isSelfServiceOrBase,
}),
{
headers: [
Expand Down Expand Up @@ -272,15 +274,17 @@ export default function BookingsIndexPage({
>
{!isChildBookingsPage ? (
<Header>
<Button
to="new"
role="link"
aria-label={`new booking`}
data-test-id="createNewBooking"
prefetch="none"
>
New booking
</Button>
<CreateBookingDialog
trigger={
<Button
aria-label="new booking"
data-test-id="createNewBooking"
prefetch="none"
>
New booking
</Button>
}
/>
</Header>
) : null}

Expand Down
60 changes: 40 additions & 20 deletions app/routes/_layout+/calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import { HoverCardPortal } from "@radix-ui/react-hover-card";
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import { Link } from "@remix-run/react";
import { ClientOnly } from "remix-utils/client-only";
import CreateBookingDialog from "~/components/booking/create-booking-dialog";
import TitleContainer from "~/components/calendar/title-container";
import FallbackLoading from "~/components/dashboard/fallback-loading";
import { ErrorContent } from "~/components/errors";
import { ArrowRightIcon } from "~/components/icons/library";
Expand All @@ -30,7 +32,9 @@ import {
import { Spinner } from "~/components/shared/spinner";
import { UserBadge } from "~/components/shared/user-badge";
import When from "~/components/when/when";
import { hasGetAllValue } from "~/hooks/use-model-filters";
import { useViewportHeight } from "~/hooks/use-viewport-height";
import { getTeamMemberForCustodianFilter } from "~/modules/team-member/service.server";
import calendarStyles from "~/styles/layout/calendar.css?url";
import { appendToMetaTitle } from "~/utils/append-to-meta-title";
import { bookingStatusColorMap } from "~/utils/bookings";
Expand All @@ -41,7 +45,8 @@ import {
} from "~/utils/calendar";
import { getWeekStartingAndEndingDates } from "~/utils/date-fns";
import { makeShelfError, ShelfError } from "~/utils/error";
import { data, error } from "~/utils/http.server";
import { data, error, getCurrentSearchParams } from "~/utils/http.server";
import { getParamsValues } from "~/utils/list";
import { isPersonalOrg } from "~/utils/organization";
import {
PermissionAction,
Expand Down Expand Up @@ -77,12 +82,13 @@ export const loader = async ({ request, context }: LoaderFunctionArgs) => {
const { userId } = authSession;

try {
const { currentOrganization } = await requirePermission({
userId,
request,
entity: PermissionEntity.booking,
action: PermissionAction.read,
});
const { isSelfServiceOrBase, currentOrganization, organizationId } =
await requirePermission({
userId,
request,
entity: PermissionEntity.booking,
action: PermissionAction.read,
});

if (isPersonalOrg(currentOrganization)) {
throw new ShelfError({
Expand All @@ -107,7 +113,20 @@ export const loader = async ({ request, context }: LoaderFunctionArgs) => {

const title = `${currentMonth} ${currentYear}`;

return json(data({ header, title }));
const searchParams = getCurrentSearchParams(request);
const { teamMemberIds } = getParamsValues(searchParams);

const teamMembersData = await getTeamMemberForCustodianFilter({
organizationId,
selectedTeamMembers: teamMemberIds,
getAll:
searchParams.has("getAll") &&
hasGetAllValue(searchParams, "teamMember"),
isSelfService: isSelfServiceOrBase, // we can assume this is false because this view is not allowed for
userId,
});

return json(data({ header, title, ...teamMembersData }));
} catch (cause) {
const reason = makeShelfError(cause);
throw json(error(reason), { status: reason.status });
Expand All @@ -125,11 +144,10 @@ export const DATE_FORMAT_OPTIONS = {

// Calendar Component
export default function Calendar() {
const { title } = useLoaderData<typeof loader>();
const { isMd } = useViewportHeight();
const [startingDay, endingDay] = getWeekStartingAndEndingDates(new Date());
const [_error, setError] = useState<string | null>(null);
const [calendarTitle, setCalendarTitle] = useState(title);
const [calendarTitle, setCalendarTitle] = useState<string>();
const [calendarSubtitle, setCalendarSubtitle] = useState(
isMd ? undefined : `${startingDay} - ${endingDay}`
);
Expand Down Expand Up @@ -252,17 +270,19 @@ export default function Calendar() {

return (
<>
<Header hidePageDescription={true} />
<Header hidePageDescription>
<CreateBookingDialog
trigger={<Button aria-label="new booking">New booking</Button>}
/>
</Header>

<div className="mt-4">
<div className="flex items-center justify-between gap-4 rounded-t-md border bg-white px-4 py-3">
<div>
<div className="text-left font-sans text-lg font-semibold leading-[20px] ">
{calendarTitle}
</div>
{!isMd || calendarView == "timeGridWeek" ? (
<div className="text-gray-600">{calendarSubtitle}</div>
) : null}
</div>
<TitleContainer
calendarTitle={calendarTitle}
calendarSubtitle={calendarSubtitle}
calendarView={calendarView}
/>

<div className="flex items-center">
<div ref={ripple} className="mr-3 flex justify-center">
Expand Down

0 comments on commit 68f3f75

Please sign in to comment.