Edit and delete brands and shops
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>
This commit is contained in:
2026-05-03 21:33:42 -04:00
parent d00eb4c12b
commit 8ef8859c7d
6 changed files with 365 additions and 9 deletions
+58 -4
View File
@@ -1,13 +1,33 @@
import type { Bootstrap } from "../types.js";
import { Btn, Card, Pill } from "../components/primitives/index.js";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { Bootstrap, Brand } from "../types.js";
import { api } from "../api.js";
import { Btn, Card, Pill, Icon } from "../components/primitives/index.js";
export function BrandsView({
data,
onAddBrand,
onEditBrand,
}: {
data: Bootstrap;
onAddBrand: () => void;
onEditBrand: (brand: Brand) => void;
}) {
const qc = useQueryClient();
const remove = useMutation({
mutationFn: (id: string) => api.deleteBrand(id),
onSuccess: () => qc.invalidateQueries({ queryKey: ["bootstrap"] }),
});
const handleDelete = (brandId: string, brandName: string, productCount: number, strainCount: number) => {
const parts: string[] = [];
if (productCount > 0) parts.push(`${productCount} product${productCount === 1 ? "" : "s"}`);
if (strainCount > 0) parts.push(`${strainCount} strain${strainCount === 1 ? "" : "s"}`);
const tail = parts.length > 0
? ` ${parts.join(" and ")} will be unbranded.`
: "";
if (window.confirm(`Delete "${brandName}"?${tail}`)) remove.mutate(brandId);
};
return (
<div
style={{
@@ -51,7 +71,8 @@ export function BrandsView({
}}
>
{data.brands.map((b) => {
const count = data.products.filter((p) => p.brandId === b.id).length;
const productCount = data.products.filter((p) => p.brandId === b.id).length;
const strainCount = data.strains.filter((s) => s.brandId === b.id).length;
return (
<Card key={b.id} style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ flex: 1, minWidth: 0 }}>
@@ -60,8 +81,41 @@ export function BrandsView({
</div>
</div>
<Pill tone="outline">
{count} purchase{count === 1 ? "" : "s"}
{productCount} purchase{productCount === 1 ? "" : "s"}
</Pill>
<button
onClick={() => onEditBrand(b)}
title="Edit brand"
aria-label={`Edit brand ${b.name}`}
style={{
background: "transparent",
border: "none",
padding: 4,
borderRadius: "var(--r-sm)",
cursor: "pointer",
color: "var(--ink-3)",
display: "inline-flex",
}}
>
<Icon name="edit" size={14} />
</button>
<button
onClick={() => handleDelete(b.id, b.name, productCount, strainCount)}
title="Remove brand"
aria-label={`Remove brand ${b.name}`}
disabled={remove.isPending}
style={{
background: "transparent",
border: "none",
padding: 4,
borderRadius: "var(--r-sm)",
cursor: remove.isPending ? "wait" : "pointer",
color: "var(--ink-3)",
display: "inline-flex",
}}
>
<Icon name="bin" size={14} />
</button>
</Card>
);
})}
+52 -2
View File
@@ -1,13 +1,30 @@
import type { Bootstrap } from "../types.js";
import { Btn, Card, Pill } from "../components/primitives/index.js";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { Bootstrap, Shop } from "../types.js";
import { api } from "../api.js";
import { Btn, Card, Pill, Icon } from "../components/primitives/index.js";
export function ShopsView({
data,
onAddShop,
onEditShop,
}: {
data: Bootstrap;
onAddShop: () => void;
onEditShop: (shop: Shop) => void;
}) {
const qc = useQueryClient();
const remove = useMutation({
mutationFn: (id: string) => api.deleteShop(id),
onSuccess: () => qc.invalidateQueries({ queryKey: ["bootstrap"] }),
});
const handleDelete = (shopId: string, shopName: string, productCount: number) => {
const tail = productCount > 0
? ` ${productCount} product${productCount === 1 ? "" : "s"} will lose this shop.`
: "";
if (window.confirm(`Delete "${shopName}"?${tail}`)) remove.mutate(shopId);
};
return (
<div
style={{
@@ -67,6 +84,39 @@ export function ShopsView({
<Pill tone="outline">
{count} purchase{count === 1 ? "" : "s"}
</Pill>
<button
onClick={() => onEditShop(s)}
title="Edit shop"
aria-label={`Edit shop ${s.name}`}
style={{
background: "transparent",
border: "none",
padding: 4,
borderRadius: "var(--r-sm)",
cursor: "pointer",
color: "var(--ink-3)",
display: "inline-flex",
}}
>
<Icon name="edit" size={14} />
</button>
<button
onClick={() => handleDelete(s.id, s.name, count)}
title="Remove shop"
aria-label={`Remove shop ${s.name}`}
disabled={remove.isPending}
style={{
background: "transparent",
border: "none",
padding: 4,
borderRadius: "var(--r-sm)",
cursor: remove.isPending ? "wait" : "pointer",
color: "var(--ink-3)",
display: "inline-flex",
}}
>
<Icon name="bin" size={14} />
</button>
</Card>
);
})}