From 65269f172f8b61daff258cb7d45620b2c58c8eea Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 14 Mar 2026 21:31:40 -0500 Subject: [PATCH] inventory2 --- INSTRUCTIONS.md | 2 + README.md | 3 + STRUCTURE.md | 2 +- .../modules/inventory/InventoryFormPage.tsx | 111 +++++++++++++++--- 4 files changed, 103 insertions(+), 15 deletions(-) diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index e4fd880..ee1dbbc 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -18,6 +18,7 @@ This repository implements the platform foundation milestone: 3. Add Prisma models and migrations for all persisted schema changes. 4. Keep uploaded files on disk under `/app/data/uploads`; never store blobs in SQLite. 5. Reuse shared DTOs and permission keys from the `shared` package. +6. Any UI that looks up items by SKU or item name must use a searchable picker/autocomplete, not a long dropdown. ## Operational notes @@ -27,6 +28,7 @@ This repository implements the platform foundation milestone: - Prefer Node 22 locally when running Prisma migration commands to match the Docker runtime. - Branding defaults live in the frontend theme token layer and are overridden by the persisted company profile. - Back up the whole `/app/data` volume to capture both the database and attachments. +- Treat searchable SKU lookup as a standing UX requirement for inventory, BOM, sales, purchasing, and manufacturing flows. ## Next roadmap candidates diff --git a/README.md b/README.md index 0033392..f4ac833 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ The current inventory foundation supports: - protected item master list, detail, create, and edit flows - SKU, description, type, status, unit-of-measure, sellable/purchasable, default cost, and notes fields - BOM header and BOM line editing directly on the item form +- searchable component lookup for BOM lines, designed for large item catalogs - BOM detail display with component SKU, name, quantity, unit, notes, and position - protected warehouse list, detail, create, and edit flows - nested stock-location management inside each warehouse record @@ -92,6 +93,8 @@ The current inventory foundation supports: This module introduces `inventory.read` and `inventory.write` permissions. After updating the code, restart the server against the migrated database so bootstrap can upsert the new permissions onto the default administrator role. +Moving forward, any UI that requires searching for an item by SKU or item name should use a searchable picker/autocomplete rather than a static dropdown. + ## Branding Brand colors and typography are configured through the Company Settings page and the frontend theme token layer. Update runtime branding in-app, or adjust defaults in the theme config if you need a new baseline brand. diff --git a/STRUCTURE.md b/STRUCTURE.md index 1ce5dc2..76d6370 100644 --- a/STRUCTURE.md +++ b/STRUCTURE.md @@ -15,6 +15,7 @@ - Keep reusable UI primitives in `src/components`. - Theme state and brand tokens belong in `src/theme`. - PDF screen components must remain separate from API-rendered document templates. +- Any item/SKU lookup UI must be implemented as a searchable picker or autocomplete; do not use long static dropdowns for inventory-scale datasets. ## Backend rules @@ -36,4 +37,3 @@ 3. Add permission keys in `shared/src/auth`. 4. Add frontend route/module under `client/src/modules/`. 5. Register navigation and route guards through the app shell without refactoring existing modules. - diff --git a/client/src/modules/inventory/InventoryFormPage.tsx b/client/src/modules/inventory/InventoryFormPage.tsx index adf9f54..ac965ea 100644 --- a/client/src/modules/inventory/InventoryFormPage.tsx +++ b/client/src/modules/inventory/InventoryFormPage.tsx @@ -16,9 +16,16 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) { const { itemId } = useParams(); const [form, setForm] = useState(emptyInventoryItemInput); const [componentOptions, setComponentOptions] = useState([]); + const [componentSearchTerms, setComponentSearchTerms] = useState([]); + const [activeComponentPicker, setActiveComponentPicker] = useState(null); const [status, setStatus] = useState(mode === "create" ? "Create a new inventory item." : "Loading inventory item..."); const [isSaving, setIsSaving] = useState(false); + function getComponentLabel(componentItemId: string) { + const match = componentOptions.find((option) => option.id === componentItemId); + return match ? `${match.sku} - ${match.name}` : ""; + } + useEffect(() => { if (!token) { return; @@ -26,9 +33,22 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) { api .getInventoryItemOptions(token) - .then((options) => setComponentOptions(options.filter((option) => option.id !== itemId))) + .then((options) => { + const nextOptions = options.filter((option) => option.id !== itemId); + setComponentOptions(nextOptions); + setComponentSearchTerms((current) => + form.bomLines.map((line, index) => { + if (current[index]?.trim()) { + return current[index]; + } + + const match = nextOptions.find((option) => option.id === line.componentItemId); + return match ? `${match.sku} - ${match.name}` : ""; + }) + ); + }) .catch(() => setComponentOptions([])); - }, [itemId, token]); + }, [form.bomLines, itemId, token]); useEffect(() => { if (mode !== "edit" || !token || !itemId) { @@ -57,6 +77,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) { position: line.position, })), }); + setComponentSearchTerms(item.bomLines.map((line) => `${line.componentSku} - ${line.componentName}`)); setStatus("Inventory item loaded."); }) .catch((error: unknown) => { @@ -76,6 +97,14 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) { })); } + function updateComponentSearchTerm(index: number, value: string) { + setComponentSearchTerms((current) => { + const nextTerms = [...current]; + nextTerms[index] = value; + return nextTerms; + }); + } + function addBomLine() { setForm((current) => ({ ...current, @@ -87,6 +116,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) { }, ], })); + setComponentSearchTerms((current) => [...current, ""]); } function removeBomLine(index: number) { @@ -94,6 +124,8 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) { ...current, bomLines: current.bomLines.filter((_line, lineIndex) => lineIndex !== index), })); + setComponentSearchTerms((current) => current.filter((_term, termIndex) => termIndex !== index)); + setActiveComponentPicker((current) => (current === index ? null : current != null && current > index ? current - 1 : current)); } async function handleSubmit(event: React.FormEvent) { @@ -268,18 +300,69 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {