From 31e539102bf1cb43d49430c5f72bef52e41d5546 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 21 Mar 2026 17:48:44 -0500 Subject: [PATCH] Vendor-assign events and scope event catalog to vendor - Add vendorWhereClause() helper: admin + ?vendorId= filters to that vendor; admin with no filter sees all; other roles locked to own - Fix events GET / to use vendorWhereClause so vendor filter works - EventFormModal: admin sees a Vendor picker when creating a new event, pre-populated from the active VendorFilter; POST includes ?vendorId= - EventConfigPanel: scope /taxes and /products fetches to event.vendorId so only the event's vendor's catalog items are selectable Co-Authored-By: Claude Sonnet 4.6 --- client/src/pages/EventsPage.tsx | 43 ++++++++++++++++++++++++++++----- server/src/lib/vendorScope.ts | 19 +++++++++++++++ server/src/routes/events.ts | 4 +-- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/client/src/pages/EventsPage.tsx b/client/src/pages/EventsPage.tsx index 15587c9..4e289c5 100644 --- a/client/src/pages/EventsPage.tsx +++ b/client/src/pages/EventsPage.tsx @@ -5,7 +5,7 @@ import { PageHeader } from "../components/PageHeader"; import { VendorFilter } from "../components/VendorFilter"; import { Table } from "../components/Table"; import { Modal } from "../components/Modal"; -import { FormField, Btn } from "../components/FormField"; +import { FormField, Btn, inputStyle } from "../components/FormField"; // ─── Types ────────────────────────────────────────────────────────────────── @@ -139,6 +139,7 @@ export default function EventsPage() { {showForm && ( setShowForm(false)} onSaved={() => { setShowForm(false); load(); }} /> @@ -159,20 +160,39 @@ export default function EventsPage() { // ─── Event Form Modal ──────────────────────────────────────────────────────── -function EventFormModal({ event, onClose, onSaved }: { +function EventFormModal({ event, defaultVendorId, onClose, onSaved }: { event: Event | null; + defaultVendorId?: string; onClose: () => void; onSaved: () => void; }) { + const { user } = useAuth(); + const isAdmin = user?.role === "admin"; const [name, setName] = useState(event?.name ?? ""); const [description, setDescription] = useState(event?.description ?? ""); const [startsAt, setStartsAt] = useState(event ? toDatetimeLocal(event.startsAt) : ""); const [endsAt, setEndsAt] = useState(event ? toDatetimeLocal(event.endsAt) : ""); const [isActive, setIsActive] = useState(event?.isActive ?? true); + const [selectedVendorId, setSelectedVendorId] = useState( + event?.vendorId ?? defaultVendorId ?? "" + ); + const [vendors, setVendors] = useState([]); const [error, setError] = useState(""); const [saving, setSaving] = useState(false); + useEffect(() => { + if (isAdmin && !event) { + api.get<{ data: Vendor[] }>("/vendors?limit=200") + .then((r) => setVendors(r.data)) + .catch(console.error); + } + }, [isAdmin, event]); + const save = async () => { + if (isAdmin && !event && !selectedVendorId) { + setError("Please select a vendor for this event."); + return; + } setSaving(true); setError(""); try { @@ -185,7 +205,8 @@ function EventFormModal({ event, onClose, onSaved }: { if (event) { await api.put(`/events/${event.id}`, body); } else { - await api.post("/events", body); + const q = isAdmin && selectedVendorId ? `?vendorId=${encodeURIComponent(selectedVendorId)}` : ""; + await api.post(`/events${q}`, body); } onSaved(); } catch (err) { @@ -197,6 +218,15 @@ function EventFormModal({ event, onClose, onSaved }: { return ( + {isAdmin && !event && ( + + + + )} setName(e.target.value)} /> @@ -276,11 +306,12 @@ function EventConfigPanel({ event, onRefresh }: { event: Event; onRefresh: () => const [err, setErr] = useState(""); useEffect(() => { + const q = `?vendorId=${encodeURIComponent(event.vendorId)}&limit=200`; Promise.all([ - api.get>("/taxes?limit=100").then((r) => setTaxes(r.data)), - api.get>("/products?limit=200").then((r) => setProducts(r.data)), + api.get>(`/taxes${q}`).then((r) => setTaxes(r.data)), + api.get>(`/products${q}`).then((r) => setProducts(r.data)), ]).catch(console.error); - }, []); + }, [event.vendorId]); const addTax = async () => { setErr(""); diff --git a/server/src/lib/vendorScope.ts b/server/src/lib/vendorScope.ts index e0c1a04..156c22f 100644 --- a/server/src/lib/vendorScope.ts +++ b/server/src/lib/vendorScope.ts @@ -14,3 +14,22 @@ export function resolveVendorId( } return authReq.auth.vendorId; } + +/** + * Returns a Prisma `where` fragment for vendor filtering on list queries. + * - Admin with ?vendorId= → filter to that vendor + * - Admin without ?vendorId= → no filter (sees all) + * - Other roles → always locked to own vendorId + */ +export function vendorWhereClause( + authReq: AuthenticatedRequest, + query: Record = {} +): Record { + if (authReq.auth.roleName === "admin") { + if (typeof query.vendorId === "string" && query.vendorId) { + return { vendorId: query.vendorId }; + } + return {}; + } + return { vendorId: authReq.auth.vendorId }; +} diff --git a/server/src/routes/events.ts b/server/src/routes/events.ts index 86056a0..27f1d73 100644 --- a/server/src/routes/events.ts +++ b/server/src/routes/events.ts @@ -5,7 +5,7 @@ import { requireAuth, requireRole } from "../middleware/auth.js"; import { AppError } from "../middleware/errorHandler.js"; import { parsePage, paginatedResponse } from "../lib/pagination.js"; import { AuthenticatedRequest } from "../types/index.js"; -import { resolveVendorId } from "../lib/vendorScope.js"; +import { resolveVendorId, vendorWhereClause } from "../lib/vendorScope.js"; const router = Router(); const auth = requireAuth as unknown as (r: Request, s: Response, n: NextFunction) => void; @@ -39,7 +39,7 @@ router.get("/", auth, vendorUp, async (req: Request, res: Response, next: NextFu try { const authReq = req as AuthenticatedRequest; const { page, limit, skip } = parsePage(req.query as Record); - const where = vendorScope(authReq); + const where = vendorWhereClause(authReq, req.query as Record); const [data, total] = await Promise.all([ prisma.event.findMany({