Initial commit: Apothecary v0.4.0

This commit is contained in:
2026-05-03 20:19:26 -04:00
commit 027cf032be
55 changed files with 14678 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
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.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.post("/bins", (req, res) => {
const { name, location, capacity } = req.body as {
name: string;
location?: string;
capacity?: 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;
db.prepare("INSERT INTO bins (id, name, location, capacity) VALUES (?, ?, ?, ?)").run(
id,
name.trim(),
location?.trim() ?? null,
cap,
);
res.json({ id, name: name.trim(), location: location?.trim() ?? null, capacity: cap });
});
catalogRouter.patch("/bins/:id", (req, res) => {
const { id } = req.params;
const { name, location, capacity } = req.body as {
name?: string;
location?: string | null;
capacity?: number;
};
const existing = db
.prepare<[string], { id: string; name: string; location: string | null; capacity: number }>(
"SELECT id, name, location, capacity 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 nextLocation =
location === undefined ? existing.location : location?.toString().trim() || null;
const nextCapacity =
Number.isFinite(capacity) && (capacity as number) > 0
? Math.floor(capacity as number)
: existing.capacity;
db.prepare("UPDATE bins SET name = ?, location = ?, capacity = ? WHERE id = ?").run(
nextName,
nextLocation,
nextCapacity,
id,
);
res.json({ id, name: nextName, location: nextLocation, capacity: nextCapacity });
});
// Deleting a bin unassigns any products that reference it (bin_id → NULL),
// so users never lose products when reorganizing storage.
catalogRouter.delete("/bins/:id", (req, res) => {
const { id } = req.params;
const tx = db.transaction(() => {
db.prepare("UPDATE products 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" });
}
});