feat(quotes): filter list to the logged-in rep's own quotes

This commit is contained in:
Dave 2026-08-31 23:26:42 +02:00
parent defa481661
commit 8faa4437b6

View file

@ -32,6 +32,8 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import getPocketBase from "@/lib/pocketbase"; import getPocketBase from "@/lib/pocketbase";
import { useAuth } from "@/components/auth-provider";
import { isRepUser } from "@/lib/rep-visibility";
import { import {
getQuoteItems, getQuoteItems,
fetchQuoteItemCounts, fetchQuoteItemCounts,
@ -140,6 +142,7 @@ const AGED_STATUSES: ReadonlySet<string> = new Set(["new", "claimed"]);
export default function QuotesPage() { export default function QuotesPage() {
const router = useRouter(); const router = useRouter();
const { user } = useAuth();
const [quotes, setQuotes] = useState<Quote[]>([]); const [quotes, setQuotes] = useState<Quote[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(""); const [error, setError] = useState("");
@ -439,6 +442,15 @@ export default function QuotesPage() {
return map; return map;
}, [users]); }, [users]);
// Rep-scoped UI filter — same rationale as the Orders page (see
// src/lib/rep-visibility.ts). Quotes only carry rep via the
// customer relation (no denormalized text field like orders.rep).
const visibleQuotes = useMemo(() => {
const userType = (user as { user_type?: string[] } | null)?.user_type;
if (!user || !isRepUser(userType)) return quotes;
return quotes.filter((quote) => quote.expand?.customer?.rep === user.id);
}, [quotes, user]);
/** /**
* Toggle a row's expanded state. The first time a row opens we kick * Toggle a row's expanded state. The first time a row opens we kick
* off a `getQuoteItems(id)` fetch; subsequent toggles use the cache. * off a `getQuoteItems(id)` fetch; subsequent toggles use the cache.
@ -867,7 +879,7 @@ export default function QuotesPage() {
}, },
}, },
]} ]}
data={quotes} data={visibleQuotes}
loading={loading} loading={loading}
error={error} error={error}
// Placeholder lists every surface the pushdown actually hits // Placeholder lists every surface the pushdown actually hits
@ -898,7 +910,7 @@ export default function QuotesPage() {
"text-xs whitespace-nowrap", "text-xs whitespace-nowrap",
loading loading
? "text-muted-foreground/60" ? "text-muted-foreground/60"
: quotes.length === 0 : visibleQuotes.length === 0
? "text-warn font-medium" ? "text-warn font-medium"
: "text-muted-foreground", : "text-muted-foreground",
)} )}
@ -906,11 +918,11 @@ export default function QuotesPage() {
> >
{loading ? ( {loading ? (
<>Searching…</> <>Searching…</>
) : quotes.length === 0 ? ( ) : visibleQuotes.length === 0 ? (
<>0 matches for &ldquo;{searchInput.trim()}&rdquo;</> <>0 matches for &ldquo;{searchInput.trim()}&rdquo;</>
) : ( ) : (
<> <>
{quotes.length} match{quotes.length === 1 ? "" : "es"} for{" "} {visibleQuotes.length} match{visibleQuotes.length === 1 ? "" : "es"} for{" "}
<span className="font-medium text-foreground"> <span className="font-medium text-foreground">
&ldquo;{searchInput.trim()}&rdquo; &ldquo;{searchInput.trim()}&rdquo;
</span> </span>