Skip to content

Commit

Permalink
Merge pull request #937 from Bhavyajain21/feat/calender-view
Browse files Browse the repository at this point in the history
feat: Calender View
  • Loading branch information
DonKoko authored May 10, 2024
2 parents 23a51b9 + 0e85e20 commit 4bc5ae9
Show file tree
Hide file tree
Showing 18 changed files with 616 additions and 9 deletions.
1 change: 0 additions & 1 deletion app/components/booking/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export const NewBookingFormSchema = (
) =>
z
.object({
// @TODO this is why its not working, because the id is required even on new bookings
id:
inputFieldIsDisabled || isNewBooking
? z.string().optional()
Expand Down
1 change: 0 additions & 1 deletion app/components/booking/page-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { BookingForm } from "./form";
export function BookingPageContent() {
const { booking, teamMembers } = useLoaderData<typeof loader>();

// @TODO fix this
const bookingStatus = useBookingStatus(booking);

return (
Expand Down
19 changes: 19 additions & 0 deletions app/components/icons/library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,25 @@ export const CalendarIcon = (props: SVGProps<SVGSVGElement>) => (
</svg>
);

export const BookingsIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 20 22"
fill="none"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7H1m13-6v3M6 1v3m4 13v-6m-3 3h6m-7.2 7h8.4c1.68 0 2.52 0 3.162-.327a3 3 0 0 0 1.311-1.311C19 18.72 19 17.88 19 16.2V7.8c0-1.68 0-2.52-.327-3.162a3 3 0 0 0-1.311-1.311C16.72 3 15.88 3 14.2 3H5.8c-1.68 0-2.52 0-3.162.327a3 3 0 0 0-1.311 1.311C1 5.28 1 6.12 1 7.8v8.4c0 1.68 0 2.52.327 3.162a3 3 0 0 0 1.311 1.311C3.28 21 4.12 21 5.8 21Z"
/>
</svg>
);

export const CustomFiedIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
width="16"
Expand Down
5 changes: 4 additions & 1 deletion app/components/shared/icons-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
GraphIcon,
ScanQRIcon,
SwitchIcon,
BookingsIcon,
} from "../icons/library";

/** The possible options for icons to be rendered in the button */
Expand Down Expand Up @@ -69,7 +70,8 @@ export type IconType =
| "calendar"
| "graph"
| "scanQR"
| "switch";
| "switch"
| "bookings";

type IconsMap = {
[key in IconType]: JSX.Element;
Expand Down Expand Up @@ -106,6 +108,7 @@ export const iconsMap: IconsMap = {
send: <SendIcon />,
user: <UserIcon />,
calendar: <CalendarIcon className="size-5" />,
bookings: <BookingsIcon />,
graph: <GraphIcon />,
scanQR: <ScanQRIcon />,
switch: <SwitchIcon />,
Expand Down
1 change: 0 additions & 1 deletion app/database/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ enum NoteType {
}

model Qr {
// @TODO we might need to change this to shorten the length of the id
id String @id @default(cuid())
// Version of the QR code based on spec from Denso wave
Expand Down
5 changes: 5 additions & 0 deletions app/hooks/use-main-menu-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export function useMainMenuItems() {
},
{
icon: <Icon icon="calendar" />,
to: "calendar",
label: "Calendar",
},
{
icon: <Icon icon="bookings" />,
to: "bookings",
label: "Bookings (beta)",
},
Expand Down
72 changes: 72 additions & 0 deletions app/modules/booking/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {
} from "@prisma/client";
import { db } from "~/database/db.server";
import { bookingUpdatesTemplateString } from "~/emails/bookings-updates-template";
import { getStatusClasses, isOneDayEvent } from "~/utils/calendar";
import { calcTimeDifference } from "~/utils/date-fns";
import { sendNotification } from "~/utils/emitter/send-notification.server";
import type { ErrorLabel } from "~/utils/error";
import { ShelfError } from "~/utils/error";
import { getCurrentSearchParams } from "~/utils/http.server";
import { Logger } from "~/utils/logger";
import { sendEmail } from "~/utils/mail.server";
import { scheduler } from "~/utils/scheduler.server";
Expand Down Expand Up @@ -452,6 +454,7 @@ export async function getBookings(params: {
bookingFrom?: Booking["from"] | null;
bookingTo?: Booking["to"] | null;
userId: Booking["creatorId"];
extraInclude?: Prisma.BookingInclude;
}) {
const {
organizationId,
Expand All @@ -466,6 +469,7 @@ export async function getBookings(params: {
excludeBookingIds,
bookingFrom,
userId,
extraInclude,
} = params;

try {
Expand Down Expand Up @@ -571,6 +575,7 @@ export async function getBookings(params: {
profilePicture: true,
},
},
...(extraInclude || undefined),
},
orderBy: { from: "asc" },
}),
Expand Down Expand Up @@ -768,6 +773,73 @@ export async function getBooking(
}
}

export async function getBookingsForCalendar(params: {
request: Request;
organizationId: Organization["id"];
userId: string;
isSelfService: boolean;
}) {
const { request, organizationId, userId, isSelfService = false } = params;
const searchParams = getCurrentSearchParams(request);

const start = searchParams.get("start") as string;
const end = searchParams.get("end") as string;

try {
const { bookings } = await getBookings({
organizationId,
page: 1,
perPage: 1000,
userId,
bookingFrom: new Date(start),
bookingTo: new Date(end),
...(isSelfService && {
// If the user is self service, we only show bookings that belong to that user)
custodianUserId: userId,
}),
extraInclude: {
custodianTeamMember: true,
custodianUser: true,
},
});

const events = bookings
.filter((booking) => booking.from && booking.to)
.map((booking) => {
const custodianName = booking?.custodianUser
? `${booking.custodianUser.firstName} ${booking.custodianUser.lastName}`
: booking.custodianTeamMember?.name;

return {
title: `${booking.name} | ${custodianName}`,
start: (booking.from as Date).toISOString(),
end: (booking.to as Date).toISOString(),
url: `/bookings/${booking.id}`,
classNames: [
`bookingId-${booking.id}`,
...getStatusClasses(
booking.status,
isOneDayEvent(booking.from as Date, booking.to as Date)
),
],
extendedProps: {
status: booking.status,
id: booking.id,
},
};
});

return events;
} catch (cause) {
throw new ShelfError({
cause,
message:
"Something went wrong while fetching the bookings for the calendar. Please try again or contact support.",
additionalData: { ...params },
label,
});
}
}
export async function createNotesForBookingUpdate(
intent: BookingUpdateIntent,
booking: Booking & { assets: Pick<Asset, "id">[] },
Expand Down
1 change: 0 additions & 1 deletion app/modules/custom-field/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ export async function upsertCustomField(
});

if (!existingCustomField) {
// @TODO not sure how to handle this case
const newCustomField = await createCustomField(def);
customFields[def.name] = newCustomField;
} else {
Expand Down
6 changes: 4 additions & 2 deletions app/modules/user/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,10 @@ export async function createUser(
organizationIds.push(organizationId);
}

// @TODO this is weird and organizationIds is not used
// We haev to figure out why its being called 2 times and whats going on here
/** Create user organization association
* 1. For the personal org
* 2. For the org that the user is being attached to
*/
await Promise.all([
createUserOrgAssociation(tx, {
userId: user.id,
Expand Down
2 changes: 1 addition & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function Document({ children, title }: PropsWithChildren<{ title?: string }>) {
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<ClientHintCheck nonce={nonce} />

<style data-fullcalendar />
<Meta />
{title ? <title>{title}</title> : null}
<Links />
Expand Down
2 changes: 1 addition & 1 deletion app/routes/_layout+/bookings.$bookingId.add-assets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export default function AddAssetsToNewBooking() {
* Initially here we were using useHydrateAtoms, but we found that it was causing the selected assets to stay the same as it hydrates only once per store and we dont have different stores per booking
* So we do a manual effect to set the selected assets to the booking assets ids
* I would still rather use the useHydrateAtoms, but it's not working as expected.
* @TODO Going to ask here: https://github.com/pmndrs/jotai/discussions/669
* https://github.com/pmndrs/jotai/discussions/669
*/
useEffect(() => {
setSelectedAssets(bookingAssetsIds);
Expand Down
44 changes: 44 additions & 0 deletions app/routes/_layout+/calendar.events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { OrganizationRoles } from "@prisma/client";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";

import { getBookingsForCalendar } from "~/modules/booking/service.server";
import { makeShelfError } from "~/utils/error";
import { error } from "~/utils/http.server";
import {
PermissionAction,
PermissionEntity,
} from "~/utils/permissions/permission.validator.server";
import { requirePermission } from "~/utils/roles.server";

// Loader Function to Return Bookings Data
export const loader = async ({ request, context }: LoaderFunctionArgs) => {
const authSession = context.getSession();
const { userId } = authSession;

try {
const { organizationId, role } = await requirePermission({
userId: authSession.userId,
request,
entity: PermissionEntity.booking,
action: PermissionAction.read,
});
const isSelfService = role === OrganizationRoles.SELF_SERVICE;

const calendarEvents = await getBookingsForCalendar({
request,
organizationId,
userId,
isSelfService,
});

return new Response(JSON.stringify(calendarEvents), {
headers: {
"Content-Type": "application/json",
},
});
} catch (cause) {
const reason = makeShelfError(cause);
throw json(error(reason), { status: reason.status });
}
};
Loading

0 comments on commit 4bc5ae9

Please sign in to comment.