Skip to content

Commit

Permalink
Merge pull request #1564 from Shelf-nu/feat-bookings-filters
Browse files Browse the repository at this point in the history
feat: custodian filtering on bookings index
  • Loading branch information
DonKoko authored Jan 9, 2025
2 parents 09b922e + cbb5023 commit df39bda
Show file tree
Hide file tree
Showing 13 changed files with 121 additions and 57 deletions.
2 changes: 1 addition & 1 deletion app/components/booking/booking-assets-column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ChevronDownIcon } from "@radix-ui/react-icons";
import { useLoaderData } from "@remix-run/react";
import { useBookingStatusHelpers } from "~/hooks/use-booking-status";
import { useUserRoleHelper } from "~/hooks/user-user-role-helper";
import type { BookingWithCustodians } from "~/routes/_layout+/bookings";
import type { BookingWithCustodians } from "~/modules/booking/types";
import type { AssetWithBooking } from "~/routes/_layout+/bookings.$bookingId.add-assets";
import { tw } from "~/utils/tw";
import { groupBy } from "~/utils/utils";
Expand Down
2 changes: 1 addition & 1 deletion app/components/booking/kit-row-actions-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Kit } from "@prisma/client";
import { Form, useLoaderData } from "@remix-run/react";
import { useBookingStatusHelpers } from "~/hooks/use-booking-status";
import type { BookingWithCustodians } from "~/routes/_layout+/bookings";
import type { BookingWithCustodians } from "~/modules/booking/types";
import { tw } from "~/utils/tw";
import { TrashIcon, VerticalDotsIcon } from "../icons/library";
import { Button } from "../shared/button";
Expand Down
2 changes: 1 addition & 1 deletion app/components/booking/remove-asset-from-booking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
AlertDialogTrigger,
} from "~/components/shared/modal";
import { useBookingStatusHelpers } from "~/hooks/use-booking-status";
import type { BookingWithCustodians } from "~/routes/_layout+/bookings";
import type { BookingWithCustodians } from "~/modules/booking/types";
import { isFormProcessing } from "~/utils/form";
import { tw } from "~/utils/tw";
import { Form } from "../custom-form";
Expand Down
2 changes: 1 addition & 1 deletion app/components/dynamic-dropdown/dynamic-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export default function DynamicDropdown({
<Button
icon="x"
variant="tertiary"
disabled={Boolean(searchQuery)}
disabled={!searchQuery || searchQuery === ""}
onClick={() => {
resetModelFiltersFetcher();
setSearchQuery("");
Expand Down
27 changes: 19 additions & 8 deletions app/modules/booking/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,13 +529,13 @@ export async function getBookings(params: {
statuses?: Booking["status"][] | null;
assetIds?: Asset["id"][] | null;
custodianUserId?: Booking["custodianUserId"] | null;
custodianTeamMemberId?: Booking["custodianTeamMemberId"] | null;
/** Accepts an array of team member IDs instead of a single ID so it can be used for filtering of bookings on index */
custodianTeamMemberIds?: string[] | null;
excludeBookingIds?: Booking["id"][] | null;
bookingFrom?: Booking["from"] | null;
bookingTo?: Booking["to"] | null;
userId: Booking["creatorId"];
extraInclude?: Prisma.BookingInclude;

/** Controls whether entries should be paginated or not */
takeAll?: boolean;
}) {
Expand All @@ -546,7 +546,7 @@ export async function getBookings(params: {
search,
statuses,
custodianUserId,
custodianTeamMemberId,
custodianTeamMemberIds,
assetIds,
bookingTo,
excludeBookingIds,
Expand Down Expand Up @@ -596,20 +596,30 @@ export async function getBookings(params: {
};
}

/** In the case both are passed, we do an OR */
if (custodianTeamMemberId && custodianUserId) {
/** Handle combination of custodianTeamMemberIds and custodianUserId */
if (
custodianTeamMemberIds &&
custodianTeamMemberIds?.length &&
custodianUserId
) {
where.OR = [
{
custodianTeamMemberId,
custodianTeamMemberId: {
in: custodianTeamMemberIds,
},
},
{
custodianUserId,
},
];
} else {
if (custodianTeamMemberId) {
where.custodianTeamMemberId = custodianTeamMemberId;
/** Handle custodianTeamMemberIds if present */
if (custodianTeamMemberIds?.length) {
where.custodianTeamMemberId = {
in: custodianTeamMemberIds,
};
}
/** Handle custodianUserId if present */
if (custodianUserId) {
where.custodianUserId = custodianUserId;
}
Expand Down Expand Up @@ -638,6 +648,7 @@ export async function getBookings(params: {
if (excludeBookingIds?.length) {
where.id = { notIn: excludeBookingIds };
}

if (bookingFrom && bookingTo) {
where.OR = [
{
Expand Down
10 changes: 10 additions & 0 deletions app/modules/booking/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@ export type BookingUpdateIntent =
| "checkIn"
| "archive"
| "cancel";

export type BookingWithCustodians = Prisma.BookingGetPayload<{
include: {
assets: true;
from: true;
to: true;
custodianUser: true;
custodianTeamMember: true;
};
}>;
5 changes: 5 additions & 0 deletions app/modules/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ export interface SearchableIndexResponse {
text: string;
};
}

export type RouteHandleWithName = {
name?: string;
[key: string]: any;
};
4 changes: 2 additions & 2 deletions app/routes/_layout+/bookings.$bookingId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ import {
import { createNotes } from "~/modules/note/service.server";
import { setSelectedOrganizationIdCookie } from "~/modules/organization/context.server";
import { getTeamMemberForCustodianFilter } from "~/modules/team-member/service.server";
import type { RouteHandleWithName } from "~/modules/types";
import { getUserByID } from "~/modules/user/service.server";
import { appendToMetaTitle } from "~/utils/append-to-meta-title";
import { bookingStatusColorMap } from "~/utils/bookings";
import { checkExhaustiveSwitch } from "~/utils/check-exhaustive-switch";
import { getClientHint, getHints } from "~/utils/client-hints";
import {
Expand All @@ -54,8 +56,6 @@ import {
PermissionEntity,
} from "~/utils/permissions/permission.data";
import { requirePermission } from "~/utils/roles.server";
import type { RouteHandleWithName } from "./bookings";
import { bookingStatusColorMap } from "./bookings";

export async function loader({ context, request, params }: LoaderFunctionArgs) {
const authSession = context.getSession();
Expand Down
108 changes: 68 additions & 40 deletions app/routes/_layout+/bookings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import type { MetaFunction, LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import type { ShouldRevalidateFunction } from "@remix-run/react";
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 { StatusFilter } from "~/components/booking/status-filter";
import DynamicDropdown from "~/components/dynamic-dropdown/dynamic-dropdown";
import { ErrorContent } from "~/components/errors";

import ContextualModal from "~/components/layout/contextual-modal";
Expand All @@ -19,11 +21,16 @@ import { Filters } from "~/components/list/filters";
import { Badge } from "~/components/shared/badge";
import { Button } from "~/components/shared/button";
import { Td, Th } from "~/components/table";
import When from "~/components/when/when";
import { db } from "~/database/db.server";
import { hasGetAllValue } from "~/hooks/use-model-filters";
import { useUserRoleHelper } from "~/hooks/user-user-role-helper";
import { getBookings } from "~/modules/booking/service.server";
import { setSelectedOrganizationIdCookie } from "~/modules/organization/context.server";
import { getTeamMemberForCustodianFilter } from "~/modules/team-member/service.server";
import type { RouteHandleWithName } from "~/modules/types";
import { appendToMetaTitle } from "~/utils/append-to-meta-title";
import { bookingStatusColorMap } from "~/utils/bookings";
import { getDateTimeFormat } from "~/utils/client-hints";
import {
setCookie,
Expand All @@ -38,7 +45,9 @@ import {
PermissionAction,
PermissionEntity,
} from "~/utils/permissions/permission.data";
import { userHasPermission } from "~/utils/permissions/permission.validator.client";
import { requirePermission } from "~/utils/roles.server";
import { resolveTeamMemberName } from "~/utils/user";

export async function loader({ context, request }: LoaderFunctionArgs) {
const authSession = context.getSession();
Expand All @@ -65,14 +74,14 @@ export async function loader({ context, request }: LoaderFunctionArgs) {
}

const searchParams = getCurrentSearchParams(request);
const { page, perPageParam, search, status } =
const { page, perPageParam, search, status, teamMemberIds } =
getParamsValues(searchParams);
const cookie = await updateCookieWithPerPage(request, perPageParam);
const { perPage } = cookie;

/**
* For self service and base users, we need to get the teamMember to be able to filter by it as well.
* Tis is to handle a case when a booking was assigned when there wasnt a user attached to a team member but they were later on linked.
* This is to handle a case when a booking was assigned when there wasn't a user attached to a team member but they were later on linked.
* This is to ensure that the booking is still visible to the user that was assigned to it.
* Also this shouldn't really happen as we now have a fix implemented when accepting invites,
* to make sure it doesnt happen, hwoever its good to keep this as an extra safety thing.
Expand Down Expand Up @@ -104,18 +113,32 @@ export async function loader({ context, request }: LoaderFunctionArgs) {
};
}

const { bookings, bookingCount } = await getBookings({
organizationId,
page,
perPage,
search,
userId: authSession?.userId,
...(status && {
// If status is in the params, we filter based on it
statuses: [status],
const [{ bookings, bookingCount }, teamMembersData] = await Promise.all([
getBookings({
organizationId,
page,
perPage,
search,
userId: authSession?.userId,
...(status && {
// If status is in the params, we filter based on it
statuses: [status],
}),
custodianTeamMemberIds: teamMemberIds,
...selfServiceData,
}),
...selfServiceData,
});

// team members/custodian
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,
}),
]);

const totalPages = Math.ceil(bookingCount / perPage);

Expand Down Expand Up @@ -161,6 +184,7 @@ export async function loader({ context, request }: LoaderFunctionArgs) {
totalPages,
perPage,
modelName,
...teamMembersData,
}),
{
headers: [
Expand Down Expand Up @@ -196,11 +220,6 @@ export const shouldRevalidate: ShouldRevalidateFunction = ({
return defaultShouldRevalidate;
};

export type RouteHandleWithName = {
name?: string;
[key: string]: any;
};

export default function BookingsIndexPage({
className,
disableBulkActions = false,
Expand All @@ -211,7 +230,7 @@ export default function BookingsIndexPage({
const navigate = useNavigate();
const matches = useMatches();

const { isBaseOrSelfService } = useUserRoleHelper();
const { isBaseOrSelfService, roles } = useUserRoleHelper();

const currentRoute: RouteHandleWithName = matches[matches.length - 1];

Expand Down Expand Up @@ -270,7 +289,36 @@ export default function BookingsIndexPage({
slots={{
"left-of-search": <StatusFilter statusItems={BookingStatus} />,
}}
/>
>
<When
truthy={
userHasPermission({
roles,
entity: PermissionEntity.custody,
action: PermissionAction.read,
}) && !isBaseOrSelfService
}
>
<DynamicDropdown
trigger={
<div className="flex cursor-pointer items-center gap-2">
Custodian{" "}
<ChevronRight className="hidden rotate-90 md:inline" />
</div>
}
model={{
name: "teamMember",
queryKey: "name",
deletedAt: null,
}}
renderItem={(item) => resolveTeamMemberName(item, true)}
label="Filter by custodian"
placeholder="Search team members"
initialDataKey="teamMembers"
countKey="totalTeamMembers"
/>
</When>
</Filters>
<List
bulkActions={
disableBulkActions || isBaseOrSelfService ? undefined : (
Expand Down Expand Up @@ -299,16 +347,6 @@ export default function BookingsIndexPage({
);
}

export const bookingStatusColorMap: { [key in BookingStatus]: string } = {
DRAFT: "#667085",
RESERVED: "#175CD3",
ONGOING: "#7A5AF8",
OVERDUE: "#B54708",
COMPLETE: "#17B26A",
ARCHIVED: "#667085",
CANCELLED: "#667085",
};

const ListAssetContent = ({
item,
}: {
Expand Down Expand Up @@ -456,14 +494,4 @@ function UserBadge({ img, name }: { img?: string; name: string }) {
);
}

export type BookingWithCustodians = Prisma.BookingGetPayload<{
include: {
assets: true;
from: true;
to: true;
custodianUser: true;
custodianTeamMember: true;
};
}>;

export const ErrorBoundary = () => <ErrorContent />;
2 changes: 1 addition & 1 deletion app/routes/_layout+/calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import When from "~/components/when/when";
import { useViewportHeight } from "~/hooks/use-viewport-height";
import calendarStyles from "~/styles/layout/calendar.css?url";
import { appendToMetaTitle } from "~/utils/append-to-meta-title";
import { bookingStatusColorMap } from "~/utils/bookings";
import {
getStatusClasses,
isOneDayEvent,
Expand All @@ -48,7 +49,6 @@ import {
} from "~/utils/permissions/permission.data";
import { requirePermission } from "~/utils/roles.server";
import { tw } from "~/utils/tw";
import { bookingStatusColorMap } from "./bookings";

export function links() {
return [{ rel: "stylesheet", href: calendarStyles }];
Expand Down
2 changes: 1 addition & 1 deletion app/routes/_layout+/settings.team.invites.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { db } from "~/database/db.server";

import { getPaginatedAndFilterableSettingInvites } from "~/modules/invite/service.server";
import type { TeamMembersWithUserOrInvite } from "~/modules/settings/service.server";
import type { RouteHandleWithName } from "~/modules/types";
import { resolveUserAction } from "~/modules/user/utils.server";
import { appendToMetaTitle } from "~/utils/append-to-meta-title";
import { makeShelfError, ShelfError } from "~/utils/error";
Expand All @@ -29,7 +30,6 @@ import {
} from "~/utils/permissions/permission.data";
import { requirePermission } from "~/utils/roles.server";
import { tw } from "~/utils/tw";
import type { RouteHandleWithName } from "./bookings";

export async function loader({ request, context }: LoaderFunctionArgs) {
const authSession = context.getSession();
Expand Down
2 changes: 1 addition & 1 deletion app/routes/_layout+/settings.team.users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { db } from "~/database/db.server";

import type { TeamMembersWithUserOrInvite } from "~/modules/settings/service.server";
import { getPaginatedAndFilterableSettingUsers } from "~/modules/settings/service.server";
import type { RouteHandleWithName } from "~/modules/types";
import { resolveUserAction } from "~/modules/user/utils.server";
import { appendToMetaTitle } from "~/utils/append-to-meta-title";
import { makeShelfError, ShelfError } from "~/utils/error";
Expand All @@ -29,7 +30,6 @@ import {
} from "~/utils/permissions/permission.data";
import { requirePermission } from "~/utils/roles.server";
import { tw } from "~/utils/tw";
import type { RouteHandleWithName } from "./bookings";

export async function loader({ request, context }: LoaderFunctionArgs) {
const authSession = context.getSession();
Expand Down
Loading

0 comments on commit df39bda

Please sign in to comment.