From 10b47da724470fd10d9b765e98f3b627337ccb43 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 14 Mar 2026 22:37:09 -0500 Subject: [PATCH] inventory --- INSTRUCTIONS.md | 7 +- README.md | 12 + ROADMAP.md | 14 +- STRUCTURE.md | 1 + client/src/lib/api.ts | 15 ++ .../modules/inventory/InventoryDetailPage.tsx | 225 ++++++++++++++++- .../modules/inventory/InventoryFormPage.tsx | 6 +- .../InventoryTransactionTypeBadge.tsx | 13 + client/src/modules/inventory/config.ts | 28 ++- .../migration.sql | 27 ++ server/prisma/schema.prisma | 26 ++ server/src/modules/inventory/router.ts | 38 ++- server/src/modules/inventory/service.ts | 232 +++++++++++++++--- shared/src/inventory/types.ts | 50 ++++ 14 files changed, 651 insertions(+), 43 deletions(-) create mode 100644 client/src/modules/inventory/InventoryTransactionTypeBadge.tsx create mode 100644 server/prisma/migrations/20260314235500_inventory_transactions/migration.sql diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index ee1dbbc..1f2299b 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -8,6 +8,8 @@ This repository implements the platform foundation milestone: - local auth and RBAC - company settings and branding - file attachment storage +- CRM foundation through reseller hierarchy, contacts, attachments, and lifecycle metadata +- inventory master data, BOM, warehouse, and stock-location foundation - Dockerized single-container deployment - Puppeteer PDF pipeline foundation @@ -19,6 +21,7 @@ This repository implements the platform foundation milestone: 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. +7. Maintain the denser UI baseline on active screens; avoid reintroducing oversized `px-4 py-3` style controls, tall action bars, or overly loose card spacing without a specific reason. ## Operational notes @@ -32,8 +35,8 @@ This repository implements the platform foundation milestone: ## Next roadmap candidates -- CRM entity detail pages and search -- inventory and BOM management +- inventory transactions and on-hand tracking +- BOM/item drawing attachments and support documents - sales orders, purchase orders, and document templates - shipping workflows and printable logistics documents - manufacturing gantt scheduling with live project data diff --git a/README.md b/README.md index f4ac833..cd3dff7 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ The current inventory foundation supports: - 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 +- SKU-first searchable component lookup for BOM lines, with SKU shown in the picker and description kept separate in the selected row - 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 @@ -95,6 +96,16 @@ This module introduces `inventory.read` and `inventory.write` permissions. After 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. +## UI Density + +The active client screens have been normalized toward a denser workspace layout: + +- form controls and action bars use tighter padding +- CRM, inventory, warehouse, settings, dashboard, and login screens use reduced card spacing +- headings, metric cards, empty states, and long-text blocks were tightened for better data density + +This denser layout is now the baseline for future screens. New pages should avoid reverting to oversized card padding, tall action bars, or long static dropdowns for operational datasets. + ## 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. @@ -121,6 +132,7 @@ As of March 14, 2026, the latest committed domain migrations include: - Dark mode persistence is handled through the frontend theme provider and should remain stable across page navigation. - The shell layout is tuned for wider desktop use than the original foundation build, and now exposes CRM, inventory, settings, and planning modules from the same app shell. +- The active module screens now follow a tighter density baseline for forms, tables, and detail cards. - The client build still emits a Vite chunk-size warning because the app has not been code-split yet. ## PDF Generation diff --git a/ROADMAP.md b/ROADMAP.md index 99003a5..2dcf677 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -25,7 +25,10 @@ MRP Codex is being built as a streamlined, modular manufacturing resource planni - CRM shared file attachments on customer and vendor records, including delete support - CRM reseller hierarchy, parent-child customer structure, and reseller discount support - CRM multi-contact records, commercial terms, lifecycle stages, operational flags, and activity rollups +- Inventory item master, BOM, warehouse, and stock-location foundation +- SKU-searchable BOM component selection for inventory-scale datasets - Theme persistence fixes and denser responsive workspace layouts +- Full-site density normalization pass across active CRM, inventory, settings, dashboard, and login screens - SVAR Gantt integration wrapper with demo planning data - Multi-stage Docker packaging and migration-aware entrypoint - Docker image validated locally with successful app startup and login flow @@ -36,6 +39,7 @@ MRP Codex is being built as a streamlined, modular manufacturing resource planni - Prisma migration execution is committed and documented, but local Windows Node 24 schema-engine behavior remains inconsistent; use Node 22 or Docker for migration execution - The frontend bundle is functional but should be code-split later, especially around the gantt module - CRM reporting is now functional, but broader account-role depth and downstream document rollups can still evolve later +- Inventory currently covers master data and warehouse structure, but not stock movement, on-hand balances, or transaction history yet ## Planned feature phases @@ -97,8 +101,8 @@ MRP Codex is being built as a streamlined, modular manufacturing resource planni ## Near-term priority order -1. Inventory item and BOM data model -2. Sales order and quote foundation -3. Shipping module tied to sales orders -4. Live manufacturing gantt scheduling -5. Expanded role and audit administration +1. Inventory transactions and on-hand tracking +2. BOM and item attachments for drawings and support docs +3. Sales order and quote foundation +4. Shipping module tied to sales orders +5. Live manufacturing gantt scheduling diff --git a/STRUCTURE.md b/STRUCTURE.md index 76d6370..d1405ea 100644 --- a/STRUCTURE.md +++ b/STRUCTURE.md @@ -16,6 +16,7 @@ - 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. +- Preserve the current dense operations UI style on active module pages: compact controls, tighter card padding, and shorter empty states unless a screen has a clear reason to be more spacious. ## Backend rules diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 160591e..d8b90f4 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -26,9 +26,11 @@ import type { InventoryItemOptionDto, InventoryItemStatus, InventoryItemSummaryDto, + InventoryTransactionInput, InventoryItemType, WarehouseDetailDto, WarehouseInput, + WarehouseLocationOptionDto, WarehouseSummaryDto, } from "@mrp/shared/dist/inventory/types.js"; @@ -298,6 +300,9 @@ export const api = { getInventoryItemOptions(token: string) { return request("/api/v1/inventory/items/options", undefined, token); }, + getWarehouseLocationOptions(token: string) { + return request("/api/v1/inventory/locations/options", undefined, token); + }, createInventoryItem(token: string, payload: InventoryItemInput) { return request( "/api/v1/inventory/items", @@ -318,6 +323,16 @@ export const api = { token ); }, + createInventoryTransaction(token: string, itemId: string, payload: InventoryTransactionInput) { + return request( + `/api/v1/inventory/items/${itemId}/transactions`, + { + method: "POST", + body: JSON.stringify(payload), + }, + token + ); + }, getWarehouses(token: string) { return request("/api/v1/inventory/warehouses", undefined, token); }, diff --git a/client/src/modules/inventory/InventoryDetailPage.tsx b/client/src/modules/inventory/InventoryDetailPage.tsx index 2391305..fcab444 100644 --- a/client/src/modules/inventory/InventoryDetailPage.tsx +++ b/client/src/modules/inventory/InventoryDetailPage.tsx @@ -1,17 +1,23 @@ -import type { InventoryItemDetailDto } from "@mrp/shared/dist/inventory/types.js"; +import type { InventoryItemDetailDto, InventoryTransactionInput, WarehouseLocationOptionDto } from "@mrp/shared/dist/inventory/types.js"; import { permissions } from "@mrp/shared"; import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { useAuth } from "../../auth/AuthProvider"; import { api, ApiError } from "../../lib/api"; +import { emptyInventoryTransactionInput, inventoryTransactionOptions } from "./config"; import { InventoryStatusBadge } from "./InventoryStatusBadge"; +import { InventoryTransactionTypeBadge } from "./InventoryTransactionTypeBadge"; import { InventoryTypeBadge } from "./InventoryTypeBadge"; export function InventoryDetailPage() { const { token, user } = useAuth(); const { itemId } = useParams(); const [item, setItem] = useState(null); + const [locationOptions, setLocationOptions] = useState([]); + const [transactionForm, setTransactionForm] = useState(emptyInventoryTransactionInput); + const [transactionStatus, setTransactionStatus] = useState("Record receipts, issues, and adjustments against this item."); + const [isSavingTransaction, setIsSavingTransaction] = useState(false); const [status, setStatus] = useState("Loading inventory item..."); const canManage = user?.permissions.includes(permissions.inventoryWrite) ?? false; @@ -31,8 +37,60 @@ export function InventoryDetailPage() { const message = error instanceof ApiError ? error.message : "Unable to load inventory item."; setStatus(message); }); + + api + .getWarehouseLocationOptions(token) + .then((options) => { + setLocationOptions(options); + setTransactionForm((current) => { + if (current.locationId) { + return current; + } + + const firstOption = options[0]; + return firstOption + ? { + ...current, + warehouseId: firstOption.warehouseId, + locationId: firstOption.locationId, + } + : current; + }); + }) + .catch(() => setLocationOptions([])); }, [itemId, token]); + function updateTransactionField(key: Key, value: InventoryTransactionInput[Key]) { + setTransactionForm((current) => ({ ...current, [key]: value })); + } + + async function handleTransactionSubmit(event: React.FormEvent) { + event.preventDefault(); + if (!token || !itemId) { + return; + } + + setIsSavingTransaction(true); + setTransactionStatus("Saving stock transaction..."); + + try { + const nextItem = await api.createInventoryTransaction(token, itemId, transactionForm); + setItem(nextItem); + setTransactionStatus("Stock transaction recorded."); + setTransactionForm((current) => ({ + ...emptyInventoryTransactionInput, + transactionType: current.transactionType, + warehouseId: current.warehouseId, + locationId: current.locationId, + })); + } catch (error: unknown) { + const message = error instanceof ApiError ? error.message : "Unable to save stock transaction."; + setTransactionStatus(message); + } finally { + setIsSavingTransaction(false); + } + } + if (!item) { return
{status}
; } @@ -63,6 +121,24 @@ export function InventoryDetailPage() { +
+
+

On Hand

+
{item.onHandQuantity}
+
+
+

Stock Locations

+
{item.stockBalances.length}
+
+
+

Transactions

+
{item.recentTransactions.length}
+
+
+

BOM Lines

+
{item.bomLines.length}
+
+

Item Definition

@@ -93,6 +169,28 @@ export function InventoryDetailPage() {
Created {new Date(item.createdAt).toLocaleDateString()}
+
+

Current stock by location

+ {item.stockBalances.length === 0 ? ( +

No stock has been posted for this item yet.

+ ) : ( +
+ {item.stockBalances.map((balance) => ( +
+
+
+ {balance.warehouseCode} / {balance.locationCode} +
+
+ {balance.warehouseName} · {balance.locationName} +
+
+
{balance.quantityOnHand}
+
+ ))} +
+ )} +
@@ -132,6 +230,131 @@ export function InventoryDetailPage() { )}
+
+ {canManage ? ( +
+

Stock Transactions

+

Record movement

+

Post receipts, issues, and adjustments to update on-hand inventory.

+
+
+ + +
+ + +