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:
+58
-8
@@ -11,18 +11,48 @@ export const db = new Database(DB_PATH);
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.pragma("foreign_keys = ON");
|
||||
|
||||
archiveLegacyIfPresent();
|
||||
|
||||
const schema = readFileSync(join(__dirname, "schema.sql"), "utf8");
|
||||
db.exec(schema);
|
||||
|
||||
// Add strain_id to products table if missing — older DBs predate the column.
|
||||
const productCols = db
|
||||
.prepare(`PRAGMA table_info(products)`)
|
||||
.all() as { name: string }[];
|
||||
if (!productCols.some((c) => c.name === "strain_id")) {
|
||||
db.exec(`ALTER TABLE products ADD COLUMN strain_id TEXT REFERENCES strains(id);`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_products_strain ON products(strain_id);`);
|
||||
// One-shot migration: the old schema put per-instance fields (weight, bin_id,
|
||||
// etc.) directly on `products`. The new schema splits products (catalog) from
|
||||
// inventory_items (instance). When we detect the old shape, rename the live
|
||||
// tables out of the way so the new CREATE TABLE IF NOT EXISTS statements can
|
||||
// build the empty new ones. Legacy tables stay queryable for reference.
|
||||
function archiveLegacyIfPresent(): void {
|
||||
const productCols = db
|
||||
.prepare(`PRAGMA table_info(products)`)
|
||||
.all() as { name: string }[];
|
||||
const looksLikeOldSchema =
|
||||
productCols.length > 0 && productCols.some((c) => c.name === "weight");
|
||||
if (!looksLikeOldSchema) return;
|
||||
|
||||
const legacyExists = db
|
||||
.prepare(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name='products_legacy'`,
|
||||
)
|
||||
.get();
|
||||
if (legacyExists) return;
|
||||
|
||||
db.exec(`
|
||||
ALTER TABLE products RENAME TO products_legacy;
|
||||
ALTER TABLE audits RENAME TO audits_legacy;
|
||||
ALTER TABLE strains RENAME TO strains_legacy;
|
||||
`);
|
||||
}
|
||||
|
||||
const PAD: Record<string, number> = {
|
||||
prd: 4,
|
||||
pdt: 4,
|
||||
inv: 4,
|
||||
str: 4,
|
||||
brd: 2,
|
||||
shp: 2,
|
||||
bin: 2,
|
||||
};
|
||||
|
||||
export function nextId(prefix: string, table: string): string {
|
||||
const row = db
|
||||
.prepare<[string], { id: string }>(
|
||||
@@ -30,11 +60,31 @@ export function nextId(prefix: string, table: string): string {
|
||||
)
|
||||
.get(`${prefix}-%`);
|
||||
const last = row ? Number(row.id.slice(prefix.length + 1)) : 0;
|
||||
const pad = prefix === "prd" || prefix === "str" ? 4 : 2;
|
||||
const pad = PAD[prefix] ?? 2;
|
||||
const n = String(last + 1).padStart(pad, "0");
|
||||
return `${prefix}-${n}`;
|
||||
}
|
||||
|
||||
// Crockford base32 alphabet — drops I, L, O, U so labels are unambiguous when
|
||||
// read off a sticker. 32^6 ≈ 1B addresses, so collisions on any reasonable
|
||||
// inventory are vanishingly rare; we still loop just to be safe.
|
||||
const ASSET_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
||||
export function generateAssetId(): string {
|
||||
for (let attempt = 0; attempt < 16; attempt++) {
|
||||
let id = "";
|
||||
for (let i = 0; i < 6; i++) {
|
||||
id += ASSET_ALPHABET[Math.floor(Math.random() * ASSET_ALPHABET.length)];
|
||||
}
|
||||
const taken = db
|
||||
.prepare<[string], { id: string }>(
|
||||
`SELECT id FROM inventory_items WHERE asset_id = ?`,
|
||||
)
|
||||
.get(id);
|
||||
if (!taken) return id;
|
||||
}
|
||||
throw new Error("could not generate a unique asset id after 16 attempts");
|
||||
}
|
||||
|
||||
export function randomSku(): string {
|
||||
return "SKU-" + Math.random().toString(36).slice(2, 8).toUpperCase();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user