User-supplied asset ids; brand on product; strain is the name
Build and push image / build (push) Successful in 48s

Four UX changes after using the rework for a bit:

1. Asset ids are 6-digit numbers from a roll of physical labels — server
   no longer generates them. POST /api/inventory requires assetId; the
   add-inventory form has a digits-only input that auto-focuses on entry.
2. Strain and product name are the same thing. Drop products.name; the
   strain's name supplies the display. Product creation just asks for
   "Name (strain)" and matches/creates a strain by that name.
3. Brand moves from inventory_items to products. SKUs are brand-specific,
   so all instances of a product share the brand. Brand selector lives
   on the product create/edit form, not the per-instance form.
4. Scanning an unknown SKU on the add-inventory step now opens the
   create-product subform with the SKU prefilled — one less click.

Migration: detect prior shape (products.name column present) and rename
products/inventory_items/audits to *_v1 archives, recreate empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 18:17:12 -04:00
parent 02dc6e523f
commit 80034b47c5
15 changed files with 380 additions and 256 deletions
+17 -13
View File
@@ -1,11 +1,11 @@
import { Router } from "express";
import { db, generateAssetId, nextId } from "../db.js";
import { ASSET_ID_RE, db, nextId } from "../db.js";
export const inventoryRouter: Router = Router();
type CreateBody = {
assetId: string;
productId: string;
brandId?: string | null;
shopId?: string | null;
binId?: string | null;
price: number;
@@ -25,6 +25,18 @@ inventoryRouter.post("/inventory", (req, res) => {
if (!Number.isFinite(body.price) || body.price < 0) {
return res.status(400).json({ error: "price required" });
}
const assetId = (body.assetId ?? "").trim();
if (!ASSET_ID_RE.test(assetId)) {
return res.status(400).json({ error: "assetId must be exactly 6 digits" });
}
const taken = db
.prepare<[string], { id: string }>(
`SELECT id FROM inventory_items WHERE asset_id = ?`,
)
.get(assetId);
if (taken) {
return res.status(409).json({ error: "asset id already in use", id: taken.id });
}
const product = db
.prepare<[string], { id: string; kind: string }>(
@@ -35,19 +47,18 @@ inventoryRouter.post("/inventory", (req, res) => {
const isDiscrete = product.kind === "discrete";
const id = nextId("inv", "inventory_items");
const assetId = generateAssetId();
db.prepare(
`INSERT INTO inventory_items (
id, asset_id, product_id,
brand_id, shop_id, bin_id,
shop_id, bin_id,
price, thc, cbd, total_cannabinoids,
weight, last_audit_weight,
count_original, count_last_audit, unit_weight,
purchase_date, status
) VALUES (
@id, @assetId, @productId,
@brandId, @shopId, @binId,
@shopId, @binId,
@price, @thc, @cbd, @totalCannabinoids,
@weight, @lastAuditWeight,
@countOriginal, @countLastAudit, @unitWeight,
@@ -57,7 +68,6 @@ inventoryRouter.post("/inventory", (req, res) => {
id,
assetId,
productId: body.productId,
brandId: body.brandId ?? null,
shopId: body.shopId ?? null,
binId: body.binId ?? null,
price: body.price,
@@ -76,7 +86,6 @@ inventoryRouter.post("/inventory", (req, res) => {
});
type UpdateBody = Partial<{
brandId: string | null;
shopId: string | null;
binId: string | null;
price: number;
@@ -95,7 +104,6 @@ inventoryRouter.patch("/inventory/:id", (req, res) => {
type Row = {
id: string;
brand_id: string | null;
shop_id: string | null;
bin_id: string | null;
product_id: string;
@@ -113,7 +121,7 @@ inventoryRouter.patch("/inventory/:id", (req, res) => {
const existing = db
.prepare<[string], Row>(
`SELECT id, brand_id, shop_id, bin_id, product_id, price, thc, cbd,
`SELECT id, shop_id, bin_id, product_id, price, thc, cbd,
total_cannabinoids, weight, last_audit_weight, count_original,
count_last_audit, unit_weight, purchase_date
FROM inventory_items WHERE id = ?`,
@@ -132,8 +140,6 @@ inventoryRouter.patch("/inventory/:id", (req, res) => {
)
.get(id)!.n;
const nextBrandId =
body.brandId === undefined ? existing.brand_id : body.brandId || null;
const nextShopId =
body.shopId === undefined ? existing.shop_id : body.shopId || null;
const nextBinId =
@@ -174,7 +180,6 @@ inventoryRouter.patch("/inventory/:id", (req, res) => {
db.prepare(
`UPDATE inventory_items SET
brand_id = @brandId,
shop_id = @shopId,
bin_id = @binId,
price = @price,
@@ -190,7 +195,6 @@ inventoryRouter.patch("/inventory/:id", (req, res) => {
WHERE id = @id`,
).run({
id,
brandId: nextBrandId,
shopId: nextShopId,
binId: nextBinId,
price: nextPrice,