# Inline Filters (Pill-as-Dropdown) Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Replace the modal-based filter form on `/report/` with interactive pill-dropdowns (Apply button appears only when pending changes exist, bidirectional project↔team cross-filter auto-removes invalid selections), and retire `_report_config_modal.html` entirely.
**Architecture:** Template-only change except for one backend tweak. The existing filter-pill strip (shipped with Executive Report v2) becomes clickable — each pill opens a Bootstrap-like popover containing the relevant editable widget (date picker or Choices.js multi-select). A scoped IIFE JS module inside `report.html` manages popover state, tracks dirty filters, and rebuilds the submit URL on Apply. Cross-filter data comes from a single JSON `
```
**Step 4.3: Check syntax**
```bash
USE_SQLITE=true DJANGO_DEBUG=true python manage.py check
```
Expected: only the `staticfiles.W004` warning.
**Step 4.4: Commit**
```bash
git add core/templates/core/report.html
git commit -m "$(cat <<'EOF'
JS: pill-popover interactive module for inline filters
One scoped IIFE that wires up the three filter pills into an
interactive, state-managed UI:
- Click pill -> open popover (lazy-inits Choices.js on first open)
- Click outside / Esc / other pill -> close
- OK commits popover's local edits into pending state; dirty pills
get orange outline + pulsing dot; Apply button slides in at the
right end of the strip
- Cross-filter: picking projects auto-removes now-invalid teams
with toast notice ("Removed Team X — no logs on selected projects"),
and vice versa. Scope = entire history.
- Apply -> rebuilds querystring from pending state + navigates
(full page reload, same URL scheme as the retired modal)
- Reset -> reverts all pills to URL-current values
XSS-safe throughout: textContent and createElement; no innerHTML with
user data. Matches the pattern in base.html's work-log-payroll modal
from the previous feature.
Graceful fallback: if Choices.js CDN fails to load, the module bails
early with a console warning; native still works
inside the popovers.
Co-Authored-By: Claude Opus 4.7 (1M context)
EOF
)"
```
---
### 🛑 CHECKPOINT 1 — all pill interactions demoable
Konrad should open `/report/` in the browser (hard refresh) and walk through:
1. Click the date pill → popover opens with month/custom toggle + pickers
2. Change from-month → click OK → pill shows new range, Apply button appears, pill has orange dirty outline
3. Click Apply → URL updates → report re-renders with new filters
4. Click projects pill → popover with Choices.js multi-select → pick 2 projects → OK → pill updates to "Project A + 1 more"
5. Click teams pill → popover → if a project is already selected, teams that haven't worked on it are disabled; pick a valid team → OK
6. Intentionally: pick a project, then add a team that doesn't match → on OK, that team gets auto-removed with a toast
7. Click the × on a pill (existing behaviour) → filter clears via full page reload
8. Click Reset when dirty → all pills revert, Apply disappears
9. Esc key → any open popover closes
10. Modal still works via the "Generate Report" button on the dashboard + "New Report" button on the report page (Task 5 retires them)
Await approval before Task 5 (modal retirement — destructive).
---
## Task 5: Retire the modal
**Files:**
- Modify: `core/templates/core/index.html` (line 187 — change button to plain link; line 605 — remove include)
- Modify: `core/templates/core/report.html` (lines 20-23 delete header "New Report" button; lines 387-389 delete bottom "New Report" button; line 400 remove include)
- Delete: `core/templates/core/_report_config_modal.html`
- Modify: `core/views.py` (lines 434-435 remove from `index()` context; lines 2395-2404 remove from `generate_report` context)
**Step 5.1: Update `core/templates/core/index.html`**
Find around line 187 (Quick Actions "Generate Report"):
```django
Generate Report
```
Replace with a plain link to `/report/` pre-loaded with the current month:
```django
{# Plain link: pills on the report page are the new-report interface. #}
Generate Report
```
Then find line 605 and delete the `{% include 'core/_report_config_modal.html' %}` line entirely.
**Step 5.2: Update `core/templates/core/report.html`**
Delete the header "New Report" button block (around lines 20-23):
```django
New Report
```
Delete the bottom "New Report" button block (around lines 387-389):
```django
New Report
```
Delete the include line (around line 400):
```django
{% include 'core/_report_config_modal.html' %}
```
(Also delete any comment block immediately above that include, if it only refers to the modal.)
**Step 5.3: Delete the modal template file**
```bash
git rm core/templates/core/_report_config_modal.html
```
**Step 5.4: Update `core/views.py` — remove `selected_*_ids` threading**
In `index()` (around line 434):
Find:
```python
'selected_project_ids': [],
'selected_team_ids': [],
```
Delete these two lines.
In `generate_report` (around line 2395-2404):
Find the whole block:
```python
# Pass projects and teams so the "New Report" modal's dropdowns can
# populate (same lists the Dashboard modal uses)
context['projects'] = Project.objects.all().order_by('name')
context['teams'] = Team.objects.all().order_by('name')
# For the modal's pre-selection: stringify the IDs so
# the template's `{% if p.id|stringformat:"s" in selected_project_ids %}`
# comparison works (Django templates compare strings to strings).
context['selected_project_ids'] = [str(p) for p in (project_ids or [])]
context['selected_team_ids'] = [str(t) for t in (team_ids or [])]
```
**Keep** the `projects` / `teams` context keys (the pills' popovers still use them to render `` lists). But **remove** the `selected_project_ids` / `selected_team_ids` stringify block — the JS reads those from `json_script` tags which the template still needs. Wait — the pills DO use `selected_project_ids` / `selected_team_ids` for pre-selection. So these stay!
Actually, re-read Task 2's markup: the popover ` ` tags use `{% if p.id|stringformat:"s" in selected_project_ids %} selected{% endif %}`, and the json_script tag emits `{{ selected_project_ids|json_script:"urlSelectedProjectIds" }}`. Both references are in Task 2's markup — so **these context keys stay**.
**Correction**: do NOT remove the `selected_project_ids` / `selected_team_ids` context keys. The inline-filters feature still uses them.
Only the **modal itself** is deleted; the data it consumed is still needed by the pill markup.
So Step 5.4 reduces to: **no change to `core/views.py`**. All backend context stays.
**Step 5.5: Run tests + manage.py check**
```bash
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 0 2>&1 | tail -5
USE_SQLITE=true DJANGO_DEBUG=true python manage.py check
```
Expected: `Ran 44 tests ... OK`; only the `node_modules` warning.
**Step 5.6: Verify no dangling references to the deleted modal**
```bash
grep -rn "reportConfigModal\|_report_config_modal" core/templates/ core/views.py
```
Expected: **zero hits**. If any remain, delete them.
**Step 5.7: Commit**
```bash
git add core/templates/core/index.html core/templates/core/report.html
git commit -m "$(cat <<'EOF'
Retire _report_config_modal.html — pills are the new-report interface
- Dashboard 'Generate Report' button: modal trigger -> plain link
to /report/?from_month=&to_month=
- Report page 'New Report' buttons (both header + bottom action bar):
deleted (the pills replace them)
- _report_config_modal.html: deleted
- No backend changes — selected_project_ids / selected_team_ids
context keys are still used by the pill popovers for pre-selection
(just not by the retired modal)
grep -rn 'reportConfigModal' core/templates/ core/views.py returns 0.
Co-Authored-By: Claude Opus 4.7 (1M context)
EOF
)"
```
---
## Task 6: Final QA + mark shipped
**Files:**
- Modify: `docs/plans/2026-04-23-inline-filters-design.md` (append "Shipped" block)
**Step 6.1: Manual QA matrix**
As admin, walk through:
| Flow | Expected |
|---|---|
| `/report/` no filters | Pills show current-month range + "All Projects" + "All Teams"; no Apply button; no × buttons |
| Click date pill → change month → OK → Apply | URL has `?from_month=...&to_month=...`; report re-renders |
| Click projects pill → pick 2 → OK → teams popover | Teams not linked to either project are disabled in the dropdown |
| Pick a team not linked to selected projects → OK | Team gets auto-removed with toast "Removed Team X — no logs on selected projects" |
| Reset button (when dirty) | All pills revert, Apply disappears |
| Esc key when popover open | Popover closes |
| Existing × buttons on pills | Still clear individual filters via full page reload |
| Click dashboard "Generate Report" card | Navigates directly to `/report/?from_month=&to_month=` — no modal |
| `grep -rn 'reportConfigModal' core/templates/` | Zero hits |
| Supervisor user hits `/report/` | 403 (unchanged) |
| PDF download with any filter | Mirrors current filter state (unchanged) |
| Phone viewport | Popovers dock to bottom of viewport full-width |
**Step 6.2: Full test run**
```bash
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 0 2>&1 | tail -5
```
Expected: `Ran 44 tests ... OK`.
**Step 6.3: Django system check + migrations dry-run**
```bash
USE_SQLITE=true DJANGO_DEBUG=true python manage.py check
USE_SQLITE=true DJANGO_DEBUG=true python manage.py makemigrations --dry-run
```
Expected: only `staticfiles.W004`; "No changes detected".
**Step 6.4: Append "Shipped" block to the design doc**
Append to `docs/plans/2026-04-23-inline-filters-design.md`:
```markdown
---
## Shipped — 23 Apr 2026
**Commits:** 30d0991 (design) through the Task 6 shipped commit.
**Plan:** `docs/plans/2026-04-23-inline-filters-plan.md`
**Tests:** 42 → 44 (2 new — InlineFiltersPairsContextTests covering
the `project_team_pairs_json` context key + NULL-team exclusion).
**QA outcome:** 44/44 tests pass. `manage.py check` clean.
`makemigrations --dry-run` reports no changes. All 10 manual QA
flows green. Cross-filter auto-remove + toast behaviour verified.
_report_config_modal.html fully retired; grep confirms no dangling
references.
**Deferred / out of scope (revisit if requested):**
- AJAX partial re-render (still full page reload on Apply)
- "Save this filter set" / named filter presets
- Permissive cross-filter variant (strict was chosen)
- Date-range-scoped cross-filter (entire history was chosen)
**Notable decisions made during implementation:**
- Modal `selected_project_ids` / `selected_team_ids` context keys
were KEPT (not removed as the design initially suggested) because
the pill popovers still use them for pre-selecting the Choices.js
`` tags and for URL-diff initialisation via json_script.
- Choices.js re-init on cross-filter change (destroy + reinstantiate)
is wasteful but the simplest way to reflect ` `
changes; acceptable at FoxFitt's scale (≤10 projects, ≤10 teams).
```
**Step 6.5: Commit**
```bash
git add docs/plans/2026-04-23-inline-filters-design.md
git commit -m "Docs: mark Inline Filters feature as shipped (23 Apr 2026)
Co-Authored-By: Claude Opus 4.7 (1M context) "
```
---
### 🛑 CHECKPOINT 2 — ship
Final demo: all pills work, modal gone, tests green, PDF still respects filters.
**On approval → single batched push** of all 6 commits (plus the two design-doc commits from earlier — 30d0991 inline-filters-design + 12edafa adjustments-tab-design). The adjustments-tab design doc rides along since it was committed locally in the same brainstorm session.
Gemini deploy prompt (CSS change → collectstatic mandatory):
```bash
git fetch github && git pull github ai-dev && \
python3 manage.py collectstatic --noinput && \
sudo systemctl restart django-dev.service
```
---
## Out of scope (not in this plan)
- Inline filter persistence across browser tabs (a URL-sharing improvement; URLs already bookmarkable)
- Filter-chip sort order preference
- Keyboard navigation between pills (Tab works via native button focus)
- Adjustments tab (separate plan after this ships)
---
Plan complete and saved to `docs/plans/2026-04-23-inline-filters-plan.md`. Two execution options:
**1. Subagent-Driven (this session)** — I dispatch a fresh subagent per task, review between tasks, fast iteration.
**2. Parallel Session (separate)** — Open a new Claude Code session with `superpowers:executing-plans`, batch execution.
**Which approach?**