# Saved List Filters

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 143  
**Tier:** All tiers  
**Depends on:** `foundation-design-system`, `foundation-auth-rbac`, `marketing-leads-pipeline`, `invoices-core`, `projects-module`, `customers-module`, `tasks-board-engine`, `expenses-module`  
**Referenced by:** (all list-view module specs)

---

## Overview

Every module list view (leads, invoices, projects, customers, tasks, expenses) has filters but no way to save them. Users who repeatedly apply the same filters (e.g., "overdue invoices from this month" or "web design leads in negotiation") re-apply filters manually every session. This spec defines a saved filters system that works across all list views.

---

## Data Model

```sql
CREATE TABLE saved_filters (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,  -- null = shared with whole team
  module TEXT NOT NULL
    CHECK (module IN ('leads', 'invoices', 'projects', 'customers', 'tasks', 'expenses', 'time_entries', 'contractors')),
  name TEXT NOT NULL,
  filters JSONB NOT NULL,             -- module-specific filter shape
  is_default BOOLEAN NOT NULL DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_saved_filters_tenant_module ON saved_filters(tenant_id, module);
-- Only one default per user+module combination (enforced in application layer)
```

---

## Filter Shape by Module

```ts
// leads
type LeadsFilters = {
  status?: string[];    // e.g. ['qualified', 'proposal_sent']
  assignee_id?: string;
  source?: string[];
  score_min?: number;
  score_max?: number;
  date_from?: string;
  date_to?: string;
};

// invoices
type InvoicesFilters = {
  status?: string[];
  customer_id?: string;
  date_from?: string;
  date_to?: string;
  overdue_only?: boolean;
};

// projects
type ProjectsFilters = {
  status?: string[];
  billing_type?: string[];
  customer_id?: string;
  assignee_id?: string;
};

// (similar shape per module — keys match existing filter params)
```

---

## UI Pattern (Consistent Across All List Views)

Every module list view gets a **[Saved views ▾]** button next to filters:

```
┌──────────────────────────────────────────────────────────────┐
│  Leads                 [Search...]  [Filters ▾]  [Saved views ▾]│
│                                                              │
│  [Saved views ▾] expands to:                                 │
│  ─────────────────────────────────────────────────────────  │
│  ★ Qualified leads (default)                         [Edit]  │
│    Overdue invoices this month                       [Edit]  │
│    ── Team views ──                                          │
│    High-value projects                               [Edit]  │
│    Web design pipeline                               [Edit]  │
│  ─────────────────────────────────────────────────────────  │
│  [+ Save current filters as view]                            │
│  [Manage saved views →]                                      │
└──────────────────────────────────────────────────────────────┘
```

**★ default** = loaded automatically when user opens the module.

---

## Save Current Filters

**[+ Save current filters as view]**:

```
┌──────────────────────────────────────────────────────────────┐
│  Save as view                                                │
│                                                              │
│  Name     [Qualified leads this quarter_______________]      │
│                                                              │
│  Visible to                                                  │
│  ● Just me                                                   │
│  ○ Whole team                                                │
│                                                              │
│  ☐ Set as default view for this module                       │
│                                                              │
│  [Cancel]  [Save view]                                       │
└──────────────────────────────────────────────────────────────┘
```

---

## Default View Behavior

If a saved filter has `is_default = true` for a user+module:
- Opening the module URL applies those filters automatically
- URL does not change (filters applied client-side from saved state)
- Clear button shows "Clear (reset to default)" if a default view is set

Only one default per user+module. Saving a new default removes the previous default (application-layer enforcement, not DB unique constraint — constraint would need to ignore `is_default = false` rows).

---

## Manage Saved Views Page

**[Manage saved views →]** → `/settings/saved-views` (a dedicated settings page — it spans all modules via the module switcher, so it is a page, not a per-list modal):

```
┌──────────────────────────────────────────────────────────────┐
│  Saved views — Leads                                         │
│                                                              │
│  My views                                                    │
│  ★ Qualified leads        (default)       [Edit] [Delete]    │
│    Overdue this month                     [Edit] [Delete]    │
│                                                              │
│  Team views                                                  │
│    Web design pipeline    by Alex Cohen   [Edit] [Delete]    │
│    High-value > ₪50k      by Yossi Levi   [Edit] [Delete]    │
│                                                              │
│  Module: [Leads ▾]  [Invoices ▾]  [Projects ▾]  ...         │
└──────────────────────────────────────────────────────────────┘
```

Users can delete team views they created (or any view if `users:manage`). Users cannot delete another user's personal views.

---

## URL Persistence

When a saved view is active, the URL gains `?view={id}` so the view can be shared via link:

`/leads?view=a1b2c3d4`

Loading a URL with `?view=` applies that view's filters (if the user has access to it). Views with `user_id IS NULL` (team views) are accessible to all tenant members via URL.

---

## API

```
GET /api/saved-filters?module=leads
    → list saved filters for module (personal + team)
      Returns: [{ id, name, filters, is_default, user_id, created_by_name }]
      Requires: authenticated

POST /api/saved-filters
     → create saved filter
       body: { module, name, filters, is_default, shared: boolean }
       Requires: authenticated

PATCH /api/saved-filters/:id
      → update name, filters, is_default, or shared
        Requires: owner of filter, or users:manage for others

DELETE /api/saved-filters/:id
       → delete saved filter
         Requires: owner of filter, or users:manage for others
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| `filters JSONB` | Not normalized columns | Filter shapes differ per module; JSONB is the only practical choice without 8 separate tables |
| Personal + team distinction | Not all-shared or all-personal | Power users want private views for their workflow; team leads want shared views to standardize list operations |
| Default enforced in app layer | Not DB unique partial index | Toggling default requires reading current state first; app-layer is simpler and correct under single-tenant write load |
| URL `?view=` parameter | Not session storage | Links to specific views can be shared between team members; session storage breaks on tab change |
