User-supplied asset ids; brand on product; strain is the name
Build and push image / build (push) Successful in 48s
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user