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 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { PageHeader } from "../components/PageHeader";
|
|||||||
import { VendorFilter } from "../components/VendorFilter";
|
import { VendorFilter } from "../components/VendorFilter";
|
||||||
import { Table } from "../components/Table";
|
import { Table } from "../components/Table";
|
||||||
import { Modal } from "../components/Modal";
|
import { Modal } from "../components/Modal";
|
||||||
import { FormField, Btn } from "../components/FormField";
|
import { FormField, Btn, inputStyle } from "../components/FormField";
|
||||||
|
|
||||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -139,6 +139,7 @@ export default function EventsPage() {
|
|||||||
{showForm && (
|
{showForm && (
|
||||||
<EventFormModal
|
<EventFormModal
|
||||||
event={editing}
|
event={editing}
|
||||||
|
defaultVendorId={vendorId}
|
||||||
onClose={() => setShowForm(false)}
|
onClose={() => setShowForm(false)}
|
||||||
onSaved={() => { setShowForm(false); load(); }}
|
onSaved={() => { setShowForm(false); load(); }}
|
||||||
/>
|
/>
|
||||||
@@ -159,20 +160,39 @@ export default function EventsPage() {
|
|||||||
|
|
||||||
// ─── Event Form Modal ────────────────────────────────────────────────────────
|
// ─── Event Form Modal ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function EventFormModal({ event, onClose, onSaved }: {
|
function EventFormModal({ event, defaultVendorId, onClose, onSaved }: {
|
||||||
event: Event | null;
|
event: Event | null;
|
||||||
|
defaultVendorId?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSaved: () => void;
|
onSaved: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const isAdmin = user?.role === "admin";
|
||||||
const [name, setName] = useState(event?.name ?? "");
|
const [name, setName] = useState(event?.name ?? "");
|
||||||
const [description, setDescription] = useState(event?.description ?? "");
|
const [description, setDescription] = useState(event?.description ?? "");
|
||||||
const [startsAt, setStartsAt] = useState(event ? toDatetimeLocal(event.startsAt) : "");
|
const [startsAt, setStartsAt] = useState(event ? toDatetimeLocal(event.startsAt) : "");
|
||||||
const [endsAt, setEndsAt] = useState(event ? toDatetimeLocal(event.endsAt) : "");
|
const [endsAt, setEndsAt] = useState(event ? toDatetimeLocal(event.endsAt) : "");
|
||||||
const [isActive, setIsActive] = useState(event?.isActive ?? true);
|
const [isActive, setIsActive] = useState(event?.isActive ?? true);
|
||||||
|
const [selectedVendorId, setSelectedVendorId] = useState(
|
||||||
|
event?.vendorId ?? defaultVendorId ?? ""
|
||||||
|
);
|
||||||
|
const [vendors, setVendors] = useState<Vendor[]>([]);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [saving, setSaving] = useState(false);
|
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 () => {
|
const save = async () => {
|
||||||
|
if (isAdmin && !event && !selectedVendorId) {
|
||||||
|
setError("Please select a vendor for this event.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -185,7 +205,8 @@ function EventFormModal({ event, onClose, onSaved }: {
|
|||||||
if (event) {
|
if (event) {
|
||||||
await api.put(`/events/${event.id}`, body);
|
await api.put(`/events/${event.id}`, body);
|
||||||
} else {
|
} else {
|
||||||
await api.post("/events", body);
|
const q = isAdmin && selectedVendorId ? `?vendorId=${encodeURIComponent(selectedVendorId)}` : "";
|
||||||
|
await api.post(`/events${q}`, body);
|
||||||
}
|
}
|
||||||
onSaved();
|
onSaved();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -197,6 +218,15 @@ function EventFormModal({ event, onClose, onSaved }: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title={event ? "Edit Event" : "New Event"} onClose={onClose}>
|
<Modal title={event ? "Edit Event" : "New Event"} onClose={onClose}>
|
||||||
|
{isAdmin && !event && (
|
||||||
|
<FormField label="Vendor" required>
|
||||||
|
<select style={inputStyle} value={selectedVendorId}
|
||||||
|
onChange={(e) => setSelectedVendorId(e.target.value)} required>
|
||||||
|
<option value="">Select vendor…</option>
|
||||||
|
{vendors.map((v) => <option key={v.id} value={v.id}>{v.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
<FormField label="Name">
|
<FormField label="Name">
|
||||||
<input style={input} value={name} onChange={(e) => setName(e.target.value)} />
|
<input style={input} value={name} onChange={(e) => setName(e.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -276,11 +306,12 @@ function EventConfigPanel({ event, onRefresh }: { event: Event; onRefresh: () =>
|
|||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const q = `?vendorId=${encodeURIComponent(event.vendorId)}&limit=200`;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
api.get<ApiList<Tax>>("/taxes?limit=100").then((r) => setTaxes(r.data)),
|
api.get<ApiList<Tax>>(`/taxes${q}`).then((r) => setTaxes(r.data)),
|
||||||
api.get<ApiList<Product>>("/products?limit=200").then((r) => setProducts(r.data)),
|
api.get<ApiList<Product>>(`/products${q}`).then((r) => setProducts(r.data)),
|
||||||
]).catch(console.error);
|
]).catch(console.error);
|
||||||
}, []);
|
}, [event.vendorId]);
|
||||||
|
|
||||||
const addTax = async () => {
|
const addTax = async () => {
|
||||||
setErr("");
|
setErr("");
|
||||||
|
|||||||
@@ -14,3 +14,22 @@ export function resolveVendorId(
|
|||||||
}
|
}
|
||||||
return authReq.auth.vendorId;
|
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<string, unknown> = {}
|
||||||
|
): Record<string, string> {
|
||||||
|
if (authReq.auth.roleName === "admin") {
|
||||||
|
if (typeof query.vendorId === "string" && query.vendorId) {
|
||||||
|
return { vendorId: query.vendorId };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return { vendorId: authReq.auth.vendorId };
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { requireAuth, requireRole } from "../middleware/auth.js";
|
|||||||
import { AppError } from "../middleware/errorHandler.js";
|
import { AppError } from "../middleware/errorHandler.js";
|
||||||
import { parsePage, paginatedResponse } from "../lib/pagination.js";
|
import { parsePage, paginatedResponse } from "../lib/pagination.js";
|
||||||
import { AuthenticatedRequest } from "../types/index.js";
|
import { AuthenticatedRequest } from "../types/index.js";
|
||||||
import { resolveVendorId } from "../lib/vendorScope.js";
|
import { resolveVendorId, vendorWhereClause } from "../lib/vendorScope.js";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const auth = requireAuth as unknown as (r: Request, s: Response, n: NextFunction) => void;
|
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 {
|
try {
|
||||||
const authReq = req as AuthenticatedRequest;
|
const authReq = req as AuthenticatedRequest;
|
||||||
const { page, limit, skip } = parsePage(req.query as Record<string, unknown>);
|
const { page, limit, skip } = parsePage(req.query as Record<string, unknown>);
|
||||||
const where = vendorScope(authReq);
|
const where = vendorWhereClause(authReq, req.query as Record<string, unknown>);
|
||||||
|
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
prisma.event.findMany({
|
prisma.event.findMany({
|
||||||
|
|||||||
Reference in New Issue
Block a user