Files
Apothecary/server/src/routes/catalog.ts
T
josh 5fa1e34914
Build and push image / build (push) Successful in 57s
Split audits into Weigh Ins (bulk) and Bin Checks (discrete)
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>
2026-06-06 18:28:55 -04:00

155 lines
6.1 KiB
TypeScript

import { Router } from "express";
import { db, nextId } from "../db.js";
export const catalogRouter: Router = Router();
catalogRouter.post("/brands", (req, res) => {
const { name } = req.body as { name: string };
if (!name?.trim()) return res.status(400).json({ error: "name required" });
const existing = db
.prepare<[string], { id: string }>("SELECT id FROM brands WHERE name = ?")
.get(name.trim());
if (existing) return res.json({ id: existing.id, name: name.trim() });
const id = nextId("brd", "brands");
db.prepare("INSERT INTO brands (id, name) VALUES (?, ?)").run(id, name.trim());
res.json({ id, name: name.trim() });
});
catalogRouter.patch("/brands/:id", (req, res) => {
const { id } = req.params;
const { name } = req.body as { name?: string };
if (!name?.trim()) return res.status(400).json({ error: "name required" });
const existing = db
.prepare<[string], { id: string }>("SELECT id FROM brands WHERE id = ?")
.get(id);
if (!existing) return res.status(404).json({ error: "brand not found" });
try {
db.prepare("UPDATE brands SET name = ? WHERE id = ?").run(name.trim(), id);
res.json({ id, name: name.trim() });
} catch (err) {
const msg = err instanceof Error ? err.message : "";
if (msg.includes("UNIQUE")) {
return res.status(409).json({ error: "another brand already uses that name" });
}
throw err;
}
});
// Deleting a brand unparents any products that reference it
// (brand_id → NULL on products), so users never lose inventory.
catalogRouter.delete("/brands/:id", (req, res) => {
const { id } = req.params;
const tx = db.transaction(() => {
db.prepare("UPDATE products SET brand_id = NULL WHERE brand_id = ?").run(id);
const result = db.prepare("DELETE FROM brands WHERE id = ?").run(id);
if (result.changes === 0) throw new Error("not found");
});
try {
tx();
res.json({ ok: true });
} catch {
res.status(404).json({ error: "brand not found" });
}
});
catalogRouter.post("/shops", (req, res) => {
const { name, location } = req.body as { name: string; location?: string };
if (!name?.trim()) return res.status(400).json({ error: "name required" });
const id = nextId("shp", "shops");
db.prepare("INSERT INTO shops (id, name, location) VALUES (?, ?, ?)").run(
id,
name.trim(),
location?.trim() ?? null,
);
res.json({ id, name: name.trim(), location: location?.trim() ?? null });
});
catalogRouter.patch("/shops/:id", (req, res) => {
const { id } = req.params;
const { name, location } = req.body as { name?: string; location?: string | null };
const existing = db
.prepare<[string], { id: string; name: string; location: string | null }>(
"SELECT id, name, location FROM shops WHERE id = ?",
)
.get(id);
if (!existing) return res.status(404).json({ error: "shop not found" });
const nextName = name?.trim() ? name.trim() : existing.name;
const nextLocation =
location === undefined ? existing.location : location?.toString().trim() || null;
db.prepare("UPDATE shops SET name = ?, location = ? WHERE id = ?").run(
nextName,
nextLocation,
id,
);
res.json({ id, name: nextName, location: nextLocation });
});
// Deleting a shop unparents any inventory items that reference it (shop_id → NULL).
catalogRouter.delete("/shops/:id", (req, res) => {
const { id } = req.params;
const tx = db.transaction(() => {
db.prepare("UPDATE inventory_items SET shop_id = NULL WHERE shop_id = ?").run(id);
const result = db.prepare("DELETE FROM shops WHERE id = ?").run(id);
if (result.changes === 0) throw new Error("not found");
});
try {
tx();
res.json({ ok: true });
} catch {
res.status(404).json({ error: "shop not found" });
}
});
catalogRouter.post("/bins", (req, res) => {
const { name, capacity, cadenceDays } = req.body as { name: string; capacity?: number; cadenceDays?: number };
if (!name?.trim()) return res.status(400).json({ error: "name required" });
const id = nextId("bin", "bins");
const cap = Number.isFinite(capacity) && (capacity as number) > 0 ? Math.floor(capacity as number) : 10;
const cad = Number.isFinite(cadenceDays) && (cadenceDays as number) > 0 ? Math.floor(cadenceDays as number) : 30;
db.prepare("INSERT INTO bins (id, name, capacity, cadence_days) VALUES (?, ?, ?, ?)").run(id, name.trim(), cap, cad);
res.json({ id, name: name.trim(), capacity: cap, cadenceDays: cad, lastChecked: null });
});
catalogRouter.patch("/bins/:id", (req, res) => {
const { id } = req.params;
const { name, capacity, cadenceDays } = req.body as { name?: string; capacity?: number; cadenceDays?: number };
const existing = db
.prepare<[string], { id: string; name: string; capacity: number; cadence_days: number; last_checked: string | null }>(
"SELECT id, name, capacity, cadence_days, last_checked FROM bins WHERE id = ?",
)
.get(id);
if (!existing) return res.status(404).json({ error: "bin not found" });
const nextName = name?.trim() ? name.trim() : existing.name;
const nextCapacity =
Number.isFinite(capacity) && (capacity as number) > 0
? Math.floor(capacity as number)
: existing.capacity;
const nextCadence =
Number.isFinite(cadenceDays) && (cadenceDays as number) > 0
? Math.floor(cadenceDays as number)
: existing.cadence_days;
db.prepare("UPDATE bins SET name = ?, capacity = ?, cadence_days = ? WHERE id = ?").run(nextName, nextCapacity, nextCadence, id);
res.json({ id, name: nextName, capacity: nextCapacity, cadenceDays: nextCadence, lastChecked: existing.last_checked });
});
// Deleting a bin unassigns any inventory items that reference it (bin_id → NULL),
// so users never lose inventory when reorganizing storage.
catalogRouter.delete("/bins/:id", (req, res) => {
const { id } = req.params;
const tx = db.transaction(() => {
db.prepare("UPDATE inventory_items SET bin_id = NULL WHERE bin_id = ?").run(id);
const result = db.prepare("DELETE FROM bins WHERE id = ?").run(id);
if (result.changes === 0) throw new Error("not found");
});
try {
tx();
res.json({ ok: true });
} catch {
res.status(404).json({ error: "bin not found" });
}
});