Skip to content

Commit

Permalink
Merge pull request #1319 from Shelf-nu/1290-feat-allow-importing-with…
Browse files Browse the repository at this point in the history
…-linking-to-existing-qr-code

feat: allow linking of QR codes on import
  • Loading branch information
DonKoko authored Sep 23, 2024
2 parents 528f6fc + 860ae9c commit 457a4a4
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 7 deletions.
98 changes: 98 additions & 0 deletions app/components/assets/import-content.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ChangeEvent } from "react";
import { useRef, useState } from "react";
import { useFetcher } from "@remix-run/react";
import type { QRCodePerImportedAsset } from "~/modules/qr/service.server";
import type { action } from "~/routes/_layout+/assets.import";
import { isFormProcessing } from "~/utils/form";
import { tw } from "~/utils/tw";
Expand All @@ -18,6 +19,7 @@ import {
AlertDialogTrigger,
} from "../shared/modal";
import { WarningBox } from "../shared/warning-box";
import { Table, Td, Th, Tr } from "../table";

export const ImportBackup = () => (
<>
Expand Down Expand Up @@ -110,6 +112,35 @@ export const ImportContent = () => (
<b>"cf:purchase date, type:date"</b>
</div>

<h4 className="mt-2">Importing with QR codes</h4>
<div>
You also have the option to se a Shelf QR code for each asset. This is
very valuable if you already have Shelf QR codes printed and you want to
link them to the assets you are importing.
<br />
This feature comes with the following limitations:
<ul className="list-inside list-disc pl-4">
<li>
<b>Existing code</b> - the QR code needs to already exist in shelf
</li>
<li>
<b>No duplicate codes</b> - the qrId needs to be unique for each asset
</li>
<li>
<b>No linked codes</b> - the qrId needs not be linked to any asset or
kit
</li>
<li>
<b>QR ownership</b> - the QR code needs to be either unclaimed or
belong to the organization you are trying to import it to.
</li>
</ul>
If no <b>"qrId"</b> is used a new QR code will be generated.
<br />
If you are interesting in receiving some unclaimed or unlinked codes, feel
free to get in touch with support and we can provide those for you.
</div>

<h4 className="mt-2">Extra considerations</h4>
<ul className="list-inside list-disc">
<li>
Expand Down Expand Up @@ -212,6 +243,43 @@ export const FileForm = ({ intent, url }: { intent: string; url?: string }) => {
<div>
<h5 className="text-red-500">{data.error.title}</h5>
<p className="text-red-500">{data.error.message}</p>
{data?.error?.additionalData?.duplicateCodes ? (
<BrokenQrCodesTable
title="Duplicate codes"
data={
data.error.additionalData
.duplicateCodes as QRCodePerImportedAsset[]
}
/>
) : null}
{data?.error?.additionalData?.nonExistentCodes ? (
<BrokenQrCodesTable
title="Non existent codes"
data={
data.error.additionalData
.nonExistentCodes as QRCodePerImportedAsset[]
}
/>
) : null}
{data?.error?.additionalData?.linkedCodes ? (
<BrokenQrCodesTable
title="Already linked codes"
data={
data.error.additionalData
.linkedCodes as QRCodePerImportedAsset[]
}
/>
) : null}
{data?.error?.additionalData?.connectedToOtherOrgs ? (
<BrokenQrCodesTable
title="Some codes do not belong to this organization"
data={
data.error.additionalData
.connectedToOtherOrgs as QRCodePerImportedAsset[]
}
/>
) : null}

<p className="mt-2">
Please fix your CSV file and try again. If the issue persists,
don't hesitate to get in touch with us.
Expand Down Expand Up @@ -261,3 +329,33 @@ export const FileForm = ({ intent, url }: { intent: string; url?: string }) => {
</fetcher.Form>
);
};

function BrokenQrCodesTable({
title,
data,
}: {
title: string;
data: QRCodePerImportedAsset[];
}) {
return (
<div className="mt-3">
<h5>{title}</h5>
<Table className="mt-1 [&_td]:p-1 [&_th]:p-1">
<thead>
<Tr>
<Th>Asset title</Th>
<Th>QR ID</Th>
</Tr>
</thead>
<tbody>
{data.map((code: { title: string; qrId: string }) => (
<Tr key={code.title}>
<Td>{code.title}</Td>
<Td>{code.qrId}</Td>
</Tr>
))}
</tbody>
</Table>
</div>
);
}
21 changes: 17 additions & 4 deletions app/modules/asset/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from "~/modules/custom-field/service.server";
import type { CustomFieldDraftPayload } from "~/modules/custom-field/types";
import { createLocationsIfNotExists } from "~/modules/location/service.server";
import { getQr } from "~/modules/qr/service.server";
import { getQr, parseQrCodesFromImportData } from "~/modules/qr/service.server";
import { createTagsIfNotExists } from "~/modules/tag/service.server";
import {
createTeamMemberIfNotExists,
Expand Down Expand Up @@ -740,10 +740,11 @@ export async function createAsset({
* 2. If the qr code belongs to the current organization
* 3. If the qr code is not linked to an asset or a kit
*/

const qr = qrId ? await getQr({ id: qrId }) : null;
const qrCodes =
qr &&
qr.organizationId === organizationId &&
(qr.organizationId === organizationId || !qr.organizationId) &&
qr.assetId === null &&
qr.kitId === null
? { connect: { id: qrId } }
Expand Down Expand Up @@ -1737,6 +1738,12 @@ export async function createAssetsFromContentImport({
organizationId: Organization["id"];
}) {
try {
const qrCodesPerAsset = await parseQrCodesFromImportData({
data,
organizationId,
userId,
});

const kits = await createKitsIfNotExists({
data,
userId,
Expand Down Expand Up @@ -1791,6 +1798,7 @@ export async function createAssetsFromContentImport({
}, [] as ShelfAssetCustomFieldValueType[]);

await createAsset({
qrId: qrCodesPerAsset.find((item) => item?.title === asset.title)?.qrId,
organizationId,
title: asset.title,
description: asset.description || "",
Expand All @@ -1812,12 +1820,17 @@ export async function createAssetsFromContentImport({
});
}
} catch (cause) {
const isShelfError = isLikeShelfError(cause);
throw new ShelfError({
cause,
message: isLikeShelfError(cause)
message: isShelfError
? cause?.message
: "Something went wrong while creating assets from content import",
additionalData: { userId, organizationId },
additionalData: {
userId,
organizationId,
...(isShelfError && cause.additionalData),
},
label,
});
}
Expand Down
126 changes: 126 additions & 0 deletions app/modules/qr/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { id } from "~/utils/id/id.server";
import { getParamsValues } from "~/utils/list";
// eslint-disable-next-line import/no-cycle
import { generateCode } from "./utils.server";
import type { CreateAssetFromContentImportPayload } from "../asset/types";
import { generateRandomCode } from "../invite/helpers";

const label: ErrorLabel = "QR";
Expand Down Expand Up @@ -486,3 +487,128 @@ export async function getQrCodeMaps({
}
return finalMap;
}

/** Extracts qrCodes from data and checks their validity for import
* You can only import unclaimed or unlinked codes
* - For non-existing codes - we can allow them to be imported
* - For linked codes - we don't allow any imports
* - For unlinked - we can only allowed if the code is already claimed within the current workspace the user is trying to import to
* - For unclaimed - there are not really any limitations we need to place. This should work directly
*/

export type QRCodePerImportedAsset = {
title: string;
qrId: string;
};

export async function parseQrCodesFromImportData({
data,
userId,
organizationId,
}: {
data: CreateAssetFromContentImportPayload[];
userId: User["id"];
organizationId: Organization["id"];
}) {
try {
const qrCodePerAsset = data
.map((asset) => {
if (asset.qrId) {
return {
title: asset.title,
qrId: asset.qrId,
};
}
return null;
})
.filter((asset) => asset !== null); // Filter out null values

const codes = await db.qr.findMany({
where: {
id: {
in: qrCodePerAsset.map((asset) => asset?.qrId),
},
},
});

/** Check for any codes that are present more than 1 time in the data */
const duplicateCodes = qrCodePerAsset.filter(
(asset, index, self) =>
self.findIndex((t) => t?.qrId === asset?.qrId) !== index
);

if (duplicateCodes.length) {
throw new ShelfError({
cause: null,
message:
"Some of the QR codes you are trying to import are present more than once in the data. Please make sure each QR code is only present once.",
additionalData: { duplicateCodes },
label,
});
}

/** Check if any of the codes are non-existent */
const nonExistentCodes = qrCodePerAsset.filter(
(asset) => !codes.find((code) => code.id === asset?.qrId) && asset?.qrId
);

if (nonExistentCodes.length) {
throw new ShelfError({
cause: null,
message: "Some of the QR codes you are trying to import do not exist",
additionalData: { nonExistentCodes },
label,
});
}

/** Check for codes already linked to asset or kit. Returns QRCodePerImportedAsset[] */
const linkedCodes = qrCodePerAsset.filter((asset) =>
codes.find(
(code) => code.id === asset?.qrId && (code.assetId || code.kitId)
)
);
if (linkedCodes.length) {
throw new ShelfError({
cause: null,
message:
"Some of the QR codes you are trying to import are already linked to an asset or a kit. Please use unlinkned or unclaimed codes for your import.",
additionalData: { linkedCodes },
label,
});
}

/** Check for codes linked to other any organization and the organization is different than the current one */
const connectedToOtherOrgs = qrCodePerAsset.filter((asset) =>
codes.find(
(code) =>
code.id === asset?.qrId &&
code.organizationId &&
code.organizationId !== organizationId
)
);
if (connectedToOtherOrgs.length) {
throw new ShelfError({
cause: null,
message:
"Some of the QR codes you are trying to import don't belong to your current organization. You can only import codes that are unclaimed, unlinked or linked to your organization.",
additionalData: { connectedToOtherOrgs },
label,
});
}

return qrCodePerAsset;
} catch (cause) {
const isShelfError = isLikeShelfError(cause);
throw new ShelfError({
cause,
message: isShelfError ? cause.message : "Failed to get qr codes",
additionalData: {
data,
userId,
organizationId,
...(isShelfError && cause.additionalData),
},
label,
});
}
}
6 changes: 3 additions & 3 deletions public/static/shelf.nu-example-asset-import-from-content.csv
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
title,description,kit,category,tags,location,valuation,custodian,"cf: Serial number,type:text","cf:brand, type:option","cf:purchase date, type:date"
AMD Ryzen,"CPU from my new home PC",Home PC,CPU,"High priority, small",Sofia office,100,John,WQE239123d,amd,02/22/2024
"Macbook Pro.","New laptop",Working gear,Laptop,"High priority, mid-size",Dutch office,2500,Thea,43543we23d,apple,03/12/2022
qrId,title,description,kit,category,tags,location,valuation,custodian,"cf: Serial number,type:text","cf:brand, type:option","cf:purchase date, type:date"
abcde12345,AMD Ryzen,"CPU from my new home PC",Home PC,CPU,"High priority, small",Sofia office,100,John,WQE239123d,amd,02/22/2024
12345abcde"Macbook Pro.","New laptop",Working gear,Laptop,"High priority, mid-size",Dutch office,2500,Thea,43543we23d,apple,03/12/2022

0 comments on commit 457a4a4

Please sign in to comment.