From b2015f44910e1544ed5b9528699a55ecd626eefa Mon Sep 17 00:00:00 2001 From: FirePhoenix555 <53152322+FirePhoenix555@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:14:33 -0800 Subject: [PATCH 01/16] kit object fields --- common/inventory.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/common/inventory.ts b/common/inventory.ts index f34c8ac5..1fb237ff 100644 --- a/common/inventory.ts +++ b/common/inventory.ts @@ -16,7 +16,7 @@ export enum ITEM_RELATIVE_QUANTITY { export type ItemQuantity = ITEM_RELATIVE_QUANTITY | number; /** - * TLocation - Location of an inventory item + * TInventoryItemLocation - Location of an inventory item * @property room - The UUID of an area where the item is stored * @property quantity - of item stored in this location * @property container - (optional) subsection of room @@ -98,7 +98,7 @@ export const ITEM_ACCESS_DESCRIPTORS: { * @property available - (optional) The current available quantity * after accounting for checkouts/reservations (only applicable to items * with a number quantity, not relative quantity) - * @property locations - See {@link TLocation} documentation + * @property locations - See {@link TInventoryItemLocation} documentation * @property reorder_url - (optional) url for reordering item * @property serial_number - (optional) serial number of item * @property kit_contents - (optional) if kit, lists all item UUIDs in this kit @@ -108,6 +108,8 @@ export const ITEM_ACCESS_DESCRIPTORS: { * allowed to use this item. A user must have at least one of * these roles to checkout the given item. If null, any user may * checkout this item. + * @property kit_contents - The contents of the item, if it is a kit + * @property parent_kit - The UUID of the parent kit (if this item is in one) */ export type TInventoryItem = { uuid: InventoryItemUUID; @@ -124,4 +126,6 @@ export type TInventoryItem = { keywords?: string[]; required_certifications?: TRequiredCertificate[]; authorized_roles?: UserRoleUUID[] | null; + kit_contents?: InventoryItemUUID[]; + parent_kit?: InventoryItemUUID; }; From d4fba33c0011f58ebcc2df27c5a8fcd5576aa9fb Mon Sep 17 00:00:00 2001 From: FirePhoenix555 <53152322+FirePhoenix555@users.noreply.github.com> Date: Sun, 15 Feb 2026 16:49:02 -0800 Subject: [PATCH 02/16] kit object fields properly working with mongodb --- server/models/inventory.model.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/models/inventory.model.ts b/server/models/inventory.model.ts index 3d1e96bd..0fb39889 100644 --- a/server/models/inventory.model.ts +++ b/server/models/inventory.model.ts @@ -34,6 +34,8 @@ export const InventoryItem = new mongoose.Schema( required: false, }, authorized_roles: { type: [String], required: false }, + kit_contents: { type: [String], required: false }, + parent_kit: { type: String, required: false }, }, { collection: "inventory" }, ); From 342e48e6bfdb4f8e6095d8ac4b552191110ab974 Mon Sep 17 00:00:00 2001 From: FirePhoenix555 <53152322+FirePhoenix555@users.noreply.github.com> Date: Sun, 15 Feb 2026 16:50:18 -0800 Subject: [PATCH 03/16] can edit kit contents --- .../kiosks/admin/inventory/ItemEditorForm.tsx | 45 ++++- .../kiosks/admin/inventory/KitEditorModal.tsx | 178 ++++++++++++++++++ 2 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 website/components/kiosks/admin/inventory/KitEditorModal.tsx diff --git a/website/components/kiosks/admin/inventory/ItemEditorForm.tsx b/website/components/kiosks/admin/inventory/ItemEditorForm.tsx index 6173fad4..f2f295e0 100644 --- a/website/components/kiosks/admin/inventory/ItemEditorForm.tsx +++ b/website/components/kiosks/admin/inventory/ItemEditorForm.tsx @@ -33,6 +33,7 @@ import { BookmarkIcon, UserIcon, GlobeAmericasIcon, + BriefcaseIcon, } from "@heroicons/react/24/solid"; import { TUserRole } from "common/user"; import ItemRoleIcon from "./ItemRoleIcon"; @@ -42,6 +43,7 @@ import { motion } from "motion/react"; import ItemQuantityIcon from "./ItemQuantityIcon"; import ItemLocationModal from "./ItemLocationModal"; import { TArea } from "common/area"; +import KitEditorModal from "./KitEditorModal"; // Define the mutation function that will run when the form is submitted const createUpdateItem = async ({ @@ -248,12 +250,14 @@ export default function ItemEditorForm({ const [authrolesOpen, setAuthrolesOpen] = React.useState(false); // whether authroles edit modal is open const [locationEditorOpen, setLocationEditorOpen] = React.useState(false); // whether location editor modal is open + const [kitEditorOpen, setKitEditorOpen] = React.useState(false); // whether kit content edit modal is open const reqcertsMutation = patchMutation(() => setReqcertsOpen(false)); const authrolesMutation = patchMutation(() => setAuthrolesOpen(false)); const locationEditorMutation = patchMutation(() => setLocationEditorOpen(false), ); + const kitEditorMutation = patchMutation(() => setKitEditorOpen(false)); const [isNumericQuantity, setQtype] = React.useState( item.quantity >= 0, @@ -530,12 +534,7 @@ export default function ItemEditorForm({ Area - + Kit @@ -689,6 +688,33 @@ export default function ItemEditorForm({ {item.authorized_roles?.length ?? 0} + + {item.role == ITEM_ROLE.KIT && ( + + + + )} {/* */} @@ -748,6 +774,13 @@ export default function ItemEditorForm({ onOpenChange={setLocationEditorOpen} patchMutation={locationEditorMutation} /> + ); } diff --git a/website/components/kiosks/admin/inventory/KitEditorModal.tsx b/website/components/kiosks/admin/inventory/KitEditorModal.tsx new file mode 100644 index 00000000..8b9c39b7 --- /dev/null +++ b/website/components/kiosks/admin/inventory/KitEditorModal.tsx @@ -0,0 +1,178 @@ +import { + Button, + Modal, + Form, + ModalContent, + Input, +} from "@heroui/react"; +import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; + +import React from "react"; +import { UseMutationResult } from "@tanstack/react-query"; +import clsx from "clsx"; + +import { UUID } from "common/global"; +import { InventoryItemUUID } from "common/inventory"; + +export default function KitEditorModal< + // Allow any type that has a uuid and optional required_certs list + T extends { uuid: UUID; kit_contents?: InventoryItemUUID[] }, +>({ + element, + isOpen, + onOpenChange, + patchMutation, +}: { + element: T; + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + patchMutation: UseMutationResult< + T, + Error, + { + uuid: UUID; + patch: { + kit_contents?: InventoryItemUUID[]; + }; + } + >; +}) { + const [hasEdits, setHasEdits] = React.useState(false); + const [currentContents, setCurrentContents] = React.useState< + InventoryItemUUID[] + >(element.kit_contents || []); + + const onSubmit = React.useCallback( + (e: React.FormEvent) => { + // Prevent default browser page refresh. + e.preventDefault(); + + if (!hasEdits) return; + + patchMutation.reset(); + + // Run the mutation + patchMutation.mutate({ + uuid: element.uuid, + patch: { kit_contents: currentContents }, + }); + onOpenChange(false); + setHasEdits(false); + }, + [patchMutation, hasEdits, currentContents, element.uuid], + ); + + function wrapEdit( i: number ) { + return (val: InventoryItemUUID) => { + currentContents[i] = val; + setCurrentContents([...currentContents]); // update the instance list + setHasEdits(true); + }; + } + + const isValid = React.useMemo(() => { + for (let i = 0; i < currentContents.length; i++) { + if (currentContents[i] == "") { + return false; // invalid edit if either field is empty + } + } + + return hasEdits; // otherwise, valid iff edits made + }, [hasEdits, currentContents]); + + return ( + + + {(onClose) => ( +
+
+ Edit Kit Contents +
+ + {currentContents.map((content, i) => ( +
+ +
+ + + +
+
+ ))} + +
+ +
+ + +
+
+
+ )} +
+
+ ); +} From 536fc5c0e2f2bbe7a0c0a5650cca0422f68fa18a Mon Sep 17 00:00:00 2001 From: FirePhoenix555 <53152322+FirePhoenix555@users.noreply.github.com> Date: Sun, 15 Feb 2026 16:57:06 -0800 Subject: [PATCH 04/16] kit contents show in table --- .../components/kiosks/admin/inventory/InventoryTable.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/website/components/kiosks/admin/inventory/InventoryTable.tsx b/website/components/kiosks/admin/inventory/InventoryTable.tsx index f0a01d4b..9cebc7f2 100644 --- a/website/components/kiosks/admin/inventory/InventoryTable.tsx +++ b/website/components/kiosks/admin/inventory/InventoryTable.tsx @@ -53,6 +53,7 @@ const baseColumns = [ { name: "Locations", id: "locations" }, { name: "Required Certs", id: "required_certifications" }, { name: "Authorized Roles", id: "authorized_roles" }, + { name: "Kit Contents", id: "kit_contents" }, { name: "Keywords", id: "keywords" }, { name: "Serial Number", id: "serial_number" }, { name: "Reorder URL", id: "reorder_url" }, @@ -425,6 +426,13 @@ export default function InventoryTable({ ))} ), + kit_contents: (i) => ( +
+ {i.kit_contents?.map((content) => ( +

{content}

+ ))} +
+ ), access_type: (i) => (
{ITEM_ACCESS_DESCRIPTORS[i.access_type].label} From cf4b00d8f9663b3e9d8fc7a45f3dc19abf357938 Mon Sep 17 00:00:00 2001 From: FirePhoenix555 <53152322+FirePhoenix555@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:54:20 -0800 Subject: [PATCH 05/16] more abilities in adding items to kits - autocomplete element so you can search for items - kit content items now update their parent uuid --- .../kiosks/admin/inventory/ItemEditor.tsx | 3 + .../kiosks/admin/inventory/ItemEditorForm.tsx | 6 +- .../kiosks/admin/inventory/KitEditorModal.tsx | 99 ++++++++++++++----- website/pages/admin/InventoryKiosk.tsx | 1 + 4 files changed, 84 insertions(+), 25 deletions(-) diff --git a/website/components/kiosks/admin/inventory/ItemEditor.tsx b/website/components/kiosks/admin/inventory/ItemEditor.tsx index 195f2071..a019685e 100644 --- a/website/components/kiosks/admin/inventory/ItemEditor.tsx +++ b/website/components/kiosks/admin/inventory/ItemEditor.tsx @@ -16,6 +16,7 @@ export default function ItemEditor({ certs, roles, areas, + items, isNew, isDisabled, onUpdate = () => {}, @@ -24,6 +25,7 @@ export default function ItemEditor({ certs: TCertification[]; roles: TUserRole[]; areas: TArea[]; + items: TInventoryItem[]; isNew: boolean; isDisabled: boolean; onUpdate?: (isNew: boolean) => void; @@ -45,6 +47,7 @@ export default function ItemEditor({ certs={certs} roles={roles} areas={areas} + items={items} isMultiple={false} isDisabled={isDisabled} isNew={isNew} diff --git a/website/components/kiosks/admin/inventory/ItemEditorForm.tsx b/website/components/kiosks/admin/inventory/ItemEditorForm.tsx index f2f295e0..66f64d88 100644 --- a/website/components/kiosks/admin/inventory/ItemEditorForm.tsx +++ b/website/components/kiosks/admin/inventory/ItemEditorForm.tsx @@ -91,6 +91,7 @@ export default function ItemEditorForm({ certs, roles, areas, + items, isMultiple, isDisabled, isNew, @@ -100,6 +101,7 @@ export default function ItemEditorForm({ certs: TCertification[]; roles: TUserRole[]; areas: TArea[]; + items: TInventoryItem[]; isMultiple: boolean; isDisabled: boolean; isNew: boolean; @@ -777,9 +779,11 @@ export default function ItemEditorForm({ ); diff --git a/website/components/kiosks/admin/inventory/KitEditorModal.tsx b/website/components/kiosks/admin/inventory/KitEditorModal.tsx index 8b9c39b7..b627712f 100644 --- a/website/components/kiosks/admin/inventory/KitEditorModal.tsx +++ b/website/components/kiosks/admin/inventory/KitEditorModal.tsx @@ -4,6 +4,8 @@ import { Form, ModalContent, Input, + Autocomplete, + AutocompleteItem, } from "@heroui/react"; import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; @@ -12,21 +14,24 @@ import { UseMutationResult } from "@tanstack/react-query"; import clsx from "clsx"; import { UUID } from "common/global"; -import { InventoryItemUUID } from "common/inventory"; +import { InventoryItemUUID, ITEM_ROLE, TInventoryItem } from "../../../../../common/inventory"; export default function KitEditorModal< // Allow any type that has a uuid and optional required_certs list T extends { uuid: UUID; kit_contents?: InventoryItemUUID[] }, >({ element, + items, isOpen, onOpenChange, - patchMutation, + kitPatchMutation, + contentPatchMutation, }: { element: T; + items: TInventoryItem[]; isOpen: boolean; onOpenChange: (isOpen: boolean) => void; - patchMutation: UseMutationResult< + kitPatchMutation: UseMutationResult< T, Error, { @@ -36,11 +41,28 @@ export default function KitEditorModal< }; } >; + contentPatchMutation: UseMutationResult< + T, + Error, + { + uuid: UUID; + patch: { + parent_kit?: InventoryItemUUID; + }; + } + >; }) { + items = items.filter(itm => + itm.role == ITEM_ROLE.MATERIAL || + itm.role == ITEM_ROLE.TOOL || + itm.role == ITEM_ROLE.MACHINE + ); + const [hasEdits, setHasEdits] = React.useState(false); const [currentContents, setCurrentContents] = React.useState< InventoryItemUUID[] >(element.kit_contents || []); + const [removedContents, setRemovedContents] = React.useState([]); const onSubmit = React.useCallback( (e: React.FormEvent) => { @@ -49,22 +71,43 @@ export default function KitEditorModal< if (!hasEdits) return; - patchMutation.reset(); + // Update "parent_kit" attribute of each (added or removed) item + + for (const content of currentContents) { + contentPatchMutation.reset(); + contentPatchMutation.mutate({ + uuid: content, + patch: { parent_kit: element.uuid }, + }); + } + + for (const content of removedContents) { + contentPatchMutation.reset(); + contentPatchMutation.mutate({ + uuid: content, + patch: { parent_kit: "" }, + }); + } + + kitPatchMutation.reset(); // Run the mutation - patchMutation.mutate({ + kitPatchMutation.mutate({ uuid: element.uuid, patch: { kit_contents: currentContents }, }); + onOpenChange(false); setHasEdits(false); }, - [patchMutation, hasEdits, currentContents, element.uuid], + [kitPatchMutation, contentPatchMutation, hasEdits, currentContents, removedContents, element.uuid], ); function wrapEdit( i: number ) { - return (val: InventoryItemUUID) => { - currentContents[i] = val; + return (val: any) => { + removedContents.push(currentContents[i]); // old item has been removed from the kit + currentContents[i] = val as InventoryItemUUID; + setRemovedContents(removedContents.filter(c => !currentContents.includes(c))); // if a prev removed item was added back, it shouldn't be marked removed setCurrentContents([...currentContents]); // update the instance list setHasEdits(true); }; @@ -104,29 +147,37 @@ export default function KitEditorModal< >
- j != i), ...(items.filter(itm => itm.parent_kit != "" && itm.parent_kit != element.uuid) as any[])]} variant="faded" color="primary" size="md" - classNames={{ - input: clsx([ - "placeholder:text-default-500", - "placeholder:italic", - "text-default-700", - ]), - }} - /> + isRequired={true} + + // classNames={{ + // input: clsx([ + // "placeholder:text-default-500", + // "placeholder:italic", + // "text-default-700", + // ]), + // }} + > + {items.map((itm) => ( + {itm.name} + ))} + diff --git a/website/pages/admin/InventoryKiosk.tsx b/website/pages/admin/InventoryKiosk.tsx index a88cfd99..f6c70bea 100644 --- a/website/pages/admin/InventoryKiosk.tsx +++ b/website/pages/admin/InventoryKiosk.tsx @@ -131,6 +131,7 @@ export default function InventoryKiosk() { certs={certs} roles={roles} areas={areas} + items={inventory} isDisabled={ !isNewItem && (!item || From f9a8d8c21d9a0875ae08113df6307563146dff08 Mon Sep 17 00:00:00 2001 From: FirePhoenix555 <53152322+FirePhoenix555@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:59:45 -0800 Subject: [PATCH 06/16] kit items don't show in inv table --- website/components/kiosks/admin/inventory/InventoryTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/components/kiosks/admin/inventory/InventoryTable.tsx b/website/components/kiosks/admin/inventory/InventoryTable.tsx index 9cebc7f2..1aabc5e3 100644 --- a/website/components/kiosks/admin/inventory/InventoryTable.tsx +++ b/website/components/kiosks/admin/inventory/InventoryTable.tsx @@ -326,7 +326,7 @@ export default function InventoryTable({
!item.parent_kit)} columns={columns} visibleColumns={visibleColumns} selectedKeys={selectedKeys} From bbdeadfb52b6f438414aa1c2b99e8e394a625e21 Mon Sep 17 00:00:00 2001 From: FirePhoenix555 <53152322+FirePhoenix555@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:13:21 -0800 Subject: [PATCH 07/16] 404 bugfix --- .../kiosks/admin/inventory/KitEditorModal.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/website/components/kiosks/admin/inventory/KitEditorModal.tsx b/website/components/kiosks/admin/inventory/KitEditorModal.tsx index b627712f..d35f4e03 100644 --- a/website/components/kiosks/admin/inventory/KitEditorModal.tsx +++ b/website/components/kiosks/admin/inventory/KitEditorModal.tsx @@ -105,10 +105,16 @@ export default function KitEditorModal< function wrapEdit( i: number ) { return (val: any) => { - removedContents.push(currentContents[i]); // old item has been removed from the kit + let oldContent = currentContents[i]; currentContents[i] = val as InventoryItemUUID; - setRemovedContents(removedContents.filter(c => !currentContents.includes(c))); // if a prev removed item was added back, it shouldn't be marked removed setCurrentContents([...currentContents]); // update the instance list + + // if not being set for the first time + if (oldContent != "") { + removedContents.push(oldContent); // old item has been removed from the kit + setRemovedContents(removedContents.filter(c => !currentContents.includes(c))); // if a prev removed item was added back, it shouldn't be marked removed + } + setHasEdits(true); }; } From 60e80d06a3f7dd2fbd17330152b6699147087ade Mon Sep 17 00:00:00 2001 From: FirePhoenix555 <53152322+FirePhoenix555@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:17:45 -0800 Subject: [PATCH 08/16] customizable if items in kits show in inv table --- website/components/kiosks/admin/inventory/InventoryTable.tsx | 4 +++- website/pages/admin/InventoryKiosk.tsx | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/website/components/kiosks/admin/inventory/InventoryTable.tsx b/website/components/kiosks/admin/inventory/InventoryTable.tsx index 1aabc5e3..78493b79 100644 --- a/website/components/kiosks/admin/inventory/InventoryTable.tsx +++ b/website/components/kiosks/admin/inventory/InventoryTable.tsx @@ -83,6 +83,7 @@ export default function InventoryTable({ "authorized_roles", ], customColumnComponents, + showsKitContents = false, editable = false, emptyContent, onCreate = undefined, @@ -104,6 +105,7 @@ export default function InventoryTable({ customColumnComponents?: { [column_id: string]: (item: TInventoryItem) => React.ReactNode; }; + showsKitContents?: boolean; emptyContent?: string; editable?: React.ReactNode; onCreate?: (state: boolean) => void; @@ -326,7 +328,7 @@ export default function InventoryTable({ !item.parent_kit)} + content={showsKitContents ? filteredItems : filteredItems.filter(item => !item.parent_kit)} columns={columns} visibleColumns={visibleColumns} selectedKeys={selectedKeys} diff --git a/website/pages/admin/InventoryKiosk.tsx b/website/pages/admin/InventoryKiosk.tsx index f6c70bea..e5ebb75f 100644 --- a/website/pages/admin/InventoryKiosk.tsx +++ b/website/pages/admin/InventoryKiosk.tsx @@ -152,6 +152,7 @@ export default function InventoryKiosk() { selectedKeys={selectedItems} onSelectionChange={betterSelectionChange} isLoading={inventoryLoading} + showsKitContents={true} editable onCreate={setIsNewItem} /> From 6992f6b640fc39376264b919114887a56005f777 Mon Sep 17 00:00:00 2001 From: FirePhoenix555 <53152322+FirePhoenix555@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:23:01 -0800 Subject: [PATCH 09/16] inv table shows kit content names (rather than uuids) --- website/components/kiosks/admin/inventory/InventoryTable.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/components/kiosks/admin/inventory/InventoryTable.tsx b/website/components/kiosks/admin/inventory/InventoryTable.tsx index 78493b79..36e92b81 100644 --- a/website/components/kiosks/admin/inventory/InventoryTable.tsx +++ b/website/components/kiosks/admin/inventory/InventoryTable.tsx @@ -431,7 +431,9 @@ export default function InventoryTable({ kit_contents: (i) => (
{i.kit_contents?.map((content) => ( -

{content}

+
+ {inventory.find(a => a.uuid == content)?.name} +
))}
), From 57b696350d3831de3fa0a9e7ba0433da7b2bc4e3 Mon Sep 17 00:00:00 2001 From: FirePhoenix555 <53152322+FirePhoenix555@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:40:11 -0800 Subject: [PATCH 10/16] can quick jump to kit content / parent kit --- .../kiosks/admin/inventory/ItemEditor.tsx | 3 ++ .../kiosks/admin/inventory/ItemEditorForm.tsx | 47 +++++++++++++++++++ .../kiosks/admin/inventory/KitEditorModal.tsx | 16 ++++++- website/pages/admin/InventoryKiosk.tsx | 1 + 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/website/components/kiosks/admin/inventory/ItemEditor.tsx b/website/components/kiosks/admin/inventory/ItemEditor.tsx index a019685e..a9c38aec 100644 --- a/website/components/kiosks/admin/inventory/ItemEditor.tsx +++ b/website/components/kiosks/admin/inventory/ItemEditor.tsx @@ -20,6 +20,7 @@ export default function ItemEditor({ isNew, isDisabled, onUpdate = () => {}, + setSelectedItem, }: { item: TInventoryItem; certs: TCertification[]; @@ -29,6 +30,7 @@ export default function ItemEditor({ isNew: boolean; isDisabled: boolean; onUpdate?: (isNew: boolean) => void; + setSelectedItem: (s: Set) => void; }) { const key = isNew ? "new" : item.uuid; @@ -52,6 +54,7 @@ export default function ItemEditor({ isDisabled={isDisabled} isNew={isNew} onUpdate={onUpdate} + setSelectedItem={setSelectedItem} /> {}, + setSelectedItem, }: { item: TInventoryItem; certs: TCertification[]; @@ -106,6 +108,7 @@ export default function ItemEditorForm({ isDisabled: boolean; isNew: boolean; onUpdate?: (isNew: boolean) => void; // Function to run when the item is updated + setSelectedItem: (s: Set) => void; }) { const [UUID, setUUID] = React.useState( isNew ? crypto.randomUUID() : item.uuid, @@ -606,6 +609,49 @@ export default function ItemEditorForm({ ]), }} /> + +
+