Add multi-vendor capability with admin vendor management

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 07:59:58 -05:00
parent 65eb405cf1
commit e1b1a82e07
11 changed files with 379 additions and 195 deletions

View File

@@ -1,6 +1,8 @@
import React, { useEffect, useState, useCallback } from "react";
import { api } from "../api/client";
import { useAuth } from "../context/AuthContext";
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";
@@ -50,6 +52,8 @@ interface EventSummary {
// ─── Main Page ──────────────────────────────────────────────────────────────
export default function EventsPage() {
const { user } = useAuth();
const [vendorId, setVendorId] = useState(user?.vendorId ?? "");
const [events, setEvents] = useState<Event[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
@@ -61,7 +65,8 @@ export default function EventsPage() {
const load = useCallback(async () => {
setLoading(true);
try {
const res = await api.get<ApiList<Event>>("/events?limit=50");
const q = vendorId && user?.role === "admin" ? `?vendorId=${encodeURIComponent(vendorId)}&limit=50` : "?limit=50";
const res = await api.get<ApiList<Event>>(`/events${q}`);
setEvents(res.data);
setTotal(res.pagination.total);
} catch (err) {
@@ -69,7 +74,7 @@ export default function EventsPage() {
} finally {
setLoading(false);
}
}, []);
}, [vendorId, user?.role]);
useEffect(() => { load(); }, [load]);
@@ -120,11 +125,14 @@ export default function EventsPage() {
return (
<div style={{ padding: "32px 28px" }}>
<PageHeader
title="Events"
subtitle={`${total} event${total !== 1 ? "s" : ""}`}
action={<Btn onClick={() => { setEditing(null); setShowForm(true); }}>+ New Event</Btn>}
/>
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", flexWrap: "wrap", gap: 12, marginBottom: 4 }}>
<PageHeader
title="Events"
subtitle={`${total} event${total !== 1 ? "s" : ""}`}
action={<Btn onClick={() => { setEditing(null); setShowForm(true); }}>+ New Event</Btn>}
/>
<VendorFilter vendorId={vendorId} onChange={setVendorId} />
</div>
<Table columns={columns} data={events} keyField="id" loading={loading} emptyText="No events yet." />