5fa1e34914
Build and push image / build (push) Successful in 57s
Replaces the unified audit system with two purpose-built flows: - Weigh Ins: rebranded audit flow for bulk products (Flower, Concentrate, Tincture) with scale weigh, container weigh, and estimate modes - Bin Checks: new bin-level presence check — select a bin, scan every item, resolve discrepancies (wrong bin, unknown, missing), auto-records presence audits on verified items Adds cadence_days and last_checked to bins table, with per-bin overdue tracking on the dashboard and bins view. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
229 lines
6.5 KiB
TypeScript
229 lines
6.5 KiB
TypeScript
import type { Bootstrap, AuditMode } from "./types.js";
|
|
|
|
export type BatchOp =
|
|
| { action: "update"; id: string; fields: Partial<{ shopId: string | null; binId: string | null; price: number; thc: number; cbd: number; totalCannabinoids: number; containerWeight: number | null; purchaseDate: string }> }
|
|
| { action: "checkout"; id: string; date: string }
|
|
| { action: "checkin"; id: string; date: string; binId: string }
|
|
| { action: "finish"; id: string; date: string; rating?: number; notes?: string }
|
|
| { action: "gone"; id: string; date: string; reason: string; notes?: string };
|
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(`/api${path}`, {
|
|
...init,
|
|
headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) },
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(`${res.status} ${res.statusText}: ${text}`);
|
|
}
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
export const api = {
|
|
bootstrap: () => request<Bootstrap>("/bootstrap"),
|
|
|
|
// Catalog: products
|
|
createProduct: (body: {
|
|
sku: string;
|
|
type: string;
|
|
kind: "bulk" | "discrete";
|
|
strainId?: string | null;
|
|
strainName?: string;
|
|
brandId?: string | null;
|
|
defaultThc?: number;
|
|
defaultCbd?: number;
|
|
defaultTotalCannabinoids?: number;
|
|
}) => request<{ id: string }>("/products", { method: "POST", body: JSON.stringify(body) }),
|
|
|
|
updateProduct: (
|
|
id: string,
|
|
body: Partial<{
|
|
sku: string;
|
|
type: string;
|
|
kind: "bulk" | "discrete";
|
|
strainId: string | null;
|
|
strainName: string;
|
|
brandId: string | null;
|
|
}>,
|
|
) =>
|
|
request<{ ok: true }>(`/products/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
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: {
|
|
assetId: string;
|
|
productId: string;
|
|
shopId?: string | null;
|
|
binId?: string | null;
|
|
price: number;
|
|
thc?: number;
|
|
cbd?: number;
|
|
totalCannabinoids?: number;
|
|
weight?: number;
|
|
containerWeight?: number | null;
|
|
countOriginal?: number;
|
|
unitWeight?: number;
|
|
purchaseDate: string;
|
|
}) =>
|
|
request<{ id: string; assetId: string }>("/inventory", {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
updateInventoryItem: (
|
|
id: string,
|
|
body: Partial<{
|
|
shopId: string | null;
|
|
binId: string | null;
|
|
price: number;
|
|
thc: number;
|
|
cbd: number;
|
|
totalCannabinoids: number;
|
|
weight: number;
|
|
containerWeight: number | null;
|
|
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),
|
|
}),
|
|
|
|
markInventoryItemGone: (
|
|
id: string,
|
|
body: { date: string; reason: string; notes?: string },
|
|
) =>
|
|
request<{ ok: true }>(`/inventory/${id}/gone`, {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
checkoutInventoryItem: (
|
|
id: string,
|
|
body: { date: string },
|
|
) =>
|
|
request<{ ok: true }>(`/inventory/${id}/checkout`, {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
checkinInventoryItem: (
|
|
id: string,
|
|
body: { date: string; binId: string; remainingWeight?: number },
|
|
) =>
|
|
request<{ ok: true }>(`/inventory/${id}/checkin`, {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
reactivateInventoryItem: (
|
|
id: string,
|
|
body: { binId: string },
|
|
) =>
|
|
request<{ ok: true }>(`/inventory/${id}/reactivate`, {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
auditInventoryItem: (
|
|
id: string,
|
|
body: { date: string; mode: AuditMode; value: number; confirmedBy?: string },
|
|
) =>
|
|
request<{ ok: true }>(`/inventory/${id}/audit`, {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
batchInventory: (ops: BatchOp[]) =>
|
|
request<{ ok: true; count: number }>("/inventory/batch", {
|
|
method: "POST",
|
|
body: JSON.stringify({ ops }),
|
|
}),
|
|
|
|
// Catalog tables (brand/shop/bin) — unchanged
|
|
createBrand: (name: string) =>
|
|
request<{ id: string; name: string }>("/brands", {
|
|
method: "POST",
|
|
body: JSON.stringify({ name }),
|
|
}),
|
|
|
|
updateBrand: (id: string, body: { name: string }) =>
|
|
request<{ id: string; name: string }>(`/brands/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
deleteBrand: (id: string) =>
|
|
request<{ ok: true }>(`/brands/${id}`, { method: "DELETE" }),
|
|
|
|
createShop: (body: { name: string; location?: string }) =>
|
|
request<{ id: string; name: string; location: string | null }>("/shops", {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
updateShop: (id: string, body: { name?: string; location?: string | null }) =>
|
|
request<{ id: string; name: string; location: string | null }>(`/shops/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
deleteShop: (id: string) =>
|
|
request<{ ok: true }>(`/shops/${id}`, { method: "DELETE" }),
|
|
|
|
createBin: (body: { name: string; capacity?: number; cadenceDays?: number }) =>
|
|
request<{ id: string; name: string; capacity: number; cadenceDays: number; lastChecked: string | null }>("/bins", {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
updateBin: (id: string, body: { name?: string; capacity?: number; cadenceDays?: number }) =>
|
|
request<{ id: string; name: string; capacity: number; cadenceDays: number; lastChecked: string | null }>(`/bins/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
deleteBin: (id: string) =>
|
|
request<{ ok: true }>(`/bins/${id}`, { method: "DELETE" }),
|
|
|
|
completeBinCheck: (
|
|
binId: string,
|
|
body: { date: string; verifiedItemIds: string[]; goneItemIds: string[] },
|
|
) =>
|
|
request<{ ok: true; verified: number; gone: number }>(`/bins/${binId}/check`, {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
};
|