From 2eb374a41433864c895b030a3a62df5068aa9df9 Mon Sep 17 00:00:00 2001
From: Donkoko
Date: Wed, 3 Apr 2024 16:47:28 +0300
Subject: [PATCH 01/10] testing feature that allows users to add a specific
booking to their calendar
---
.../_layout+/bookings.$bookingId.cal[.ics].ts | 82 +++++++++++++++++++
app/routes/_layout+/bookings.$bookingId.tsx | 43 ++++++----
2 files changed, 109 insertions(+), 16 deletions(-)
create mode 100644 app/routes/_layout+/bookings.$bookingId.cal[.ics].ts
diff --git a/app/routes/_layout+/bookings.$bookingId.cal[.ics].ts b/app/routes/_layout+/bookings.$bookingId.cal[.ics].ts
new file mode 100644
index 00000000..62cdf305
--- /dev/null
+++ b/app/routes/_layout+/bookings.$bookingId.cal[.ics].ts
@@ -0,0 +1,82 @@
+import { OrganizationRoles } from "@prisma/client";
+import { json, type LoaderFunctionArgs } from "@remix-run/node";
+import { z } from "zod";
+import { getBooking } from "~/modules/booking/service.server";
+import { makeShelfError, ShelfError } from "~/utils/error";
+import { error, getParams } from "~/utils/http.server";
+import { PermissionAction, PermissionEntity } from "~/utils/permissions/types";
+import { requirePermission } from "~/utils/roles.server";
+
+export async function loader({ request, context, params }: LoaderFunctionArgs) {
+ const authSession = context.getSession();
+ const { userId } = authSession;
+ const { bookingId } = getParams(params, z.object({ bookingId: z.string() }), {
+ additionalData: { userId },
+ });
+
+ try {
+ /** Check if the current user is allowed to read booking */
+ const { organizationId, role } = await requirePermission({
+ userId: authSession.userId,
+ request,
+ entity: PermissionEntity.booking,
+ action: PermissionAction.read,
+ });
+ const booking = await getBooking({
+ id: bookingId,
+ organizationId: organizationId,
+ });
+
+ /** Check if the user is self service */
+ const isSelfService = role === OrganizationRoles.SELF_SERVICE;
+
+ /** For self service users, we only allow them to read their own bookings */
+ if (isSelfService && booking.custodianUserId !== authSession.userId) {
+ throw new ShelfError({
+ cause: null,
+ message:
+ "You are not authorized to download the calendar for this booking",
+ status: 403,
+ label: "Booking",
+ shouldBeCaptured: false,
+ });
+ }
+
+ const ics = `
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//ZContent.net//Zap Calendar 1.0//EN
+CALSCALE:GREGORIAN
+METHOD:PUBLISH
+BEGIN:VEVENT
+SUMMARY:${booking.name}
+UID:c7614cff-3549-4a00-9152-d25cc1fe077d
+SEQUENCE:${Date.now()}
+STATUS:CONFIRMED
+TRANSP:TRANSPARENT
+RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=12
+DTSTART:${booking.from}
+DTEND:${booking.to}
+DTSTAMP:${Date.now()}
+CATEGORIES:U.S. Presidents,Civil War People
+LOCATION:Hodgenville\, Kentucky
+GEO:37.5739497;-85.7399606
+DESCRIPTION:Born February 12\, 1809\nSixteenth President (1861-1865)\n\n\n
+ \nhttp://AmericanHistoryCalendar.com
+URL:http://americanhistorycalendar.com/peoplecalendar/1,328-abraham-lincol
+ n
+END:VEVENT
+END:VCALENDAR`.trim();
+
+ return new Response(ics, {
+ headers: {
+ // @TODO add caching headers
+ "Content-Type": "text/calendar",
+ "Content-Disposition": `attachment; filename="${booking.name}.ics"`,
+ },
+ });
+ } catch (cause) {
+ const reason = makeShelfError(cause, { userId });
+ return json(error(reason), { status: reason.status });
+ }
+}
diff --git a/app/routes/_layout+/bookings.$bookingId.tsx b/app/routes/_layout+/bookings.$bookingId.tsx
index e6b6bed1..936312f3 100644
--- a/app/routes/_layout+/bookings.$bookingId.tsx
+++ b/app/routes/_layout+/bookings.$bookingId.tsx
@@ -15,7 +15,7 @@ import { NewBookingFormSchema } from "~/components/booking/form";
import ContextualModal from "~/components/layout/contextual-modal";
import Header from "~/components/layout/header";
import type { HeaderData } from "~/components/layout/header/types";
-import { Badge } from "~/components/shared";
+import { Badge, Button } from "~/components/shared";
import { db } from "~/database";
import { createNotes } from "~/modules/asset";
import {
@@ -84,6 +84,17 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) {
organizationId: organizationId,
});
+ /** For self service users, we only allow them to read their own bookings */
+ if (isSelfService && booking.custodianUserId !== authSession.userId) {
+ throw new ShelfError({
+ cause: null,
+ message: "You are not authorized to view this booking",
+ status: 403,
+ label: "Booking",
+ shouldBeCaptured: false,
+ });
+ }
+
const [teamMembers, org, assets] = await Promise.all([
/**
* We need to fetch the team members to be able to display them in the custodian dropdown.
@@ -169,16 +180,6 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) {
* This is useful for more consistent data in the front-end */
booking.assets = assets;
- /** For self service users, we only allow them to read their own bookings */
- if (isSelfService && booking.custodianUserId !== authSession.userId) {
- throw new ShelfError({
- cause: null,
- message: "You are not authorized to view this booking",
- status: 403,
- label: "Booking",
- });
- }
-
const { page, perPageParam } = getParamsValues(searchParams);
const cookie = await updateCookieWithPerPage(request, perPageParam);
const { perPage } = cookie;
@@ -533,11 +534,21 @@ export default function BookingEditPage() {
+
@@ -312,3 +320,30 @@ export function BookingForm({
);
}
+
+const AddToCalendar = () => {
+ const navigation = useNavigation();
+ const disabled = isFormProcessing(navigation.state);
+
+ return (
+
+
+
+
+
+
+ Download this booking as a calendar event
+
+
+
+ );
+};
diff --git a/app/components/shared/icons-map.tsx b/app/components/shared/icons-map.tsx
index ba45aded..d28619a9 100644
--- a/app/components/shared/icons-map.tsx
+++ b/app/components/shared/icons-map.tsx
@@ -1,3 +1,4 @@
+import { CalendarIcon } from "@radix-ui/react-icons";
import { Spinner } from "./spinner";
import {
@@ -57,7 +58,8 @@ export type Icon =
| "help"
| "profile"
| "send"
- | "user";
+ | "user"
+ | "calendar";
type IconsMap = {
[key in Icon]: JSX.Element;
@@ -91,6 +93,7 @@ export const iconsMap: IconsMap = {
logout: ,
send: ,
user: ,
+ calendar: ,
};
export default iconsMap;
diff --git a/app/routes/_layout+/bookings.$bookingId.tsx b/app/routes/_layout+/bookings.$bookingId.tsx
index d899621d..4a00d668 100644
--- a/app/routes/_layout+/bookings.$bookingId.tsx
+++ b/app/routes/_layout+/bookings.$bookingId.tsx
@@ -5,7 +5,7 @@ import type {
MetaFunction,
LoaderFunctionArgs,
} from "@remix-run/node";
-import { useLoaderData, useNavigation } from "@remix-run/react";
+import { useLoaderData } from "@remix-run/react";
import { useAtomValue } from "jotai";
import { DateTime } from "luxon";
import { z } from "zod";
@@ -15,13 +15,8 @@ import { NewBookingFormSchema } from "~/components/booking/form";
import ContextualModal from "~/components/layout/contextual-modal";
import Header from "~/components/layout/header";
import type { HeaderData } from "~/components/layout/header/types";
-import { Badge, Button } from "~/components/shared";
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "~/components/shared/tooltip";
+import { Badge } from "~/components/shared";
+
import { db } from "~/database";
import { createNotes } from "~/modules/asset";
import {
@@ -51,7 +46,6 @@ import {
import { dateForDateTimeInputValue } from "~/utils/date-fns";
import { sendNotification } from "~/utils/emitter/send-notification.server";
import { ShelfError, makeShelfError } from "~/utils/error";
-import { isFormProcessing } from "~/utils/form";
import { PermissionAction, PermissionEntity } from "~/utils/permissions";
import { requirePermission } from "~/utils/roles.server";
import { bookingStatusColorMap } from "./bookings";
@@ -547,7 +541,6 @@ export default function BookingEditPage() {
{booking.status}
-
}
/>
@@ -578,29 +571,3 @@ export default function BookingEditPage() {
>
);
}
-
-const AddToCalendar = () => {
- const navigation = useNavigation();
- const disabled = isFormProcessing(navigation.state);
-
- return (
-
-
-
-
-
-
- Download this booking as a calendar event
-
-
-
- );
-};
From fc89c2365304b4c5a6d813270af62cc310d824c5 Mon Sep 17 00:00:00 2001
From: Donkoko
Date: Fri, 5 Apr 2024 09:30:49 +0300
Subject: [PATCH 08/10] mend
---
app/utils/date-fns.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/utils/date-fns.ts b/app/utils/date-fns.ts
index bd9a0399..6efe9b1b 100644
--- a/app/utils/date-fns.ts
+++ b/app/utils/date-fns.ts
@@ -79,7 +79,6 @@ export function formatDatesForICal(date: Date, hints: ClientHint) {
return formatLocalDate(date, dateTimeFormat);
}
-
export function getBookingDefaultStartEndTimes() {
const now = new Date();
From 6ef4d5601c68c6a7ca71228f14e9dcefa606d6ae Mon Sep 17 00:00:00 2001
From: Donkoko
Date: Fri, 5 Apr 2024 10:29:46 +0300
Subject: [PATCH 09/10] fixing broken DTSTAMP value in ics file
---
app/routes/_layout+/bookings.$bookingId.cal[.ics].ts | 3 ++-
app/routes/_layout+/bookings.$bookingId.tsx | 6 +++---
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/app/routes/_layout+/bookings.$bookingId.cal[.ics].ts b/app/routes/_layout+/bookings.$bookingId.cal[.ics].ts
index 07dc58bc..0b7b0f2c 100644
--- a/app/routes/_layout+/bookings.$bookingId.cal[.ics].ts
+++ b/app/routes/_layout+/bookings.$bookingId.cal[.ics].ts
@@ -48,6 +48,7 @@ export async function loader({ request, context, params }: LoaderFunctionArgs) {
const formattedFromDate = formatDatesForICal(booking.from as Date, hints);
const formattedToDate = formatDatesForICal(booking.to as Date, hints);
+ const formattedDTSTAMP = formatDatesForICal(new Date(Date.now()), hints);
const ics = `
BEGIN:VCALENDAR
@@ -63,7 +64,7 @@ STATUS:CONFIRMED
TRANSP:TRANSPARENT
DTSTART:${formattedFromDate}
DTEND:${formattedToDate}
-DTSTAMP:${Date.now()}
+DTSTAMP:${formattedDTSTAMP}
CATEGORIES:Shelf.nu booking
LOCATION:shelf.nu
DESCRIPTION:Shelf.nu booking (Asset / Equipment checkout)
diff --git a/app/routes/_layout+/bookings.$bookingId.tsx b/app/routes/_layout+/bookings.$bookingId.tsx
index 4a00d668..91647f7b 100644
--- a/app/routes/_layout+/bookings.$bookingId.tsx
+++ b/app/routes/_layout+/bookings.$bookingId.tsx
@@ -15,10 +15,10 @@ import { NewBookingFormSchema } from "~/components/booking/form";
import ContextualModal from "~/components/layout/contextual-modal";
import Header from "~/components/layout/header";
import type { HeaderData } from "~/components/layout/header/types";
-import { Badge } from "~/components/shared";
+import { Badge } from "~/components/shared/badge";
-import { db } from "~/database";
-import { createNotes } from "~/modules/asset";
+import { db } from "~/database/db.server";
+import { createNotes } from "~/modules/asset/service.server";
import {
deleteBooking,
getBooking,
From 69c25c1a85c59f9a7b3e3e53834625b79af06bb3 Mon Sep 17 00:00:00 2001
From: Donkoko
Date: Fri, 5 Apr 2024 12:32:06 +0300
Subject: [PATCH 10/10] disable add-to-calendar button when status doesnt allow
it
---
app/components/booking/form.tsx | 55 +++++++++++++++++----------------
1 file changed, 28 insertions(+), 27 deletions(-)
diff --git a/app/components/booking/form.tsx b/app/components/booking/form.tsx
index 52d5bdf0..fa3be95b 100644
--- a/app/components/booking/form.tsx
+++ b/app/components/booking/form.tsx
@@ -307,7 +307,9 @@ export function BookingForm({
assets during the duration of the booking period.
-
+ {!routeIsNewBooking && (
+
+ )}
@@ -321,29 +323,28 @@ export function BookingForm({
);
}
-const AddToCalendar = () => {
- const navigation = useNavigation();
- const disabled = isFormProcessing(navigation.state);
-
- return (
-
-
-
-
-
-
- Download this booking as a calendar event
-
-
-
- );
-};
+const AddToCalendar = ({ disabled }: { disabled: boolean }) => (
+
+
+
+
+
+
+
+ {disabled
+ ? "Not possible to add to calendar due to booking status"
+ : "Download this booking as a calendar event"}
+
+
+
+
+);