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:
@@ -14,11 +14,16 @@ export function ScanField({
|
||||
items,
|
||||
products,
|
||||
onMatch,
|
||||
onScanNoMatch,
|
||||
matchedLabel,
|
||||
}: {
|
||||
items: Item[];
|
||||
products?: Product[];
|
||||
onMatch: (result: ScanResult) => void;
|
||||
// Fired once after a debounce when the scanned text doesn't resolve to
|
||||
// any known asset id or SKU. The parent can use the raw value (e.g. to
|
||||
// open a "create new product" form prefilled with the scanned SKU).
|
||||
onScanNoMatch?: (raw: string) => void;
|
||||
matchedLabel: string | null;
|
||||
}) {
|
||||
const [scan, setScan] = useState("");
|
||||
@@ -32,11 +37,10 @@ export function ScanField({
|
||||
}
|
||||
const hit = lookup(trimmed, items, products);
|
||||
if (hit) {
|
||||
const label = hit.kind === "item" ? hit.item.name : hit.product.sku;
|
||||
onMatch(hit);
|
||||
setScan("");
|
||||
const name =
|
||||
hit.kind === "item" ? hit.item.name : hit.product.name;
|
||||
setFeedback({ type: "matched", text: `Matched ${name}` });
|
||||
setFeedback({ type: "matched", text: `Matched ${label}` });
|
||||
}
|
||||
}, [scan]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -45,9 +49,15 @@ export function ScanField({
|
||||
useEffect(() => {
|
||||
if (!scan.trim() || feedback?.type === "matched") return;
|
||||
const timer = setTimeout(() => {
|
||||
const trimmed = scan.trim().toLowerCase();
|
||||
if (!lookup(trimmed, items, products)) {
|
||||
setFeedback({ type: "miss", text: "No asset id or SKU matches that." });
|
||||
const raw = scan.trim();
|
||||
if (!lookup(raw.toLowerCase(), items, products)) {
|
||||
if (onScanNoMatch) {
|
||||
onScanNoMatch(raw);
|
||||
setScan("");
|
||||
setFeedback(null);
|
||||
} else {
|
||||
setFeedback({ type: "miss", text: "No asset id or SKU matches that." });
|
||||
}
|
||||
}
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
|
||||
Reference in New Issue
Block a user