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>
155 lines
6.1 KiB
TypeScript
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" });
|
|
}
|
|
});
|