Track inventory at the instance level, not by product
Build and push image / build (push) Successful in 46s
Build and push image / build (push) Successful in 46s
The products table conflated catalog ("kind of thing you scan") with
instance ("this jar I bought") — splitting it lets us record every
purchase as its own asset and autofill brand/shop/price/THC from the
last instance when scanning a known SKU.
- products: sku + strain + name + type + kind (catalog only)
- inventory_items: physical jars with short-UUID asset ids, per-batch
brand/shop/bin/price/cannabinoids/weight, audits, lifecycle
- audits now key on inventory_id; strains lose brand_id and type
- migration: rename existing products/audits/strains to *_legacy on
first boot so users keep historical reference, fresh start otherwise
- two-step add flow: scan SKU → select/create product → instance
details (autofilled from last instance) → generated asset id shown
- ScanField matches asset id first, falls back to SKU
- inventory list defaults flat, "By product" toggle groups instances
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+82
-31
@@ -15,41 +15,26 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
export const api = {
|
||||
bootstrap: () => request<Bootstrap>("/bootstrap"),
|
||||
|
||||
// Catalog: products
|
||||
createProduct: (body: {
|
||||
sku: string;
|
||||
name: string;
|
||||
brandId: string;
|
||||
shopId: string;
|
||||
binId: string;
|
||||
type: string;
|
||||
kind: "bulk" | "discrete";
|
||||
weight?: number;
|
||||
countOriginal?: number;
|
||||
unitWeight?: number;
|
||||
price: number;
|
||||
thc: number;
|
||||
cbd: number;
|
||||
totalCannabinoids: number;
|
||||
purchaseDate: string;
|
||||
sku?: string;
|
||||
assetTag?: string;
|
||||
strainId?: string | null;
|
||||
strainName?: string;
|
||||
defaultThc?: number;
|
||||
defaultCbd?: number;
|
||||
defaultTotalCannabinoids?: number;
|
||||
}) => request<{ id: string }>("/products", { method: "POST", body: JSON.stringify(body) }),
|
||||
|
||||
updateProduct: (
|
||||
id: string,
|
||||
body: Partial<{
|
||||
name: string;
|
||||
brandId: string | null;
|
||||
shopId: string | null;
|
||||
binId: string | null;
|
||||
assetTag: string | null;
|
||||
weight: number;
|
||||
countOriginal: number;
|
||||
unitWeight: number;
|
||||
price: number;
|
||||
thc: number;
|
||||
cbd: number;
|
||||
totalCannabinoids: number;
|
||||
purchaseDate: string;
|
||||
type: string;
|
||||
kind: "bulk" | "discrete";
|
||||
strainId: string | null;
|
||||
}>,
|
||||
) =>
|
||||
request<{ ok: true }>(`/products/${id}`, {
|
||||
@@ -57,27 +42,93 @@ export const api = {
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
finishProduct: (id: string, body: { date: string; rating?: number; notes?: string }) =>
|
||||
request<{ ok: true }>(`/products/${id}/finish`, {
|
||||
deleteProduct: (id: string) =>
|
||||
request<{ ok: true }>(`/products/${id}`, { method: "DELETE" }),
|
||||
|
||||
updateStrain: (
|
||||
id: string,
|
||||
body: Partial<{
|
||||
name: string;
|
||||
defaultThc: number | null;
|
||||
defaultCbd: number | null;
|
||||
defaultTotalCannabinoids: number | null;
|
||||
notes: string | null;
|
||||
}>,
|
||||
) =>
|
||||
request<{ ok: true }>(`/strains/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
// Inventory items (instances)
|
||||
createInventoryItem: (body: {
|
||||
productId: string;
|
||||
brandId?: string | null;
|
||||
shopId?: string | null;
|
||||
binId?: string | null;
|
||||
price: number;
|
||||
thc?: number;
|
||||
cbd?: number;
|
||||
totalCannabinoids?: number;
|
||||
weight?: number;
|
||||
countOriginal?: number;
|
||||
unitWeight?: number;
|
||||
purchaseDate: string;
|
||||
}) =>
|
||||
request<{ id: string; assetId: string }>("/inventory", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
markGone: (id: string, body: { date: string; reason: string; notes?: string }) =>
|
||||
request<{ ok: true }>(`/products/${id}/gone`, {
|
||||
updateInventoryItem: (
|
||||
id: string,
|
||||
body: Partial<{
|
||||
brandId: string | null;
|
||||
shopId: string | null;
|
||||
binId: string | null;
|
||||
price: number;
|
||||
thc: number;
|
||||
cbd: number;
|
||||
totalCannabinoids: number;
|
||||
weight: number;
|
||||
countOriginal: number;
|
||||
unitWeight: number;
|
||||
purchaseDate: string;
|
||||
}>,
|
||||
) =>
|
||||
request<{ ok: true }>(`/inventory/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
finishInventoryItem: (
|
||||
id: string,
|
||||
body: { date: string; rating?: number; notes?: string },
|
||||
) =>
|
||||
request<{ ok: true }>(`/inventory/${id}/finish`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
auditProduct: (
|
||||
markInventoryItemGone: (
|
||||
id: string,
|
||||
body: { date: string; reason: string; notes?: string },
|
||||
) =>
|
||||
request<{ ok: true }>(`/inventory/${id}/gone`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
auditInventoryItem: (
|
||||
id: string,
|
||||
body: { date: string; mode: AuditMode; value: number; confirmedBy?: string },
|
||||
) =>
|
||||
request<{ ok: true }>(`/products/${id}/audit`, {
|
||||
request<{ ok: true }>(`/inventory/${id}/audit`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
// Catalog tables (brand/shop/bin) — unchanged
|
||||
createBrand: (name: string) =>
|
||||
request<{ id: string; name: string }>("/brands", {
|
||||
method: "POST",
|
||||
|
||||
Reference in New Issue
Block a user