From e1b1a82e07bc16571d3586b5c3802fedbb2c3605 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 21 Mar 2026 07:59:58 -0500 Subject: [PATCH] Add multi-vendor capability with admin vendor management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add resolveVendorId() helper — admin can pass ?vendorId= to scope catalog operations to any vendor; other roles locked to JWT vendorId - Thread ?vendorId= through products, categories, taxes, events routes - Add DELETE /vendors/:id (admin only) with cascade-safe guard: blocks if vendor has users or transactions; otherwise cascade-deletes EventProduct → EventTax → Event → Product → Tax → Category → Vendor - Rewrite VendorPage: admin gets full CRUD list, vendor gets own settings - Add VendorFilter shared component (admin-only dropdown) - Integrate VendorFilter into Catalog, Users, and Events pages so admin can switch vendor context for all create/read operations Co-Authored-By: Claude Sonnet 4.6 --- client/src/components/VendorFilter.tsx | 53 ++++++++ client/src/pages/CatalogPage.tsx | 110 +++++++--------- client/src/pages/EventsPage.tsx | 22 +++- client/src/pages/UsersPage.tsx | 32 +++-- client/src/pages/VendorPage.tsx | 169 ++++++++++++++++++++----- server/src/lib/vendorScope.ts | 16 +++ server/src/routes/categories.ts | 44 +++---- server/src/routes/events.ts | 7 +- server/src/routes/products.ts | 48 +++---- server/src/routes/taxes.ts | 40 +++--- server/src/routes/vendors.ts | 33 +++++ 11 files changed, 379 insertions(+), 195 deletions(-) create mode 100644 client/src/components/VendorFilter.tsx create mode 100644 server/src/lib/vendorScope.ts diff --git a/client/src/components/VendorFilter.tsx b/client/src/components/VendorFilter.tsx new file mode 100644 index 0000000..0d4aa9a --- /dev/null +++ b/client/src/components/VendorFilter.tsx @@ -0,0 +1,53 @@ +import React, { useEffect, useState } from "react"; +import { api } from "../api/client"; +import { useAuth } from "../context/AuthContext"; + +interface Vendor { id: string; name: string; } + +interface Props { + vendorId: string; + onChange: (vendorId: string) => void; +} + +/** + * Dropdown that lets admin users switch vendor context. + * Renders nothing for non-admin roles. + */ +export function VendorFilter({ vendorId, onChange }: Props) { + const { user } = useAuth(); + const [vendors, setVendors] = useState([]); + + useEffect(() => { + if (user?.role !== "admin") return; + api.get<{ data: Vendor[] }>("/vendors?limit=200") + .then((r) => setVendors(r.data)) + .catch(console.error); + }, [user?.role]); + + if (user?.role !== "admin" || vendors.length === 0) return null; + + return ( +
+ Vendor: + +
+ ); +} + +const sel: React.CSSProperties = { + border: "1px solid var(--color-border)", + borderRadius: "var(--radius)", + padding: "5px 8px", + fontSize: 13, + background: "var(--color-surface)", + cursor: "pointer", + minWidth: 140, +}; diff --git a/client/src/pages/CatalogPage.tsx b/client/src/pages/CatalogPage.tsx index bb389cb..b26c856 100644 --- a/client/src/pages/CatalogPage.tsx +++ b/client/src/pages/CatalogPage.tsx @@ -1,9 +1,11 @@ import React, { useEffect, useState, useCallback } from "react"; import { api } from "../api/client"; +import { useAuth } from "../context/AuthContext"; import { Table } from "../components/Table"; import { Modal } from "../components/Modal"; import { PageHeader } from "../components/PageHeader"; import { FormField, inputStyle, Btn } from "../components/FormField"; +import { VendorFilter } from "../components/VendorFilter"; interface Category { id: string; name: string; } interface Tax { id: string; name: string; rate: number; } @@ -16,31 +18,36 @@ interface ApiList { data: T[]; } type Tab = "products" | "categories" | "taxes"; export default function CatalogPage() { + const { user } = useAuth(); const [tab, setTab] = useState("products"); + const [vendorId, setVendorId] = useState(user?.vendorId ?? ""); return (
- +
+ + +
{(["products", "categories", "taxes"] as Tab[]).map((t) => ( - ))}
- {tab === "products" && } - {tab === "categories" && } - {tab === "taxes" && } + {tab === "products" && } + {tab === "categories" && } + {tab === "taxes" && }
); } +function qs(vendorId: string) { + return vendorId ? `?vendorId=${encodeURIComponent(vendorId)}` : ""; +} + // ─── Products ────────────────────────────────────────────────────────────── -function ProductsTab() { +function ProductsTab({ vendorId }: { vendorId: string }) { const [products, setProducts] = useState([]); const [categories, setCategories] = useState([]); const [taxes, setTaxes] = useState([]); @@ -53,16 +60,17 @@ function ProductsTab() { const load = useCallback(async () => { setLoading(true); + const q = qs(vendorId); const [p, c, t] = await Promise.all([ - api.get>("/products"), - api.get>("/categories"), - api.get>("/taxes"), + api.get>(`/products${q}`), + api.get>(`/categories${q}`), + api.get>(`/taxes${q}`), ]); setProducts(p.data); setCategories(c.data); setTaxes(t.data); setLoading(false); - }, []); + }, [vendorId]); useEffect(() => { load(); }, [load]); @@ -70,8 +78,7 @@ function ProductsTab() { const openEdit = (p: Product) => { setSelected(p); setForm({ name: p.name, sku: p.sku ?? "", price: String(p.price), categoryId: p.category?.id ?? "", taxId: p.tax?.id ?? "", description: p.description ?? "" }); - setError(""); - setModal("edit"); + setError(""); setModal("edit"); }; const handleSubmit = async (e: React.FormEvent) => { @@ -80,8 +87,9 @@ function ProductsTab() { setError(""); try { const payload = { ...form, price: parseFloat(form.price), categoryId: form.categoryId || null, taxId: form.taxId || null }; - if (modal === "create") await api.post("/products", payload); - else if (selected) await api.put(`/products/${selected.id}`, payload); + const q = qs(vendorId); + if (modal === "create") await api.post(`/products${q}`, payload); + else if (selected) await api.put(`/products/${selected.id}${q}`, payload); setModal(null); load(); } catch (err) { setError(err instanceof Error ? err.message : "Save failed"); } @@ -110,9 +118,7 @@ function ProductsTab() { return ( <> -
- + Add Product -
+
+ Add Product
{modal && ( setModal(null)}> @@ -122,12 +128,8 @@ function ProductsTab() {
- - - - - - + +