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:
@@ -5,11 +5,14 @@ export const productsRouter: Router = Router();
|
||||
|
||||
type CreateBody = {
|
||||
sku: string;
|
||||
name: string;
|
||||
type: string;
|
||||
kind: "bulk" | "discrete";
|
||||
// strain identity: either link to an existing strain via id, or supply a
|
||||
// name and the server creates / matches one. Strain.name doubles as the
|
||||
// product's display name.
|
||||
strainId?: string | null;
|
||||
strainName?: string;
|
||||
brandId?: string | null;
|
||||
type: string;
|
||||
kind: "bulk" | "discrete";
|
||||
defaultThc?: number;
|
||||
defaultCbd?: number;
|
||||
defaultTotalCannabinoids?: number;
|
||||
@@ -18,14 +21,15 @@ type CreateBody = {
|
||||
productsRouter.post("/products", (req, res) => {
|
||||
const body = req.body as CreateBody;
|
||||
if (!body.sku?.trim()) return res.status(400).json({ error: "sku required" });
|
||||
if (!body.name?.trim()) return res.status(400).json({ error: "name required" });
|
||||
if (!body.type) return res.status(400).json({ error: "type required" });
|
||||
if (body.kind !== "bulk" && body.kind !== "discrete") {
|
||||
return res.status(400).json({ error: "kind must be bulk or discrete" });
|
||||
}
|
||||
if (!body.strainId && !body.strainName?.trim()) {
|
||||
return res.status(400).json({ error: "strainId or strainName required" });
|
||||
}
|
||||
|
||||
const sku = body.sku.trim();
|
||||
const name = body.name.trim();
|
||||
const todayIso = new Date().toISOString().slice(0, 10);
|
||||
|
||||
const existingSku = db
|
||||
@@ -40,9 +44,9 @@ productsRouter.post("/products", (req, res) => {
|
||||
try {
|
||||
const tx = db.transaction(() => {
|
||||
let strainId = body.strainId ?? null;
|
||||
const strainName = body.strainName?.trim() || name;
|
||||
|
||||
if (!strainId && strainName) {
|
||||
if (!strainId) {
|
||||
const strainName = body.strainName!.trim();
|
||||
const found = db
|
||||
.prepare<[string], { id: string }>(
|
||||
`SELECT id FROM strains WHERE name = ? COLLATE NOCASE`,
|
||||
@@ -70,9 +74,9 @@ productsRouter.post("/products", (req, res) => {
|
||||
}
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO products (id, sku, strain_id, name, type, kind, created_at)
|
||||
`INSERT INTO products (id, sku, strain_id, brand_id, type, kind, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(id, sku, strainId, name, body.type, body.kind, todayIso);
|
||||
).run(id, sku, strainId, body.brandId ?? null, body.type, body.kind, todayIso);
|
||||
});
|
||||
tx();
|
||||
} catch (err) {
|
||||
@@ -87,10 +91,11 @@ productsRouter.post("/products", (req, res) => {
|
||||
});
|
||||
|
||||
type UpdateBody = Partial<{
|
||||
name: string;
|
||||
type: string;
|
||||
kind: "bulk" | "discrete";
|
||||
strainId: string | null;
|
||||
strainName: string;
|
||||
brandId: string | null;
|
||||
}>;
|
||||
|
||||
productsRouter.patch("/products/:id", (req, res) => {
|
||||
@@ -100,24 +105,47 @@ productsRouter.patch("/products/:id", (req, res) => {
|
||||
const existing = db
|
||||
.prepare<
|
||||
[string],
|
||||
{ id: string; name: string; type: string; kind: string; strain_id: string | null }
|
||||
{ id: string; type: string; kind: string; strain_id: string; brand_id: string | null }
|
||||
>(
|
||||
`SELECT id, name, type, kind, strain_id FROM products WHERE id = ?`,
|
||||
`SELECT id, type, kind, strain_id, brand_id FROM products WHERE id = ?`,
|
||||
)
|
||||
.get(id);
|
||||
if (!existing) return res.status(404).json({ error: "product not found" });
|
||||
|
||||
const nextName =
|
||||
typeof body.name === "string" && body.name.trim() ? body.name.trim() : existing.name;
|
||||
const nextType = typeof body.type === "string" && body.type ? body.type : existing.type;
|
||||
const nextKind: "bulk" | "discrete" =
|
||||
body.kind === "bulk" || body.kind === "discrete" ? body.kind : (existing.kind as "bulk" | "discrete");
|
||||
const nextStrainId =
|
||||
body.strainId === undefined ? existing.strain_id : body.strainId;
|
||||
body.kind === "bulk" || body.kind === "discrete"
|
||||
? body.kind
|
||||
: (existing.kind as "bulk" | "discrete");
|
||||
const nextBrandId =
|
||||
body.brandId === undefined ? existing.brand_id : body.brandId;
|
||||
|
||||
// Strain handling: prefer explicit strainId; else if strainName given,
|
||||
// match-by-name (case-insensitive) or create a new strain.
|
||||
let nextStrainId = existing.strain_id;
|
||||
const todayIso = new Date().toISOString().slice(0, 10);
|
||||
if (body.strainId !== undefined && body.strainId) {
|
||||
nextStrainId = body.strainId;
|
||||
} else if (typeof body.strainName === "string" && body.strainName.trim()) {
|
||||
const name = body.strainName.trim();
|
||||
const found = db
|
||||
.prepare<[string], { id: string }>(
|
||||
`SELECT id FROM strains WHERE name = ? COLLATE NOCASE`,
|
||||
)
|
||||
.get(name);
|
||||
if (found) {
|
||||
nextStrainId = found.id;
|
||||
} else {
|
||||
nextStrainId = nextId("str", "strains");
|
||||
db.prepare(
|
||||
`INSERT INTO strains (id, name, created_at) VALUES (?, ?, ?)`,
|
||||
).run(nextStrainId, name, todayIso);
|
||||
}
|
||||
}
|
||||
|
||||
db.prepare(
|
||||
`UPDATE products SET name = ?, type = ?, kind = ?, strain_id = ? WHERE id = ?`,
|
||||
).run(nextName, nextType, nextKind, nextStrainId, id);
|
||||
`UPDATE products SET type = ?, kind = ?, strain_id = ?, brand_id = ? WHERE id = ?`,
|
||||
).run(nextType, nextKind, nextStrainId, nextBrandId, id);
|
||||
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user