8ef8859c7d
Build and push image / build (push) Successful in 49s
Adds PATCH and DELETE endpoints for brands and shops that mirror the existing bins pattern: deleting a brand or shop nullifies referencing products (and strains, for brands) inside a transaction so nothing is lost. Brand renames return 409 when the new name collides with the UNIQUE constraint, surfaced inline in the edit modal. The Brands and Shops views now show inline edit/trash icons on each card; the trash button confirms with a preview of how many products will be unparented. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
171 lines
6.0 KiB
TypeScript
171 lines
6.0 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 and strains that reference it
|
|
// (brand_id → NULL), so users never lose products when reorganizing.
|
|
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);
|
|
db.prepare("UPDATE strains 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 products that reference it (shop_id → NULL).
|
|
catalogRouter.delete("/shops/:id", (req, res) => {
|
|
const { id } = req.params;
|
|
const tx = db.transaction(() => {
|
|
db.prepare("UPDATE products 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, 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" });
|
|
}
|
|
});
|