docs: include rep-scoped list filter plan + summary

The plan + summary landed locally during the implementation but were
not committed alongside the code. Both documents describe the
rep-scoped Orders / Quotes / Invoicing / Delivery Notes list filter
work that already shipped (commits 8faa443, ed07617, 29eb003, 9cf8e39
etc).

- 2026-08-31-rep-scoped-list-filter.md — the implementation plan
- 2026-09-01-rep-scoped-list-filter-SUMMARY.md — post-implementation
  summary with the commit log + verification notes + the known
  limitation (Orders/Quotes tab counts are unfiltered for reps)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Dave 2026-09-01 02:11:28 +02:00
parent 742010a9a9
commit 57584798f3
2 changed files with 553 additions and 0 deletions

View file

@ -0,0 +1,495 @@
# Rep-Scoped List Filter Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** When a logged-in PocketBase user's `user_type` includes `"REP"`, the Orders, Quotes, Invoicing, and Delivery Notes list pages show only records tied to that rep (via `customer.rep` or, for orders, the denormalized `orders.rep` field). Everyone else (buyer, quoter, admin, boss, etc.) sees the unfiltered list, unchanged from today.
**Architecture:** This is a UI-layer filter only — **not** a security boundary. No PocketBase API rules change, no server session plumbing, no changes to `/api/v1/*` auth. Orders and Quotes already return `customer` (and for orders, `customer.rep`) in their expand, so those two pages filter the already-fetched array client-side. Invoicing and Delivery Notes fetch PocketBase directly from the browser (no `/api/v1` route), so those two get a real PB filter clause (`order.customer.rep = "<id>"` / `customer.rep = "<id>"`) added to their existing query.
**Known limitation (accepted for this pass):** On Orders and Quotes, the tab-count badges and pagination totals (`totalPages`, `counts`) come from the unfiltered dataset — they are not rep-scoped. A rep filtering on Orders/Quotes may see a page with fewer than `perPage` visible rows, or a "Page 1 of N" that doesn't match the filtered count. Invoicing and Delivery Notes don't have this problem — their filter is a real PB query, so their counts are accurate. Fixing Orders/Quotes properly means threading `rep` through `orders-service.ts` / `quotes-service.ts`'s PB filters, which is a larger, separate follow-up (not in scope here).
**Tech Stack:** Next.js Client Components, PocketBase JS SDK, Vitest.
---
## File Structure
- **Create:** `src/lib/rep-visibility.ts` — pure helper `isRepUser(userType)`, mirrors the existing `isRep()` pattern in `src/lib/server/tv-dashboard-service.ts:349`.
- **Create:** `src/lib/rep-visibility.test.ts` — unit tests for the helper.
- **Modify:** `src/app/(app)/orders/page.tsx` — client-side filter on the rendered order list.
- **Modify:** `src/app/(app)/quotes/page.tsx` — client-side filter on the rendered quote list.
- **Modify:** `src/app/(app)/invoicing/page.tsx` — PB filter clause added to `loadInvoicingRows`.
- **Modify:** `src/lib/pb-helpers.ts` — `getDeliveryNotes` gains an optional `repId` param that adds a PB filter clause.
- **Modify:** `src/app/(app)/delivery-notes/page.tsx` — passes the current rep's id into `getDeliveryNotes`.
---
### Task 1: `isRepUser` helper
**Files:**
- Create: `src/lib/rep-visibility.ts`
- Test: `src/lib/rep-visibility.test.ts`
- [ ] **Step 1: Write the failing test**
```typescript
// src/lib/rep-visibility.test.ts
import { describe, expect, it } from "vitest";
import { isRepUser } from "./rep-visibility";
describe("isRepUser", () => {
it("returns true when user_type includes REP", () => {
expect(isRepUser(["REP"])).toBe(true);
});
it("is case-insensitive", () => {
expect(isRepUser(["rep"])).toBe(true);
});
it("returns true when REP is combined with other roles", () => {
expect(isRepUser(["BUYER", "REP"])).toBe(true);
});
it("returns false for other roles", () => {
expect(isRepUser(["BUYER"])).toBe(false);
expect(isRepUser(["ADMIN", "QUOTER"])).toBe(false);
});
it("returns false for null, undefined, or non-array input", () => {
expect(isRepUser(null)).toBe(false);
expect(isRepUser(undefined)).toBe(false);
expect(isRepUser("REP")).toBe(false);
});
it("returns false for an empty array", () => {
expect(isRepUser([])).toBe(false);
});
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `npx vitest run src/lib/rep-visibility.test.ts`
Expected: FAIL — `Cannot find module './rep-visibility'` (or similar; the module doesn't exist yet).
- [ ] **Step 3: Write the implementation**
```typescript
// src/lib/rep-visibility.ts
/**
* `users.user_type` is a PB multi-select. "REP" is the existing
* convention for marking a sales rep — see the identical pattern in
* `src/lib/server/tv-dashboard-service.ts` (`TV_REP_ROLES` / `isRep`).
*/
export const REP_USER_TYPE = "REP";
export function isRepUser(userType: unknown): boolean {
if (!Array.isArray(userType)) return false;
return userType.some(
(value) => typeof value === "string" && value.toUpperCase() === REP_USER_TYPE,
);
}
```
- [ ] **Step 4: Run the test to verify it passes**
Run: `npx vitest run src/lib/rep-visibility.test.ts`
Expected: PASS — all 6 assertions green.
- [ ] **Step 5: Commit**
```bash
git add src/lib/rep-visibility.ts src/lib/rep-visibility.test.ts
git commit -m "feat: add isRepUser helper for rep-scoped list filtering"
```
---
### Task 2: Orders page — filter to the rep's own orders
**Files:**
- Modify: `src/app/(app)/orders/page.tsx`
- [ ] **Step 1: Import the auth hook and the helper**
In `src/app/(app)/orders/page.tsx`, add to the top imports (near the other `@/lib` and `@/components` imports, e.g. after the `getPocketBase` import at line 26):
```typescript
import { useAuth } from "@/components/auth-provider";
import { isRepUser } from "@/lib/rep-visibility";
```
- [ ] **Step 2: Read the current user and derive the visible order list**
Inside `OrdersPage`, immediately after the existing `useRouter()` call at line 114 (`const router = useRouter();`), add:
```typescript
const { user } = useAuth();
```
Then, right after the `otherTabCounts` `useMemo` block (ends at line 298, just before the `return (` on line 300), add a new `useMemo`:
```typescript
// Rep-scoped UI filter — reps only see orders tied to them, either
// via the denormalized `orders.rep` text field or via
// `customer.rep`. Everyone else (buyer/quoter/admin/boss/etc.) sees
// the unfiltered list. This filters the already-fetched page of
// orders; it does not change what the server returns, so tab
// counts / pagination totals stay unscoped — see the plan's "Known
// limitation" note.
const visibleOrders = useMemo(() => {
const userType = (user as { user_type?: string[] } | null)?.user_type;
if (!user || !isRepUser(userType)) return orders;
return orders.filter(
({ order }) =>
order.rep === user.id || order.expand?.customer?.rep === user.id,
);
}, [orders, user]);
```
- [ ] **Step 3: Use `visibleOrders` everywhere the rendered list is read**
Replace these three usages (leave the `orders` state itself, `fetchOrders`, and the realtime subscription untouched — they still fetch/refetch the full page):
1. Line 282, `const totalLoaded = orders.length;` → `const totalLoaded = visibleOrders.length;`
2. Line 390, `) : orders.length === 0 ? (` → `) : visibleOrders.length === 0 ? (`
3. Lines 445–464 (the "N matches in Tab for query" block) — change every `orders.length` reference to `visibleOrders.length`:
```typescript
{searching && (
<p className="text-sm text-muted-foreground">
{visibleOrders.length} match{visibleOrders.length === 1 ? "" : "es"} in
{" "}
{TAB_GROUP_VISUAL[activeTab].label} for
```
4. Line 466, `{orders.map(({ order, items, repName, tabGroups }) => (` → `{visibleOrders.map(({ order, items, repName, tabGroups }) => (`
- [ ] **Step 4: Manual verification**
Run the dev server (`npm run dev`, or use the existing preview tooling) and:
1. Log in as a user whose `user_type` does NOT include `REP` (e.g. an admin/buyer account). Confirm `/orders` shows the same orders as before this change.
2. Log in as a user whose `user_type` includes `REP` and who is set as `rep` on at least one customer (or has `orders.rep` set to their id on at least one order). Confirm `/orders` only shows orders for that rep's customers/orders.
3. Confirm the "New Order" flow, search, and tab-switching still work (they operate on `orders`/`fetchOrders` unchanged — only the rendered list is filtered).
- [ ] **Step 5: Commit**
```bash
git add src/app/\(app\)/orders/page.tsx
git commit -m "feat(orders): filter list to the logged-in rep's own orders"
```
---
### Task 3: Quotes page — filter to the rep's own quotes
**Files:**
- Modify: `src/app/(app)/quotes/page.tsx`
- [ ] **Step 1: Import the auth hook and the helper**
Add near the top imports, after the `getPocketBase` import at line 34:
```typescript
import { useAuth } from "@/components/auth-provider";
import { isRepUser } from "@/lib/rep-visibility";
```
- [ ] **Step 2: Read the current user and derive the visible quote list**
Inside `QuotesPage`, immediately after `const router = useRouter();` at line 142, add:
```typescript
const { user } = useAuth();
```
After the `userById` `useMemo` block (ends at line 440, just before the `toggleExpand` callback at line 447), add:
```typescript
// 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]);
```
- [ ] **Step 3: Use `visibleQuotes` in the DataTable and the search-count chip**
1. Line 870, `data={quotes}` → `data={visibleQuotes}` (the `DataTable` prop).
2. Lines 895–919 (the live result-count chip) — change every `quotes.length` reference to `visibleQuotes.length`:
```typescript
{searchInput.trim() && (
<span
className={cn(
"text-xs whitespace-nowrap",
loading
? "text-muted-foreground/60"
: visibleQuotes.length === 0
? "text-warn font-medium"
: "text-muted-foreground",
)}
data-testid="quotes-search-count"
>
{loading ? (
<>Searching…</>
) : visibleQuotes.length === 0 ? (
<>0 matches for &ldquo;{searchInput.trim()}&rdquo;</>
) : (
<>
{visibleQuotes.length} match{visibleQuotes.length === 1 ? "" : "es"} for{" "}
<span className="font-medium text-foreground">
&ldquo;{searchInput.trim()}&rdquo;
</span>
</>
)}
</span>
)}
```
Leave `itemCounts` / `itemsByQuote` / `expandedIds` untouched — they're keyed by quote id and stay correct regardless of which quotes are visible.
- [ ] **Step 4: Manual verification**
1. Log in as a non-rep user. Confirm `/quotes` is unchanged.
2. Log in as a rep user who is set as `rep` on at least one customer with quotes. Confirm `/quotes` only shows that rep's quotes, across both the tab-list view and the search view (type something into the search box).
3. Confirm row expand (chevron → items), Process/Open buttons, and template actions still work.
- [ ] **Step 5: Commit**
```bash
git add src/app/\(app\)/quotes/page.tsx
git commit -m "feat(quotes): filter list to the logged-in rep's own quotes"
```
---
### Task 4: Invoicing page — PB filter for the rep's own order items
**Files:**
- Modify: `src/app/(app)/invoicing/page.tsx`
- [ ] **Step 1: Import the auth hook and the helper**
Add near the top imports, after the `getPocketBase` import at line 14:
```typescript
import { useAuth } from "@/components/auth-provider";
import { isRepUser } from "@/lib/rep-visibility";
```
- [ ] **Step 2: Thread an optional `repId` through `loadInvoicingRows`**
Modify the function signature and its first call site (lines 162–182):
```typescript
async function loadInvoicingRows(
pb: ReturnType<typeof getPocketBase>,
repId: string | null,
): Promise<RawInvoicingRow[]> {
const repFilter = repId
? ` && order.customer.rep = "${escapeFilterValue(repId)}"`
: "";
const seedRows = await listAllInvoicingRows(pb, INVOICING_STATUS_FILTER + repFilter);
const readyOrderIds = Array.from(
new Set(
seedRows
.filter((row) => row.item_status === "ready_invoice")
.map((row) => row.expand?.order?.id?.trim() ?? "")
.filter(Boolean),
),
);
if (readyOrderIds.length === 0) {
return seedRows;
}
// No need to re-apply repFilter here — readyOrderIds is already
// derived from the rep-scoped seedRows above, so the sibling fetch
// (by explicit order id) is implicitly scoped too.
const siblingFilter = readyOrderIds
.map((orderId) => `order = "${escapeFilterValue(orderId)}"`)
.join(" || ");
const siblingRows = await listAllInvoicingRows(pb, siblingFilter);
return dedupeRawRows([...seedRows, ...siblingRows]);
}
```
- [ ] **Step 3: Compute `repId` in the page and pass it into `load()`**
Inside `InvoicingPage`, near the top of the component (alongside the other `useState` calls, e.g. right after the `useBooleanPreference` call around line 1371), add:
```typescript
const { user } = useAuth();
```
Then update the `load` callback (lines 1407–1418):
```typescript
const load = useCallback(async () => {
setRows(null);
try {
const pb = getPocketBase();
const userType = (user as { user_type?: string[] } | null)?.user_type;
const repId = user && isRepUser(userType) ? user.id : null;
const items = await loadInvoicingRows(pb, repId);
setRows(items);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
toast.error(message);
setRows([]);
}
}, [user]);
```
The existing `useEffect(() => { ...; void load(); }, [load])` (around line 1483) already re-runs whenever `load`'s identity changes, so it will automatically refetch once `user` resolves from the auth provider (which starts as `null` while PB auth is loading).
- [ ] **Step 4: Manual verification**
1. Log in as a non-rep user. Confirm the Invoicing page (`/invoicing`) shows the same rows as before.
2. Log in as a rep user tied to at least one customer with rows in `ready_invoice` / `invoiced` / `delivery_note` status. Confirm only that rep's rows appear, in both the "To Invoice" and "Invoiced" tabs.
3. Confirm the QuickBooks-send and delivery-note-creation modals still work on a visible row.
- [ ] **Step 5: Commit**
```bash
git add src/app/\(app\)/invoicing/page.tsx
git commit -m "feat(invoicing): filter rows to the logged-in rep's own orders"
```
---
### Task 5: Delivery Notes page — PB filter for the rep's own notes
**Files:**
- Modify: `src/lib/pb-helpers.ts`
- Modify: `src/app/(app)/delivery-notes/page.tsx`
- [ ] **Step 1: Add an optional `repId` param to `getDeliveryNotes`**
In `src/lib/pb-helpers.ts`, replace the existing function (lines 1068–1076):
```typescript
export async function getDeliveryNotes(
page = 1,
perPage = 50,
search = "",
repId: string | null = null,
) {
const pb = getPocketBase();
const clauses: string[] = [];
if (search) {
clauses.push(`(delivery_note_number ~ "${search}" || notes ~ "${search}")`);
}
if (repId) {
clauses.push(`customer.rep = "${repId.replace(/"/g, '\\"')}"`);
}
const filter = clauses.join(" && ");
return pb.collection("delivery_notes").getList<DeliveryNote>(page, perPage, {
filter,
sort: "-created",
expand: "order,company,customer",
});
}
```
(This is backward-compatible — the new param is optional and defaults to `null`, so the one other behavior — no filter at all when both `search` and `repId` are empty — is unchanged.)
- [ ] **Step 2: Pass the current rep's id from the Delivery Notes page**
In `src/app/(app)/delivery-notes/page.tsx`, add to the imports (after the `getDeliveryNotes` import at line 27):
```typescript
import { useAuth } from "@/components/auth-provider";
import { isRepUser } from "@/lib/rep-visibility";
```
Inside `DeliveryNotesPage`, right after `const router = useRouter();` at line 112, add:
```typescript
const { user } = useAuth();
```
Update `fetchNotes` (lines 318–352) to compute and pass `repId`, and add `user` to its dependency array:
```typescript
const fetchNotes = useCallback(async () => {
setLoading(true);
setError("");
try {
const userType = (user as { user_type?: string[] } | null)?.user_type;
const repId = user && isRepUser(userType) ? user.id : null;
// Fetch a wide page so the client-side tab filter sees the
// full set — the previous `perPage=20` made the active
// tab silently drop records past rank 20 (the operator
// saw "no DN" when the row existed but was buried). 500
// is well over the production DN count (≈30) and still
// cheap for PB.
const result = await getDeliveryNotes(page, 500, search, repId);
setNotes(result.items);
setTotalPages(result.totalPages);
// Batch-fetch the related quotes in one query so the Q
// icon can show "create" vs "open" without a per-row
// round trip. Failures here are non-fatal — the Q icon
// falls back to "create quote" if we don't know whether
// a quote exists.
if (result.items.length > 0) {
try {
const ids = result.items.map((n) => n.id);
const map = await getQuotesForDeliveryNotes(ids);
setQuoteByDnId(map);
} catch {
setQuoteByDnId(new Map());
}
} else {
setQuoteByDnId(new Map());
}
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to load delivery notes");
} finally {
setLoading(false);
}
}, [page, search, user]);
```
- [ ] **Step 3: Manual verification**
1. Log in as a non-rep user. Confirm `/delivery-notes` shows the same notes as before.
2. Log in as a rep user tied to at least one customer with delivery notes. Confirm only that rep's notes appear, across all four status tabs (Active / Cancelled / Completed / All).
3. Confirm search, company grouping, and the Combine-DNs flow still work.
- [ ] **Step 4: Commit**
```bash
git add src/lib/pb-helpers.ts src/app/\(app\)/delivery-notes/page.tsx
git commit -m "feat(delivery-notes): filter list to the logged-in rep's own notes"
```
---
### Task 6: Full regression pass
**Files:** none (verification only)
- [ ] **Step 1: Run the full test suite**
Run: `npx vitest run`
Expected: PASS — no regressions in existing suites (`orders-service.test.ts`, `quotes-service.test.ts`, `boss-dashboard-service.test.ts`, `pocketbase-admin.test.ts`, etc. are all untouched by this plan).
- [ ] **Step 2: Manual cross-check with a boss/admin account**
Log in as an admin/boss account (`user_type` does NOT include `REP`) and confirm all four pages — Orders, Quotes, Invoicing, Delivery Notes — show the full, unfiltered list exactly as they did before this plan. This is the most important check: the filter must be strictly opt-in.
- [ ] **Step 3: Manual cross-check with two different rep accounts**
If two rep test accounts are available (each tied to different customers), confirm rep A never sees rep B's orders/quotes/invoicing-rows/delivery-notes, and vice versa.
- [ ] **Step 4: Commit (if any fixes were needed)**
```bash
git add -A
git commit -m "test: regression pass for rep-scoped list filtering"
```

View file

@ -0,0 +1,58 @@
# Rep-Scoped List Filter — Implementation Summary
> Companion to [`2026-08-31-rep-scoped-list-filter.md`](./2026-08-31-rep-scoped-list-filter.md). Records what landed, how it was verified, and what still needs manual browser testing.
## Result
All 6 plan tasks complete. Pushed to `origin/main` on 2026-09-01.
## Commits (newest first)
| SHA | Message |
|-----|---------|
| `9cf8e39` | test(invoicing): mock useAuth to keep rep-filter a no-op in tests |
| `ed07617` | feat(delivery-notes): filter list to the logged-in rep's own notes |
| `29eb003` | feat(invoicing): filter rows to the logged-in rep's own orders |
| `8faa443` | feat(quotes): filter list to the logged-in rep's own quotes |
| `defa481` | feat(orders): filter list to the logged-in rep's own orders |
| `d23c811` | feat: add isRepUser helper for rep-scoped list filtering |
## Files changed
- **New:** `src/lib/rep-visibility.ts` — `isRepUser(userType)` + `REP_USER_TYPE` constant, mirrors the `isRep` pattern in `src/lib/server/tv-dashboard-service.ts`
- **New:** `src/lib/rep-visibility.test.ts` — 6 unit tests covering: REP present, case-insensitive, mixed roles, other roles, null/undefined/non-array, empty array
- **Modified:** `src/app/(app)/orders/page.tsx` — `useAuth` + `visibleOrders` `useMemo` filtering on `order.rep` / `order.expand?.customer?.rep`. Replaces `orders.length` / `orders.map` with `visibleOrders.length` / `visibleOrders.map` at the 4 render sites listed in the plan (kept the `orders` state, `fetchOrders`, and realtime subscription untouched)
- **Modified:** `src/app/(app)/quotes/page.tsx` — `useAuth` + `visibleQuotes` `useMemo` filtering on `quote.expand?.customer?.rep`. `DataTable.data` and the live search-count chip read from `visibleQuotes`
- **Modified:** `src/app/(app)/invoicing/page.tsx` — `useAuth` + new `repId` param on `loadInvoicingRows`. PB filter clause `&& order.customer.rep = "<id>"` added to the seed fetch; sibling fetch is implicitly scoped because `readyOrderIds` derives from the already-scoped seed rows
- **Modified:** `src/lib/pb-helpers.ts` — `getDeliveryNotes` gained an optional `repId: string | null = null` param; when supplied, adds a `customer.rep = "<id>"` clause to the existing filter chain
- **Modified:** `src/app/(app)/delivery-notes/page.tsx` — `useAuth` + computes `repId` in `fetchNotes`, passes to `getDeliveryNotes`
- **Modified:** `src/app/(app)/invoicing/page.test.tsx` — added `vi.mock("@/components/auth-provider", ...)` returning a stable non-REP user via `vi.hoisted` (otherwise the page's `load` re-fires every render and breaks the `toHaveBeenCalledTimes(2)` assertions)
## Verification done
- `npx vitest run src/lib/rep-visibility.test.ts` — 6/6 pass
- `npx vitest run` (full suite) — 2441 pass / 15 fail. The same 15 failures occur on `origin/main`'s tree (verified by `git checkout origin/main -- <files>` then re-running). None are new regressions.
- `npx tsc --noEmit` — zero errors in any touched file
- Plan Task 2-5 "manual verification" steps (login as non-rep, login as rep, login as two reps) are **browser tests** and were **not** run from this terminal. See "Still to do" below.
## Deviations from the plan
1. **Orders page `visibleOrders` placement.** Plan said to add the `useMemo` after `otherTabCounts` (line 298), but the plan also changed line 282's `const totalLoaded = orders.length` to `const totalLoaded = visibleOrders.length` — which would forward-reference `visibleOrders`. Moved the `useMemo` to before `totalLoaded` instead. Functionally identical, no behaviour change.
2. **Invoicing test mock.** The plan did not anticipate that adding `useAuth` to a page would break a test that doesn't wrap with `AuthProvider`. Added the `vi.mock` in `page.test.tsx` (test fix, separate commit `9cf8e39`). The mock returns a non-REP user so the filter is a no-op for existing test data (which doesn't carry a `customer.rep` field).
## Still to do (manual browser verification)
- [ ] Log in as a non-rep user (`user_type` does NOT include `REP`) → confirm `/orders`, `/quotes`, `/invoicing`, `/delivery-notes` show the same rows as before this change
- [ ] Log in as a rep user tied to ≥1 customer → confirm `/orders`, `/quotes`, `/invoicing`, `/delivery-notes` only show that rep's rows
- [ ] If two rep test accounts are available, confirm rep A never sees rep B's rows (and vice versa) across all four pages
- [ ] Tab-switching, search, and "New Order" flow on Orders — these operate on the `orders` state, not the filtered `visibleOrders`, so they should still work; confirm in browser
## Known limitations (from the plan, accepted for this pass)
- On **Orders** and **Quotes**, tab-count badges and pagination totals (`totalPages`, `counts`) are derived from the unfiltered dataset — a rep filtering may see a page with fewer than `perPage` visible rows, or a "Page 1 of N" that doesn't match the filtered count. **Invoicing** and **Delivery Notes** don't have this problem because their filter is a real PB query, so their counts are accurate.
- Proper fix is threading `rep` through `orders-service.ts` / `quotes-service.ts`'s PB filters — separate, larger follow-up.
## Pre-existing items not touched
- `src/app/(auth)/login/page.tsx` — was already modified before this work started; left as-is.
- `docs/superpowers/plans/2026-08-31-rep-scoped-list-filter.md` — the plan file itself; remains untracked.