Skip to content

Commit

Permalink
Merge pull request #1189 from Shelf-nu/1165-feature-request-implement…
Browse files Browse the repository at this point in the history
…-self-service-plus-user-type-with-enhanced-booking-capabilities

feat: implement new OrganizationRole BASE
  • Loading branch information
DonKoko authored Jul 25, 2024
2 parents 8441842 + d58397a commit 40b518d
Show file tree
Hide file tree
Showing 112 changed files with 851 additions and 559 deletions.
6 changes: 3 additions & 3 deletions app/components/assets/asset-custody-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Card } from "../shared/card";
*/
export function CustodyCard({
booking,
isSelfService,
hasPermission,
custody,
}: {
booking:
Expand All @@ -25,7 +25,7 @@ export function CustodyCard({
}
| null
| undefined;
isSelfService: boolean;
hasPermission: boolean;
custody: {
dateDisplay: string;
custodian: {
Expand All @@ -37,7 +37,7 @@ export function CustodyCard({
} | null;
}) {
/** We return null if user is selfService */
if (isSelfService) {
if (!hasPermission) {
return null;
}

Expand Down
35 changes: 15 additions & 20 deletions app/components/assets/notes/note.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { Note as NoteType } from "@prisma/client";
import { MarkdownViewer } from "~/components/markdown/markdown-viewer";
import { Switch } from "~/components/shared/switch";
import { Tag } from "~/components/shared/tag";
import { useUserIsSelfService } from "~/hooks/user-user-is-self-service";
import type { WithDateFields } from "~/modules/types";
import { timeAgo } from "~/utils/time-ago";
import { ActionsDopdown } from "./actions-dropdown";
Expand Down Expand Up @@ -32,23 +31,19 @@ const Update = ({ note }: { note: NoteWithDate; when?: boolean }) => (
</div>
);

export const Comment = ({ note }: { note: NoteWithDate; when?: boolean }) => {
const isSelfService = useUserIsSelfService();

return (
<>
<header className="flex justify-between border-b px-3.5 py-3 text-text-xs md:text-text-sm">
<div>
<span className="commentator font-medium text-gray-900">
{note.user?.firstName} {note.user?.lastName}
</span>{" "}
<span className="text-gray-600">{timeAgo(note.createdAt)}</span>
</div>
{!isSelfService ? <ActionsDopdown noteId={note.id} /> : null}
</header>
<div className="message px-3.5 py-3">
<MarkdownViewer content={note.content} />
export const Comment = ({ note }: { note: NoteWithDate; when?: boolean }) => (
<>
<header className="flex justify-between border-b px-3.5 py-3 text-text-xs md:text-text-sm">
<div>
<span className="commentator font-medium text-gray-900">
{note.user?.firstName} {note.user?.lastName}
</span>{" "}
<span className="text-gray-600">{timeAgo(note.createdAt)}</span>
</div>
</>
);
};
<ActionsDopdown noteId={note.id} />
</header>
<div className="message px-3.5 py-3">
<MarkdownViewer content={note.content} />
</div>
</>
);
44 changes: 35 additions & 9 deletions app/components/booking/actions-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useLoaderData, useSubmit } from "@remix-run/react";
import { Divider } from "@tremor/react";
import { ChevronRight } from "~/components/icons/library";
import {
DropdownMenu,
Expand All @@ -9,12 +8,19 @@ import {
DropdownMenuTrigger,
} from "~/components/shared/dropdown";
import { useBookingStatusHelpers } from "~/hooks/use-booking-status";
import { useUserIsSelfService } from "~/hooks/user-user-is-self-service";
import { useUserRoleHelper } from "~/hooks/user-user-role-helper";
import type { loader } from "~/routes/_layout+/bookings.$bookingId";
import {
PermissionAction,
PermissionEntity,
} from "~/utils/permissions/permission.data";
import { userHasPermission } from "~/utils/permissions/permission.validator.client";
import { tw } from "~/utils/tw";
import { DeleteBooking } from "./delete-booking";
import { GenerateBookingPdf } from "./generate-booking-pdf";
import { Divider } from "../layout/divider";
import { Button } from "../shared/button";
import When from "../when/when";

interface Props {
fullWidth?: boolean;
Expand All @@ -26,7 +32,19 @@ export const ActionsDropdown = ({ fullWidth }: Props) => {
useBookingStatusHelpers(booking);

const submit = useSubmit();
const isSelfService = useUserIsSelfService();
const { isBaseOrSelfService, roles } = useUserRoleHelper();

const canArchiveBooking = userHasPermission({
roles,
entity: PermissionEntity.booking,
action: PermissionAction.archive,
});

const canCancelBooking = userHasPermission({
roles,
entity: PermissionEntity.booking,
action: PermissionAction.cancel,
});

return (
<DropdownMenu modal={false}>
Expand All @@ -49,7 +67,9 @@ export const ActionsDropdown = ({ fullWidth }: Props) => {
align="end"
className="order w-[220px] rounded-md bg-white p-1.5 text-right "
>
{isOngoing || isReserved || isOverdue ? (
<When
truthy={(isOngoing || isReserved || isOverdue) && canCancelBooking}
>
<DropdownMenuItem asChild>
<Button
variant="link"
Expand All @@ -75,8 +95,8 @@ export const ActionsDropdown = ({ fullWidth }: Props) => {
Cancel
</Button>
</DropdownMenuItem>
) : null}
{isCompleted && !isSelfService ? (
</When>
<When truthy={isCompleted && canArchiveBooking}>
<DropdownMenuItem asChild>
<Button
variant="link"
Expand All @@ -102,10 +122,16 @@ export const ActionsDropdown = ({ fullWidth }: Props) => {
Archive
</Button>
</DropdownMenuItem>
) : null}
{(isSelfService && isDraft) || !isSelfService ? (
</When>

{/* Because SELF_SERVICE and BASE can only delete bookings they own and are in draft, we need to handle it like this, rather than with userHasPermission */}

<When
truthy={(isBaseOrSelfService && isDraft) || !isBaseOrSelfService}
>
<DeleteBooking booking={booking} />
) : null}
</When>

<Divider className="my-2" />
<GenerateBookingPdf
booking={booking}
Expand Down
69 changes: 40 additions & 29 deletions app/components/booking/booking-assets-column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Kit } from "@prisma/client";
import { AssetStatus, BookingStatus } from "@prisma/client";
import { useLoaderData } from "@remix-run/react";
import { useBookingStatusHelpers } from "~/hooks/use-booking-status";
import { useUserIsSelfService } from "~/hooks/user-user-is-self-service";
import { useUserRoleHelper } from "~/hooks/user-user-role-helper";
import type { BookingWithCustodians } from "~/routes/_layout+/bookings";
import type { AssetWithBooking } from "~/routes/_layout+/bookings.$bookingId.add-assets";
import { groupBy } from "~/utils/utils";
Expand All @@ -28,7 +28,7 @@ export function BookingAssetsColumn() {
totalItems: number;
}>();
const hasItems = items?.length > 0;
const isSelfService = useUserIsSelfService();
const { isBase } = useUserRoleHelper();
const { isDraft, isReserved, isCompleted, isArchived, isCancelled } =
useBookingStatusHelpers(booking);

Expand All @@ -46,8 +46,8 @@ export function BookingAssetsColumn() {
);

// Self service can only manage assets for bookings that are DRAFT
const cantManageAssetsAsSelfService =
isSelfService && booking.status !== BookingStatus.DRAFT;
const cantManageAssetsAsBase =
isBase && booking.status !== BookingStatus.DRAFT;

const { assetsWithoutKits, groupedAssetsWithKits } = useMemo(
() => ({
Expand All @@ -60,6 +60,36 @@ export function BookingAssetsColumn() {
[items]
);

const manageAssetsButtonDisabled = useMemo(
() =>
!booking.from ||
!booking.to ||
isCompleted ||
isArchived ||
isCancelled ||
cantManageAssetsAsBase
? {
reason: isCompleted
? "Booking is completed. You cannot change the assets anymore"
: isArchived
? "Booking is archived. You cannot change the assets anymore"
: isCancelled
? "Booking is cancelled. You cannot change the assets anymore"
: cantManageAssetsAsBase
? "You are unable to manage assets at this point because the booking is already reserved. Cancel this booking and create another one if you need to make changes."
: "You need to select a start and end date and save your booking before you can add assets to your booking",
}
: false,
[
booking.from,
booking.to,
isCompleted,
isArchived,
isCancelled,
cantManageAssetsAsBase,
]
);

return (
<div className="flex-1">
<div className=" w-full">
Expand All @@ -75,26 +105,7 @@ export function BookingAssetsColumn() {
<Button
to={manageAssetsUrl}
className="whitespace-nowrap"
disabled={
!booking.from ||
!booking.to ||
isCompleted ||
isArchived ||
isCancelled ||
cantManageAssetsAsSelfService
? {
reason: isCompleted
? "Booking is completed. You cannot change the assets anymore"
: isArchived
? "Booking is archived. You cannot change the assets anymore"
: isCancelled
? "Booking is cancelled. You cannot change the assets anymore"
: cantManageAssetsAsSelfService
? "You are unable to manage assets at this point because the booking is already reserved. Cancel this booking and create another one if you need to make changes."
: "You need to select a start and end date and save your booking before you can add assets to your booking",
}
: false
}
disabled={manageAssetsButtonDisabled}
>
Manage assets
</Button>
Expand All @@ -110,7 +121,7 @@ export function BookingAssetsColumn() {
newButtonRoute: manageAssetsUrl,
newButtonContent: "Manage assets",
buttonProps: {
disabled: !booking.from || !booking.to,
disabled: manageAssetsButtonDisabled,
},
}}
/>
Expand Down Expand Up @@ -156,7 +167,7 @@ export function BookingAssetsColumn() {
<Td> </Td>

<Td className="pr-4 text-right">
{(!isSelfService && isDraft) || isReserved ? (
{(!isBase && isDraft) || isReserved ? (
<KitRowActionsDropdown kit={kit} />
) : null}
</Td>
Expand Down Expand Up @@ -187,7 +198,7 @@ export function BookingAssetsColumn() {
const ListAssetContent = ({ item }: { item: AssetWithBooking }) => {
const { category } = item;
const { booking } = useLoaderData<{ booking: BookingWithCustodians }>();
const isSelfService = useUserIsSelfService();
const { isBase } = useUserRoleHelper();
const { isOngoing, isCompleted, isArchived, isOverdue, isReserved } =
useBookingStatusHelpers(booking);

Expand Down Expand Up @@ -254,8 +265,8 @@ const ListAssetContent = ({ item }: { item: AssetWithBooking }) => {
) : null}
</Td>
<Td className="pr-4 text-right">
{/* Self Service can only remove assets if the booking is not started already */}
{(isSelfService && (isOngoing || isOverdue || isReserved)) ||
{/* Base users can only remove assets if the booking is not started already */}
{(isBase && (isOngoing || isOverdue || isReserved)) ||
isPartOfKit ? null : (
<AssetRowActionsDropdown asset={item} />
)}
Expand Down
21 changes: 16 additions & 5 deletions app/components/booking/bulk-actions-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import { useNavigation } from "@remix-run/react";
import { useAtomValue } from "jotai";
import { useHydrated } from "remix-utils/use-hydrated";
import { selectedBulkItemsAtom } from "~/atoms/list";
import { useUserIsSelfService } from "~/hooks/user-user-is-self-service";
import { useUserRoleHelper } from "~/hooks/user-user-role-helper";
import { isFormProcessing } from "~/utils/form";
import {
PermissionAction,
PermissionEntity,
} from "~/utils/permissions/permission.data";
import { userHasPermission } from "~/utils/permissions/permission.validator.client";
import { tw } from "~/utils/tw";
import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu";
import BulkArchiveDialog from "./bulk-archive-dialog";
Expand Down Expand Up @@ -60,17 +65,23 @@ function ConditionalDropdown() {
].includes(b.status as any)
);

const isSelfService = useUserIsSelfService();
const { isBase, roles } = useUserRoleHelper();

const navigation = useNavigation();
const isLoading = isFormProcessing(navigation.state);

const disabled = selectedBookings.length === 0;

const archiveDisabled = !allBookingAreCompleted || isSelfService;
const canArchiveBooking = userHasPermission({
roles,
entity: PermissionEntity.booking,
action: PermissionAction.archive,
});

const archiveDisabled = !allBookingAreCompleted || !canArchiveBooking;

const deleteDisabled =
(isSelfService && !someBookingInDraft) || isSelfService || isLoading;
/** Base users dont have permissions to delete bookings unless they are draft */
const deleteDisabled = (isBase && !someBookingInDraft) || isBase || isLoading;

const {
ref: dropdownRef,
Expand Down
Loading

0 comments on commit 40b518d

Please sign in to comment.