diff --git a/app/modules/asset/service.server.ts b/app/modules/asset/service.server.ts index ba5980955..6a63872a2 100644 --- a/app/modules/asset/service.server.ts +++ b/app/modules/asset/service.server.ts @@ -838,6 +838,7 @@ export async function updateAssetMainImage({ }); } +/** Creates a singular note */ export async function createNote({ content, type, @@ -868,6 +869,29 @@ export async function createNote({ }); } +/** Creates multiple notes with the same content */ +export async function createNotes({ + content, + type, + userId, + assetIds, +}: Pick & { + type?: Note["type"]; + userId: User["id"]; + assetIds: Asset["id"][]; +}) { + const data = assetIds.map((id) => ({ + content, + type: type || "COMMENT", + userId, + assetId: id, + })); + + return db.note.createMany({ + data, + }); +} + export async function deleteNote({ id, userId, diff --git a/app/modules/booking/email-helpers.ts b/app/modules/booking/email-helpers.ts index 75dc4f44d..c26d69cfb 100644 --- a/app/modules/booking/email-helpers.ts +++ b/app/modules/booking/email-helpers.ts @@ -179,7 +179,7 @@ export const sendCheckinReminder = async ( heading: `Your booking is due for checkin in ${getTimeRemainingMessage( new Date(booking.to!), new Date() - )} minutes.`, + )}.`, assetCount, hints, }), diff --git a/app/modules/booking/service.server.ts b/app/modules/booking/service.server.ts index 22de08ff5..c88ce0cea 100644 --- a/app/modules/booking/service.server.ts +++ b/app/modules/booking/service.server.ts @@ -21,6 +21,7 @@ import { sendCheckinReminder, } from "./email-helpers"; import type { ClientHint, SchedulerData } from "./types"; +import { createNotes } from "../asset"; import { getOrganizationAdminsEmails } from "../organization"; /** Includes needed for booking to have all data required for emails */ @@ -271,7 +272,7 @@ export const upsertBooking = async ( /** We need to invoke this function separately for the admin email as the footer of emails is different */ html: bookingUpdatesTemplateString({ booking: res, - heading: `Booking reservation for ${custodian}`, + heading: `Booking reservation request for ${custodian}`, assetCount: res.assets.length, hints, isAdminEmail: true, @@ -515,9 +516,19 @@ export async function getBookings({ return { bookings, bookingCount }; } -export const removeAssets = async ( - booking: Pick & { assetIds: Asset["id"][] } -) => { +export const removeAssets = async ({ + booking, + firstName, + lastName, + userId, +}: { + booking: Pick & { + assetIds: Asset["id"][]; + }; + firstName: string; + lastName: string; + userId: string; +}) => { const { assetIds, id } = booking; const b = await db.booking.update({ // First, disconnect the assets from the booking @@ -549,6 +560,15 @@ export const removeAssets = async ( }); } + createNotes({ + content: `**${firstName?.trim()} ${lastName?.trim()}** removed asset from booking **[${ + b.name + }](/bookings/${b.id})**.`, + type: "UPDATE", + userId, + assetIds, + }); + return b; }; @@ -575,6 +595,11 @@ export const deleteBooking = async ( include: { ...commonInclude, ...bookingIncludeForEmails, + assets: { + select: { + id: true, + }, + }, }, }); diff --git a/app/modules/user/service.server.ts b/app/modules/user/service.server.ts index 8f677f9da..d0247fbc2 100644 --- a/app/modules/user/service.server.ts +++ b/app/modules/user/service.server.ts @@ -36,7 +36,14 @@ export async function getUserByEmail(email: User["email"]) { } export async function getUserByID(id: User["id"]) { - return db.user.findUnique({ where: { id } }); + try { + return db.user.findUnique({ where: { id } }); + } catch (cause) { + throw new ShelfStackError({ + message: "Failed to get user", + cause, + }); + } } export async function getUserByIDWithOrg(id: User["id"]) { diff --git a/app/routes/_layout+/bookings.$bookingId.add-assets.tsx b/app/routes/_layout+/bookings.$bookingId.add-assets.tsx index 623cd586d..229a29f19 100644 --- a/app/routes/_layout+/bookings.$bookingId.add-assets.tsx +++ b/app/routes/_layout+/bookings.$bookingId.add-assets.tsx @@ -26,8 +26,9 @@ import { AddAssetForm } from "~/components/location/add-asset-form"; import { Button } from "~/components/shared"; import { Td } from "~/components/table"; -import { getPaginatedAndFilterableAssets } from "~/modules/asset"; +import { createNote, getPaginatedAndFilterableAssets } from "~/modules/asset"; import { getBooking, removeAssets, upsertBooking } from "~/modules/booking"; +import { getUserByID } from "~/modules/user"; import { getRequiredParam, isFormProcessing } from "~/utils"; import { getClientHint } from "~/utils/client-hints"; import { ShelfStackError } from "~/utils/error"; @@ -92,7 +93,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }; export const action = async ({ request, params }: ActionFunctionArgs) => { - await requirePermision( + const { authSession } = await requirePermision( request, PermissionEntity.booking, PermissionAction.update @@ -102,18 +103,39 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const formData = await request.formData(); const assetId = formData.get("assetId") as string; const isChecked = formData.get("isChecked") === "yes"; + const user = await getUserByID(authSession.userId); + if (!user) { + throw new ShelfStackError({ message: "User not found" }); + } + if (isChecked) { - await upsertBooking( + const b = await upsertBooking( { id: bookingId, assetIds: [assetId], }, getClientHint(request) ); + /** We check the ids again after updating, and if they were sent, that means assets are being added + * So we create notes for the assets that were added + */ + await createNote({ + content: `**${user?.firstName?.trim()} ${user?.lastName?.trim()}** added asset to booking **[${ + b.name + }](/bookings/${b.id})**.`, + type: "UPDATE", + userId: authSession.userId, + assetId, + }); } else { await removeAssets({ - id: bookingId, - assetIds: [assetId], + booking: { + id: bookingId, + assetIds: [assetId], + }, + firstName: user.firstName ? user.firstName : "", + lastName: user.lastName ? user.lastName : "", + userId: authSession.userId, }); } diff --git a/app/routes/_layout+/bookings.$bookingId.tsx b/app/routes/_layout+/bookings.$bookingId.tsx index 12fec54ea..f080d964b 100644 --- a/app/routes/_layout+/bookings.$bookingId.tsx +++ b/app/routes/_layout+/bookings.$bookingId.tsx @@ -18,6 +18,7 @@ import Header from "~/components/layout/header"; import type { HeaderData } from "~/components/layout/header/types"; import { Badge } from "~/components/shared"; import { db } from "~/database"; +import { createNotes } from "~/modules/asset"; import { commitAuthSession } from "~/modules/auth"; import { deleteBooking, @@ -215,6 +216,7 @@ export async function action({ request, params }: ActionFunctionArgs) { ); const id = getRequiredParam(params, "bookingId"); const isSelfService = role === OrganizationRoles.SELF_SERVICE; + const user = await getUserByID(authSession.userId); switch (intent) { case "save": @@ -316,7 +318,20 @@ export async function action({ request, params }: ActionFunctionArgs) { } } - await deleteBooking({ id }, getClientHint(request)); + const deletedBooking = await deleteBooking( + { id }, + getClientHint(request) + ); + + createNotes({ + content: `**${user?.firstName?.trim()} ${user?.lastName?.trim()}** deleted booking **${ + deletedBooking.name + }**.`, + type: "UPDATE", + userId: authSession.userId, + assetIds: deletedBooking.assets.map((a) => a.id), + }); + sendNotification({ title: "Booking deleted", message: "Your booking has been deleted successfully", @@ -331,8 +346,12 @@ export async function action({ request, params }: ActionFunctionArgs) { }); case "removeAsset": const assetId = formData.get("assetId"); - // @ts-ignore @TODO we need to fix this. Not sure how - var booking = await removeAssets({ id, assetIds: [assetId as string] }); + var b = await removeAssets({ + booking: { id, assetIds: [assetId as string] }, + firstName: user?.firstName || "", + lastName: user?.lastName || "", + userId: authSession.userId, + }); sendNotification({ title: "Asset removed", message: "Your asset has been removed from the booking", @@ -340,7 +359,7 @@ export async function action({ request, params }: ActionFunctionArgs) { senderId: authSession.userId, }); return json( - { booking }, + { booking: b }, { status: 200, headers: [ @@ -354,6 +373,16 @@ export async function action({ request, params }: ActionFunctionArgs) { { id, status: BookingStatus.ONGOING }, getClientHint(request) ); + + createNotes({ + content: `**${user?.firstName?.trim()} ${user?.lastName?.trim()}** checked out asset with **[${ + booking.name + }](/bookings/${booking.id})**.`, + type: "UPDATE", + userId: authSession.userId, + assetIds: booking.assets.map((a) => a.id), + }); + sendNotification({ title: "Booking checked-out", message: "Your booking has been checked-out successfully", @@ -377,6 +406,15 @@ export async function action({ request, params }: ActionFunctionArgs) { }, getClientHint(request) ); + /** Create check-in notes for all assets */ + createNotes({ + content: `**${user?.firstName?.trim()} ${user?.lastName?.trim()}** checked in asset with **[${ + booking.name + }](/bookings/${booking.id})**.`, + type: "UPDATE", + userId: authSession.userId, + assetIds: booking.assets.map((a) => a.id), + }); sendNotification({ title: "Booking checked-in", message: "Your booking has been checked-in successfully", @@ -413,10 +451,19 @@ export async function action({ request, params }: ActionFunctionArgs) { } ); case "cancel": - await upsertBooking( + const cancelledBooking = await upsertBooking( { id, status: BookingStatus.CANCELLED }, getClientHint(request) ); + + createNotes({ + content: `**${user?.firstName?.trim()} ${user?.lastName?.trim()}** cancelled booking **[${ + cancelledBooking.name + }](/bookings/${cancelledBooking.id})**.`, + type: "UPDATE", + userId: authSession.userId, + assetIds: cancelledBooking.assets.map((a) => a.id), + }); sendNotification({ title: "Booking canceled", message: "Your booking has been canceled successfully",