Group bins by letter, sort by number, drop location
Build and push image / build (push) Successful in 46s

Bins follow an A1/A2/B1 naming convention, so the Bins page now parses
the leading letter prefix as a row group and the trailing number as the
within-row order. Each letter starts a fresh grid section; bins whose
names don't match the pattern fall into a trailing "Other" bucket
sorted alphabetically.

Removes the optional location field from bins end to end: the API
client signatures, server POST/PATCH routes, both product-flow inline
creates, the dropdown labels, the ProductDetail bin row, and the
BinsView header line. The bootstrap query explicitly projects only
id/name/capacity so the dead column doesn't leak through.

The location column stays in the bins table on disk to avoid a
migration on existing deployments — it just isn't read or written.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 22:07:12 -04:00
parent cd7aeb9d09
commit d335525073
9 changed files with 245 additions and 272 deletions
+1 -1
View File
@@ -59,7 +59,7 @@ bootstrapRouter.get("/bootstrap", (_req, res) => {
.all();
const shops = db.prepare("SELECT * FROM shops ORDER BY id").all();
const brands = db.prepare("SELECT * FROM brands ORDER BY id").all();
const bins = db.prepare("SELECT * FROM bins ORDER BY id").all();
const bins = db.prepare("SELECT id, name, capacity FROM bins ORDER BY id").all();
const strains = db
.prepare<[], StrainRow>("SELECT * FROM strains ORDER BY name COLLATE NOCASE")
.all();
+8 -28
View File
@@ -104,52 +104,32 @@ catalogRouter.delete("/shops/:id", (req, res) => {
});
catalogRouter.post("/bins", (req, res) => {
const { name, location, capacity } = req.body as {
name: string;
location?: string;
capacity?: number;
};
const { name, capacity } = req.body as { name: 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 });
db.prepare("INSERT INTO bins (id, name, capacity) VALUES (?, ?, ?)").run(id, name.trim(), cap);
res.json({ id, name: name.trim(), 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 { name, capacity } = req.body as { name?: string; 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 = ?",
.prepare<[string], { id: string; name: string; capacity: number }>(
"SELECT id, name, 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 });
db.prepare("UPDATE bins SET name = ?, capacity = ? WHERE id = ?").run(nextName, nextCapacity, id);
res.json({ id, name: nextName, capacity: nextCapacity });
});
// Deleting a bin unassigns any products that reference it (bin_id → NULL),