diff --git a/app/components/assets/assets-index/advanced-filters/helpers.ts b/app/components/assets/assets-index/advanced-filters/helpers.ts index 3cbc92321..2413bf1b7 100644 --- a/app/components/assets/assets-index/advanced-filters/helpers.ts +++ b/app/components/assets/assets-index/advanced-filters/helpers.ts @@ -231,3 +231,35 @@ export function getAvailableColumns( return true; }); } + +/** + * Extracts the QR ID from a URL or returns the original value if it's not a URL + * Removes any query parameters and returns the last path segment + * + * @example + * localhost:3000/qr/abc123?hello=world -> abc123 + * https://example.com/abc123 -> abc123 + * abc123 -> abc123 + * + * @param value - The input value (URL or QR ID) + * @returns The extracted QR ID or original value + */ +export function extractQrIdFromValue(value: string): string { + try { + // Try to parse as URL first + const url = new URL(value); + + // Remove leading and trailing slashes and split path + const pathParts = url.pathname.split("/").filter(Boolean); + + // Get the last part of the path (if exists) + if (pathParts.length > 0) { + return pathParts[pathParts.length - 1]; + } + + return value; + } catch (e) { + // If URL parsing fails, return original value + return value; + } +} diff --git a/app/components/assets/assets-index/advanced-filters/value-field.tsx b/app/components/assets/assets-index/advanced-filters/value-field.tsx index 9aa362570..5f22c0149 100644 --- a/app/components/assets/assets-index/advanced-filters/value-field.tsx +++ b/app/components/assets/assets-index/advanced-filters/value-field.tsx @@ -13,13 +13,25 @@ import DynamicDropdown from "~/components/dynamic-dropdown/dynamic-dropdown"; import DynamicSelect from "~/components/dynamic-select/dynamic-select"; import Input from "~/components/forms/input"; -import { CheckIcon, ChevronRight, PlusIcon } from "~/components/icons/library"; +import { + CheckIcon, + ChevronRight, + HelpIcon, + PlusIcon, +} from "~/components/icons/library"; import { Button } from "~/components/shared/button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "~/components/shared/tooltip"; import type { AssetIndexLoaderData } from "~/routes/_layout+/assets._index"; import { useHints } from "~/utils/client-hints"; import { adjustDateToUTC, isDateString } from "~/utils/date-fns"; import { tw } from "~/utils/tw"; import { resolveTeamMemberName } from "~/utils/user"; +import { extractQrIdFromValue } from "./helpers"; import type { Filter } from "./schema"; import { userFriendlyAssetStatus } from "../../asset-status-badge"; @@ -156,6 +168,63 @@ export function ValueField({ switch (filter.type) { case "string": case "text": + if (filter.name === "qrId") { + return ( +
+
+ { + setFilter(e.target.value); + }} + placeholder={placeholder(filter.operator)} + onKeyUp={(e: React.KeyboardEvent) => { + if (e.key === "Enter") { + setTimeout(() => { + // Assert the target as HTMLInputElement to access value + const input = e.target as HTMLInputElement; + const cleanValue = extractQrIdFromValue(input.value); + setFilter(cleanValue); + // Create a new keyboard event for submitOnEnter + submitOnEnter(e as React.KeyboardEvent); + }, 10); + } + }} + error={error} + name={fieldName} + /> + {!["contains", "containsAny", "matchesAny"].includes( + filter.operator + ) ? ( + + + + + + + + +
+
+ Barcode scanner ready +
+

+ This fields supports barcode scanners. Simply place + your cursor in the field and scan a Shelf QR code with + your barcode scanner. The value will be automatically + filled in for you. +

+
+
+
+
+ ) : null} +
+
+ ); + } return ( - - - - + + + + + - - Edit asset information - - + + Edit asset information + + + - - - - + + + + + - - Duplicate asset - - - - - - - } - /> + + Duplicate asset + + + - - Delete asset - - + + + + + + } + /> + + + Delete asset + + + ); } diff --git a/app/modules/asset/service.server.ts b/app/modules/asset/service.server.ts index 412750854..03ba27deb 100644 --- a/app/modules/asset/service.server.ts +++ b/app/modules/asset/service.server.ts @@ -921,11 +921,13 @@ export async function updateAssetMainImage({ assetId, userId, organizationId, + isNewAsset = false, }: { request: Request; assetId: string; userId: User["id"]; organizationId: Organization["id"]; + isNewAsset?: boolean; }) { try { const fileData = await parseFileFormData({ @@ -955,7 +957,14 @@ export async function updateAssetMainImage({ userId, organizationId, }); - await deleteOtherImages({ userId, assetId, data: { path: image } }); + + /** + * If updateAssetMainImage is called from new asset route, then we don't have to delete other images + * bcause no others images for this assets exists yet. + */ + if (!isNewAsset) { + await deleteOtherImages({ userId, assetId, data: { path: image } }); + } } catch (cause) { throw new ShelfError({ cause, @@ -1093,6 +1102,7 @@ export function createCustomFieldsPayloadFromAsset( ) || {} ); } + export async function duplicateAsset({ asset, userId, @@ -2019,14 +2029,15 @@ export async function createAssetsFromBackupImport({ } } -export async function updateAssetBookingAvailability( - id: Asset["id"], - availability: Asset["availableToBook"] -) { +export async function updateAssetBookingAvailability({ + id, + availableToBook, + organizationId, +}: Pick) { try { return await db.asset.update({ - where: { id }, - data: { availableToBook: availability }, + where: { id, organizationId }, + data: { availableToBook }, }); } catch (cause) { throw maybeUniqueConstraintViolation(cause, "Asset", { @@ -2158,7 +2169,7 @@ export async function updateAssetQrCode({ // Disconnect all existing QR codes await db.asset .update({ - where: { id: assetId }, + where: { id: assetId, organizationId }, data: { qrCodes: { set: [], @@ -2177,7 +2188,7 @@ export async function updateAssetQrCode({ // Connect the new QR code return await db.asset .update({ - where: { id: assetId }, + where: { id: assetId, organizationId }, data: { qrCodes: { connect: { id: newQrId }, @@ -2623,7 +2634,7 @@ export async function bulkAssignAssetTags({ const updatePromises = _assetIds.map((id) => db.asset.update({ - where: { id }, + where: { id, organizationId }, data: { tags: { [remove ? "disconnect" : "connect"]: tagsIds.map((id) => ({ id })), // IDs of tags you want to connect @@ -2700,7 +2711,7 @@ export async function relinkQrCode({ getQr({ id: qrId }), getUserByID(userId), db.asset.findFirst({ - where: { id: assetId }, + where: { id: assetId, organizationId }, select: { qrCodes: { select: { id: true } } }, }), ]); diff --git a/app/modules/asset/utils.server.ts b/app/modules/asset/utils.server.ts index 14989bd66..f14686ffc 100644 --- a/app/modules/asset/utils.server.ts +++ b/app/modules/asset/utils.server.ts @@ -215,7 +215,7 @@ export const importAssetsSchema = z tags: z.string().array(), location: z.string().optional(), custodian: z.string().optional(), - bookable: z.enum(["yes", "no"]).optional(), + bookable: z.enum(["yes", "no"]).optional().nullable(), imageUrl: z.string().url().optional(), }) .and(z.record(z.string(), z.any())); diff --git a/app/modules/booking/service.server.ts b/app/modules/booking/service.server.ts index 22abf96bb..f061448e3 100644 --- a/app/modules/booking/service.server.ts +++ b/app/modules/booking/service.server.ts @@ -164,7 +164,10 @@ export async function upsertBooking( | "custodianTeamMemberId" | "custodianUserId" | "description" - > & { assetIds: Asset["id"][]; isExpired: boolean } + > & { + assetIds: Asset["id"][]; + isExpired: boolean; + } >, hints: ClientHint, isBaseOrSelfService: boolean = false @@ -297,7 +300,7 @@ export async function upsertBooking( //update const res = await db.booking .update({ - where: { id }, + where: { id, organizationId }, data, include: { ...BOOKING_COMMON_INCLUDE, @@ -490,6 +493,7 @@ export async function upsertBooking( connect: { id: organizationId }, }; } + const res = await db.booking.create({ data: data as Prisma.BookingCreateInput, include: { ...BOOKING_COMMON_INCLUDE, organization: true }, @@ -711,6 +715,7 @@ export async function removeAssets({ lastName, userId, kitIds = [], + organizationId, }: { booking: Pick & { assetIds: Asset["id"][]; @@ -719,12 +724,13 @@ export async function removeAssets({ lastName: string; userId: string; kitIds?: Kit["id"][]; + organizationId: Booking["organizationId"]; }) { try { const { assetIds, id } = booking; const b = await db.booking.update({ // First, disconnect the assets from the booking - where: { id }, + where: { id, organizationId }, data: { assets: { disconnect: assetIds.map((id) => ({ id })), @@ -748,13 +754,13 @@ export async function removeAssets({ b.status === BookingStatus.OVERDUE ) { await db.asset.updateMany({ - where: { id: { in: assetIds } }, + where: { id: { in: assetIds }, organizationId }, data: { status: AssetStatus.AVAILABLE }, }); if (kitIds.length > 0) { await db.kit.updateMany({ - where: { id: { in: kitIds } }, + where: { id: { in: kitIds }, organizationId }, data: { status: KitStatus.AVAILABLE }, }); } @@ -782,15 +788,16 @@ export async function removeAssets({ } export async function deleteBooking( - booking: Pick, + booking: Pick, hints: ClientHint ) { try { - const { id } = booking; + const { id, organizationId } = booking; const activeBooking = await db.booking.findFirst({ where: { id, status: { in: [BookingStatus.OVERDUE, BookingStatus.ONGOING] }, + organizationId, }, include: { assets: { @@ -809,7 +816,7 @@ export async function deleteBooking( const hasKits = uniqueKitIds.size > 0; const b = await db.booking.delete({ - where: { id }, + where: { id, organizationId }, include: { ...BOOKING_COMMON_INCLUDE, ...bookingIncludeForEmails, diff --git a/app/modules/custody/service.server.ts b/app/modules/custody/service.server.ts index 5a433b07f..11bf6cc9a 100644 --- a/app/modules/custody/service.server.ts +++ b/app/modules/custody/service.server.ts @@ -3,10 +3,16 @@ import { AssetStatus } from "@prisma/client"; import { db } from "~/database/db.server"; import { ShelfError } from "~/utils/error"; -export async function releaseCustody({ assetId }: { assetId: Asset["id"] }) { +export async function releaseCustody({ + assetId, + organizationId, +}: { + assetId: Asset["id"]; + organizationId: Asset["organizationId"]; +}) { try { return await db.asset.update({ - where: { id: assetId }, + where: { id: assetId, organizationId }, data: { status: AssetStatus.AVAILABLE, custody: { diff --git a/app/modules/custom-field/service.server.ts b/app/modules/custom-field/service.server.ts index 973ccc624..8a66e32d8 100644 --- a/app/modules/custom-field/service.server.ts +++ b/app/modules/custom-field/service.server.ts @@ -236,6 +236,7 @@ export async function updateCustomField(payload: { active?: CustomField["active"]; options?: CustomField["options"]; categories?: string[]; + organizationId: CustomField["organizationId"]; }) { const { id, name, helpText, required, active, options, categories } = payload; @@ -339,6 +340,7 @@ export async function upsertCustomField( const updatedCustomField = await updateCustomField({ id: existingCustomField.id, options, + organizationId: def.organizationId, }); existingCustomField = updatedCustomField; newOrUpdatedFields.push(updatedCustomField); @@ -531,7 +533,7 @@ export async function bulkActivateOrDeactivateCustomFields({ const customFieldsIds = customFields.map((field) => field.id); const updatedFields = await db.customField.updateMany({ - where: { id: { in: customFieldsIds } }, + where: { id: { in: customFieldsIds }, organizationId }, data: { active }, }); diff --git a/app/modules/invite/service.server.ts b/app/modules/invite/service.server.ts index 8ecd5bfde..66cc01541 100644 --- a/app/modules/invite/service.server.ts +++ b/app/modules/invite/service.server.ts @@ -278,12 +278,8 @@ export async function updateInviteStatus({ }: Pick & { password: string }) { try { const invite = await db.invite.findFirst({ - where: { - id, - }, - include: { - inviteeTeamMember: true, - }, + where: { id }, + include: { inviteeTeamMember: true }, }); if (!invite || invite.status !== InviteStatuses.PENDING) { diff --git a/app/modules/kit/service.server.ts b/app/modules/kit/service.server.ts index db050e3e5..2f99c9276 100644 --- a/app/modules/kit/service.server.ts +++ b/app/modules/kit/service.server.ts @@ -124,10 +124,11 @@ export async function updateKit({ imageExpiration, status, createdById, + organizationId, }: UpdateKitPayload) { try { return await db.kit.update({ - where: { id }, + where: { id, organizationId }, data: { name, description, @@ -147,10 +148,12 @@ export async function updateKitImage({ request, kitId, userId, + organizationId, }: { request: Request; kitId: string; userId: string; + organizationId: Kit["organizationId"]; }) { try { const fileData = await parseFileFormData({ @@ -176,6 +179,7 @@ export async function updateKitImage({ image: signedUrl, imageExpiration: oneDayFromNow(), createdById: userId, + organizationId, }); } catch (cause) { throw new ShelfError({ @@ -581,13 +585,15 @@ export async function deleteKitImage({ export async function releaseCustody({ kitId, userId, + organizationId, }: { kitId: Kit["id"]; userId: string; + organizationId: Kit["organizationId"]; }) { try { const kit = await db.kit.findUniqueOrThrow({ - where: { id: kitId }, + where: { id: kitId, organizationId }, select: { id: true, name: true, @@ -599,7 +605,7 @@ export async function releaseCustody({ await Promise.all([ db.kit.update({ - where: { id: kitId }, + where: { id: kitId, organizationId }, data: { status: KitStatus.AVAILABLE, custody: { delete: true }, @@ -607,7 +613,7 @@ export async function releaseCustody({ }), ...kit.assets.map((asset) => db.asset.update({ - where: { id: asset.id }, + where: { id: asset.id, organizationId }, data: { status: AssetStatus.AVAILABLE, custody: { delete: true }, @@ -1177,7 +1183,7 @@ export async function updateKitQrCode({ // Disconnect all existing QR codes await db.kit .update({ - where: { id: kitId }, + where: { id: kitId, organizationId }, data: { qrCodes: { set: [], @@ -1196,7 +1202,7 @@ export async function updateKitQrCode({ // Connect the new QR code return await db.kit .update({ - where: { id: kitId }, + where: { id: kitId, organizationId }, data: { qrCodes: { connect: { id: newQrId }, @@ -1220,6 +1226,7 @@ export async function updateKitQrCode({ }); } } + export async function getAvailableKitAssetForBooking( kitIds: Kit["id"][] ): Promise { diff --git a/app/modules/kit/types.ts b/app/modules/kit/types.ts index 99994b04b..00ff8944d 100644 --- a/app/modules/kit/types.ts +++ b/app/modules/kit/types.ts @@ -12,6 +12,7 @@ export type UpdateKitPayload = Partial< > > & { id: Kit["id"]; + organizationId: Kit["organizationId"]; }; // Define the static includes diff --git a/app/modules/location/service.server.ts b/app/modules/location/service.server.ts index e8bc50c7f..6330f46c0 100644 --- a/app/modules/location/service.server.ts +++ b/app/modules/location/service.server.ts @@ -258,10 +258,13 @@ export async function createLocation({ } } -export async function deleteLocation({ id }: Pick) { +export async function deleteLocation({ + id, + organizationId, +}: Pick) { try { const location = await db.location.delete({ - where: { id }, + where: { id, organizationId }, }); if (location.imageId) { @@ -330,8 +333,8 @@ export async function updateLocation(payload: { } return await db.location.update({ - where: { id }, - data: data, + where: { id, organizationId }, + data, }); } catch (cause) { throw maybeUniqueConstraintViolation(cause, "Location", { diff --git a/app/modules/team-member/service.server.ts b/app/modules/team-member/service.server.ts index 8011518e1..3ec73b467 100644 --- a/app/modules/team-member/service.server.ts +++ b/app/modules/team-member/service.server.ts @@ -305,9 +305,17 @@ export async function getTeamMemberForCustodianFilter({ } } -export async function getTeamMember({ id }: { id: TeamMember["id"] }) { +export async function getTeamMember({ + id, + organizationId, +}: { + id: TeamMember["id"]; + organizationId: Organization["id"]; +}) { try { - return await db.teamMember.findUniqueOrThrow({ where: { id } }); + return await db.teamMember.findUniqueOrThrow({ + where: { id, organizationId }, + }); } catch (cause) { throw new ShelfError({ cause, diff --git a/app/routes/_layout+/assets.$assetId.overview.release-custody.tsx b/app/routes/_layout+/assets.$assetId.overview.release-custody.tsx index 0792091bb..a8792b578 100644 --- a/app/routes/_layout+/assets.$assetId.overview.release-custody.tsx +++ b/app/routes/_layout+/assets.$assetId.overview.release-custody.tsx @@ -113,7 +113,7 @@ export const action = async ({ }); try { - const { role } = await requirePermission({ + const { role, organizationId } = await requirePermission({ userId, request, entity: PermissionEntity.asset, @@ -145,7 +145,7 @@ export const action = async ({ } } - const asset = await releaseCustody({ assetId }); + const asset = await releaseCustody({ assetId, organizationId }); if (!asset.custody) { const formData = await request.formData(); diff --git a/app/routes/_layout+/assets.$assetId.overview.tsx b/app/routes/_layout+/assets.$assetId.overview.tsx index 00bb54aa8..c565c94eb 100644 --- a/app/routes/_layout+/assets.$assetId.overview.tsx +++ b/app/routes/_layout+/assets.$assetId.overview.tsx @@ -188,6 +188,13 @@ export async function action({ context, request, params }: ActionFunctionArgs) { }); try { + const { organizationId } = await requirePermission({ + userId, + request, + entity: PermissionEntity.asset, + action: PermissionAction.update, + }); + const formData = await request.formData(); const { intent } = parseData( formData, @@ -200,7 +207,11 @@ export async function action({ context, request, params }: ActionFunctionArgs) { AvailabilityForBookingFormSchema ); - await updateAssetBookingAvailability(id, availableToBook); + await updateAssetBookingAvailability({ + id, + organizationId, + availableToBook, + }); sendNotification({ title: "Asset availability status updated successfully", diff --git a/app/routes/_layout+/assets.new.tsx b/app/routes/_layout+/assets.new.tsx index 81763a6d8..b3412e431 100644 --- a/app/routes/_layout+/assets.new.tsx +++ b/app/routes/_layout+/assets.new.tsx @@ -182,6 +182,7 @@ export async function action({ context, request }: LoaderFunctionArgs) { assetId: asset.id, userId: authSession.userId, organizationId, + isNewAsset: true, }); sendNotification({ diff --git a/app/routes/_layout+/bookings.$bookingId.add-assets.tsx b/app/routes/_layout+/bookings.$bookingId.add-assets.tsx index 98641c3de..8c2e7da52 100644 --- a/app/routes/_layout+/bookings.$bookingId.add-assets.tsx +++ b/app/routes/_layout+/bookings.$bookingId.add-assets.tsx @@ -251,6 +251,7 @@ export async function action({ context, request, params }: ActionFunctionArgs) { firstName: user?.firstName || "", lastName: user?.lastName || "", userId: authSession.userId, + organizationId, }); } diff --git a/app/routes/_layout+/bookings.$bookingId.add-kits.tsx b/app/routes/_layout+/bookings.$bookingId.add-kits.tsx index ceaede6ba..db2ddef3d 100644 --- a/app/routes/_layout+/bookings.$bookingId.add-kits.tsx +++ b/app/routes/_layout+/bookings.$bookingId.add-kits.tsx @@ -179,7 +179,7 @@ export async function action({ context, request, params }: ActionFunctionArgs) { }); try { - await requirePermission({ + const { organizationId } = await requirePermission({ userId, request, entity: PermissionEntity.booking, @@ -243,6 +243,7 @@ export async function action({ context, request, params }: ActionFunctionArgs) { lastName: user?.lastName || "", userId, kitIds: removedKitIds, + organizationId, }); } diff --git a/app/routes/_layout+/bookings.$bookingId.tsx b/app/routes/_layout+/bookings.$bookingId.tsx index 8cb3d694c..9614e9310 100644 --- a/app/routes/_layout+/bookings.$bookingId.tsx +++ b/app/routes/_layout+/bookings.$bookingId.tsx @@ -432,7 +432,7 @@ export async function action({ context, request, params }: ActionFunctionArgs) { } const deletedBooking = await deleteBooking( - { id }, + { id, organizationId }, getClientHint(request) ); @@ -472,6 +472,7 @@ export async function action({ context, request, params }: ActionFunctionArgs) { firstName: user?.firstName || "", lastName: user?.lastName || "", userId: authSession.userId, + organizationId, }); sendNotification({ @@ -548,6 +549,7 @@ export async function action({ context, request, params }: ActionFunctionArgs) { firstName: user?.firstName || "", lastName: user?.lastName || "", userId: authSession.userId, + organizationId, }); sendNotification({ diff --git a/app/routes/_layout+/bookings.new.tsx b/app/routes/_layout+/bookings.new.tsx index 92ad05fe1..148c748ad 100644 --- a/app/routes/_layout+/bookings.new.tsx +++ b/app/routes/_layout+/bookings.new.tsx @@ -131,9 +131,9 @@ export async function action({ context, request }: ActionFunctionArgs) { { custodianUserId: custodian?.userId, custodianTeamMemberId: custodian?.id, - organizationId, name, description, + organizationId, from, to, assetIds, diff --git a/app/routes/_layout+/kits.$kitId.release-custody.tsx b/app/routes/_layout+/kits.$kitId.release-custody.tsx index 5d266d3e7..bee404e33 100644 --- a/app/routes/_layout+/kits.$kitId.release-custody.tsx +++ b/app/routes/_layout+/kits.$kitId.release-custody.tsx @@ -76,7 +76,7 @@ export async function action({ context, request, params }: ActionFunctionArgs) { }); try { - const { role } = await requirePermission({ + const { role, organizationId } = await requirePermission({ userId, request, entity: PermissionEntity.kit, @@ -108,6 +108,7 @@ export async function action({ context, request, params }: ActionFunctionArgs) { const kit = await releaseCustody({ kitId, userId, + organizationId, }); const { custodianName } = parseData( diff --git a/app/routes/_layout+/kits.$kitId_.edit.tsx b/app/routes/_layout+/kits.$kitId_.edit.tsx index fb983c209..8df1a9eba 100644 --- a/app/routes/_layout+/kits.$kitId_.edit.tsx +++ b/app/routes/_layout+/kits.$kitId_.edit.tsx @@ -113,11 +113,13 @@ export async function action({ context, request, params }: ActionFunctionArgs) { createdById: userId, name: payload.name, description: payload.description, + organizationId, }), updateKitImage({ request, kitId, userId, + organizationId, }), ]); diff --git a/app/routes/_layout+/kits.new.tsx b/app/routes/_layout+/kits.new.tsx index 15dc75390..308588dfc 100644 --- a/app/routes/_layout+/kits.new.tsx +++ b/app/routes/_layout+/kits.new.tsx @@ -86,6 +86,7 @@ export async function action({ context, request }: LoaderFunctionArgs) { request, kitId: kit.id, userId, + organizationId, }); sendNotification({ diff --git a/app/routes/_layout+/locations.$locationId.tsx b/app/routes/_layout+/locations.$locationId.tsx index fba46add2..0ed4b06b7 100644 --- a/app/routes/_layout+/locations.$locationId.tsx +++ b/app/routes/_layout+/locations.$locationId.tsx @@ -145,14 +145,14 @@ export async function action({ context, request, params }: ActionFunctionArgs) { ); try { - await requirePermission({ + const { organizationId } = await requirePermission({ userId: authSession.userId, request, entity: PermissionEntity.location, action: PermissionAction.delete, }); - await deleteLocation({ id }); + await deleteLocation({ id, organizationId }); sendNotification({ title: "Location deleted", diff --git a/app/routes/_layout+/locations.$locationId_.edit.tsx b/app/routes/_layout+/locations.$locationId_.edit.tsx index 411e269bd..86eeaaada 100644 --- a/app/routes/_layout+/locations.$locationId_.edit.tsx +++ b/app/routes/_layout+/locations.$locationId_.edit.tsx @@ -145,15 +145,15 @@ export default function LocationEditPage() { const { location } = useLoaderData(); return ( - <> +
-
+
- +
); } diff --git a/app/routes/_layout+/locations.new.tsx b/app/routes/_layout+/locations.new.tsx index 8c5ffd209..22e9bdd5b 100644 --- a/app/routes/_layout+/locations.new.tsx +++ b/app/routes/_layout+/locations.new.tsx @@ -134,11 +134,11 @@ export default function NewLocationPage() { const title = useAtomValue(dynamicTitleAtom); return ( - <> +
- +
); } diff --git a/app/routes/_layout+/settings.custom-fields.$fieldId_.edit.tsx b/app/routes/_layout+/settings.custom-fields.$fieldId_.edit.tsx index d5868f61b..44a80b7db 100644 --- a/app/routes/_layout+/settings.custom-fields.$fieldId_.edit.tsx +++ b/app/routes/_layout+/settings.custom-fields.$fieldId_.edit.tsx @@ -129,6 +129,7 @@ export async function action({ context, request, params }: ActionFunctionArgs) { required, options, categories, + organizationId, }); sendNotification({ diff --git a/app/routes/_layout+/settings.general.tsx b/app/routes/_layout+/settings.general.tsx index 68118d424..8ac0d7f65 100644 --- a/app/routes/_layout+/settings.general.tsx +++ b/app/routes/_layout+/settings.general.tsx @@ -175,6 +175,15 @@ export async function action({ context, request }: ActionFunctionArgs) { baseUserGroupId, } = payload; + /** User is allowed to edit his/her current organization only not other organizations. */ + if (currentOrganization.id !== id) { + throw new ShelfError({ + cause: null, + message: "You are not allowed to edit this organization.", + label: "Organization", + }); + } + const formDataFile = await unstable_parseMultipartFormData( request, unstable_createMemoryUploadHandler({ maxPartSize: MAX_SIZE }) diff --git a/app/routes/_layout+/settings.team.nrm.$nrmId.edit.tsx b/app/routes/_layout+/settings.team.nrm.$nrmId.edit.tsx index 0c7c93b87..aeae25474 100644 --- a/app/routes/_layout+/settings.team.nrm.$nrmId.edit.tsx +++ b/app/routes/_layout+/settings.team.nrm.$nrmId.edit.tsx @@ -35,14 +35,14 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { }); try { - await requirePermission({ + const { organizationId } = await requirePermission({ userId, request, entity: PermissionEntity.teamMember, action: PermissionAction.update, }); - const teamMember = await getTeamMember({ id: nrmId }); + const teamMember = await getTeamMember({ id: nrmId, organizationId }); return json(data({ showModal: true, teamMember })); } catch (cause) { @@ -60,7 +60,7 @@ export async function action({ context, request, params }: ActionFunctionArgs) { }); try { - await requirePermission({ + const { organizationId } = await requirePermission({ userId, request, entity: PermissionEntity.teamMember, @@ -70,7 +70,7 @@ export async function action({ context, request, params }: ActionFunctionArgs) { const { name } = parseData(await request.formData(), NewOrEditMemberSchema); await db.teamMember.update({ - where: { id: nrmId }, + where: { id: nrmId, organizationId }, data: { name: name.trim() }, }); diff --git a/app/routes/_layout+/settings.team.nrm.tsx b/app/routes/_layout+/settings.team.nrm.tsx index 630d21798..caad7a8dd 100644 --- a/app/routes/_layout+/settings.team.nrm.tsx +++ b/app/routes/_layout+/settings.team.nrm.tsx @@ -136,6 +136,7 @@ export async function action({ context, request }: ActionFunctionArgs) { .update({ where: { id: teamMemberId, + organizationId, }, data: { deletedAt: new Date(), diff --git a/app/routes/api+/asset.refresh-main-image.ts b/app/routes/api+/asset.refresh-main-image.ts index 6c3908b97..bccc591a2 100644 --- a/app/routes/api+/asset.refresh-main-image.ts +++ b/app/routes/api+/asset.refresh-main-image.ts @@ -16,6 +16,8 @@ export async function action({ context, request }: ActionFunctionArgs) { const { userId } = authSession; try { + // This is kind of a special case. Even tho we are editing the asset by updating the image + // we should still use "read" permission because we need base and self-service users to be able to see the images const { organizationId } = await requirePermission({ userId, request, diff --git a/app/routes/api+/kit.refresh-image.ts b/app/routes/api+/kit.refresh-image.ts index 8540addb8..b4851b952 100644 --- a/app/routes/api+/kit.refresh-image.ts +++ b/app/routes/api+/kit.refresh-image.ts @@ -4,6 +4,11 @@ import { updateKit } from "~/modules/kit/service.server"; import { makeShelfError, ShelfError } from "~/utils/error"; import { data, error, parseData } from "~/utils/http.server"; import { oneDayFromNow } from "~/utils/one-week-from-now"; +import { + PermissionAction, + PermissionEntity, +} from "~/utils/permissions/permission.data"; +import { requirePermission } from "~/utils/roles.server"; import { createSignedUrl } from "~/utils/storage.server"; export async function action({ context, request }: ActionFunctionArgs) { @@ -11,6 +16,15 @@ export async function action({ context, request }: ActionFunctionArgs) { const { userId } = authSession; try { + // This is kind of a special case. Even tho we are editing the kit by updating the image + // we should still use "read" permission because we need base and self-service users to be able to see the images + const { organizationId } = await requirePermission({ + userId, + request, + entity: PermissionEntity.kit, + action: PermissionAction.read, + }); + const { kitId, image } = parseData( await request.formData(), z.object({ @@ -42,6 +56,7 @@ export async function action({ context, request }: ActionFunctionArgs) { image: signedUrl, imageExpiration: oneDayFromNow(), createdById: userId, + organizationId, }); return json(data({ kit })); diff --git a/app/utils/import.server.ts b/app/utils/import.server.ts index 2656db69e..3fe65f253 100644 --- a/app/utils/import.server.ts +++ b/app/utils/import.server.ts @@ -47,6 +47,8 @@ export function extractCSVDataFromContentImport( case "imageUrl": // Return empty string if URL is empty/undefined, otherwise trim return [keys[index], value?.trim() || ""]; + case "bookable": + return [keys[index], !value ? null : value]; default: return [keys[index], value]; } diff --git a/app/utils/permissions/permission.data.ts b/app/utils/permissions/permission.data.ts index 03c978446..11867d2f6 100644 --- a/app/utils/permissions/permission.data.ts +++ b/app/utils/permissions/permission.data.ts @@ -49,7 +49,7 @@ export const Role2PermissionMap: { PermissionAction.delete, // This is for the user to delete their own bookings only when they are draft. PermissionAction.manageAssets, ], - [PermissionEntity.qr]: [], + [PermissionEntity.qr]: [PermissionAction.read], [PermissionEntity.category]: [], [PermissionEntity.customField]: [], [PermissionEntity.location]: [], @@ -79,7 +79,7 @@ export const Role2PermissionMap: { PermissionAction.manageAssets, PermissionAction.cancel, ], - [PermissionEntity.qr]: [], + [PermissionEntity.qr]: [PermissionAction.read], [PermissionEntity.category]: [], [PermissionEntity.customField]: [], [PermissionEntity.location]: [], diff --git a/package-lock.json b/package-lock.json index 305744f4f..e43a78486 100644 --- a/package-lock.json +++ b/package-lock.json @@ -135,7 +135,7 @@ "tsconfig-paths": "^4.2.0", "tsx": "^4.7.1", "typescript": "^5.1.0", - "vite": "^5.4.6", + "vite": "^5.4.14", "vite-plugin-cjs-interop": "^2.1.0", "vite-tsconfig-paths": "^4.3.2", "vitest": "^2.1.4" @@ -15459,9 +15459,10 @@ } }, "node_modules/miniflare/node_modules/undici": { - "version": "5.28.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", - "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", + "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", + "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -20979,9 +20980,10 @@ } }, "node_modules/undici": { - "version": "6.20.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz", - "integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==", + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "license": "MIT", "engines": { "node": ">=18.17" } @@ -21424,9 +21426,10 @@ } }, "node_modules/vite": { - "version": "5.4.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", - "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/package.json b/package.json index 72bd396a8..4f5df507e 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ "tsconfig-paths": "^4.2.0", "tsx": "^4.7.1", "typescript": "^5.1.0", - "vite": "^5.4.6", + "vite": "^5.4.14", "vite-plugin-cjs-interop": "^2.1.0", "vite-tsconfig-paths": "^4.3.2", "vitest": "^2.1.4"