# Inline Filters on Report Page — Design (23 Apr 2026) ## Goal Replace the modal-based filter form on `/report/` with inline, interactive filter pills so Konrad can tweak project/team/date filters without opening a modal every time. Adds cross-filter awareness: selecting a project hides teams that never worked on it (and vice versa). ## Origin Raised by Konrad at Checkpoint 3 of the Executive Report v2 work (just shipped): > _"I wonder if the generate should not rather have static filters instead of the popup open up for every report? Easier to add or remove a project or team or change dates instead of having to start from scratch."_ And the cross-filter request that came later: > _"Is it possible to filter out teams when selecting a project that has not worked on that project?"_ ## Who it's for **Admins** (`is_staff` or `is_superuser`). Supervisors keep no report access. ## Architecture at a glance - Every filter pill on the report page becomes a **clickable dropdown**. Click → popover opens directly under that pill → edit → click **OK** inside the popover → popover closes + pill shows dirty state + Apply button appears at the right end of the pill strip. - **Apply** submits to `/report/?` — full page reload, same URL scheme as today. No AJAX. - The Generate Report modal is **deleted**. The dashboard's "Generate Report" button becomes a plain link to `/report/?from_month=&to_month=`. - Cross-filter: a serialised map of `(project_id, team_id)` pairs from `WorkLog` is passed as JSON to the page. Popovers filter their options based on the other pill's current selection. - **Backend code touched: minimal** — one new context key (`project_team_pairs_json`) and removal of the now-unused `selected_project_ids` / `selected_team_ids` keys the modal needed. ## 1. Filter trigger — explicit Apply with dirty-state indicator Chosen over auto-apply-on-change because: - Admin users care about correctness; brief lag between "change filter" and "see numbers" is fine. - `_build_report_context` does ~10 aggregation queries; running it on every checkbox tick is wasteful. - Matches the rest of the codebase's pattern (Worker/Team/Project edit pages all use explicit Save). Refinement: Apply button is **hidden when filters are in sync with URL**, so it can't be "forgotten" when there are no pending changes. ## 2. Three interactive pills, one Apply action ``` ┌──────────────────────────┬──────────────────────────────────┬──────────────────────┬──────────┐ │ 📅 Mar 2026 – Apr 2026 ▾ │ 📁 Wilkot Boerdery + 1 more ▾ × │ 👥 All Teams ▾ │ [Apply] │ └──────────────────────────┴──────────────────────────────────┴──────────────────────┴──────────┘ ``` **Pill states**: - **Closed**: current value + ▾ chevron; cursor pointer; tooltip "Click to edit". - **Dirty** (has uncommitted changes): accent-orange outline + small pulsing dot; Apply button appears. - **Open**: popover expanded; pill background darker to indicate focus. **× button** on project and team pills: still clears that filter instantly (shortcut for "reset to All"). Only shown when a filter is active. ## 2.1 Date pill popover Click `📅 Mar 2026 – Apr 2026 ▾` → popover opens with same fields as the current modal: - Date Selection radio toggle: Month(s) / Custom Dates - From / To month pickers (when Month mode) - Start Date / End Date date pickers (when Custom mode) - Cancel / OK buttons Click OK → popover closes, pill updates to new range in dirty state. ## 2.2 Projects / Teams pill popover Click `📁 Wilkot Boerdery + 1 more ▾` → popover with a Choices.js multi-select (the same widget used in the just-retired modal): - Chip-style selected items at top; typing filters options. - Option list below the input (cross-filtered — see section 2.5). - Cancel / OK at the bottom. Click OK → popover closes, pill text updates. Pill display rules: - 0 selected: "All Projects" (or "All Teams") - 1 selected: show the name - 2 selected: show both comma-joined - 3+ selected: show first + "+ N more" ## 2.3 Apply button behaviour - **Hidden** when filters match the URL (no dirty state). - **Appears** when any pill is dirty. - **Click Apply** → submits via `window.location = '/report/?' + querystring`. Full page reload. - **Cancel** button appears alongside Apply when dirty → reverts all pills to URL-current values without submitting. - Browser back button works normally (each Apply = real URL change). ## 2.4 Generate Report button → plain link **Before (shipped)**: - Dashboard "Quick Actions" card → "Generate Report" button → modal → submit. - Report page "New Report" button → modal → submit. **After**: - Dashboard "Generate Report" card → `` link to `/report/?from_month={{ current_month }}&to_month={{ current_month }}`. One click → land on report with current month defaults → all filters are pills. - Report page "New Report" button → **deleted**. Pills are the new-report interface. **Files affected by retirement**: - `core/templates/core/_report_config_modal.html` — **delete** (no other callers). - `core/templates/core/index.html` — remove `{% include 'core/_report_config_modal.html' %}`; change "Generate Report" button to plain link. - `core/templates/core/report.html` — remove `{% include %}`; remove "New Report" button. - `core/views.py` — remove `context['selected_project_ids']` / `selected_team_ids` from `index()` and `generate_report` (only existed for the modal's pre-selection). ## 2.5 Cross-filter (project ↔ team) When a project is selected, the Teams popover shows only teams that have worked on at least one of the selected projects. Symmetric: selecting teams filters the Projects popover. **Semantics**: - **Union across selections**: 2 projects selected → teams that have worked on EITHER project appear. - **All (no cross-filter)**: if no project is selected, all teams appear in the Teams popover. Same for projects. - **Scope = entire history**: "has worked on" means "has at least one `WorkLog` on that project, ever". NOT filtered by the report's date range — date is about data shown; cross-filter is about data possible. **Auto-removal of invalid selections**: If you have Team Beta selected, then add Project Wilkot, and Beta has never worked on Wilkot → Beta is **auto-removed** from the team pill with a brief inline notice ("Team Beta removed — no logs on selected projects"). Toast-style, auto-dismisses after 4 seconds. **Data source — one new context key**: ```python # In generate_report view, serialise distinct (project_id, team_id) pairs as JSON. # The frontend uses this to filter dropdown options + auto-remove invalid selections. pairs = list( WorkLog.objects .filter(project__isnull=False, team__isnull=False) .values('project_id', 'team_id') .distinct() ) context['project_team_pairs_json'] = json.dumps(pairs) ``` Injected into the template via `{{ project_team_pairs_json|json_script:"projectTeamPairs" }}` (Django's safe-JSON pattern, already used for `team_workers_map_json` on the payroll dashboard per CLAUDE.md). **Frontend JS** (inside the pill-popover module): - When a popover opens, determine which options to hide based on the OTHER pill's current selection. - When a popover "OK" fires, diff the new selection against pairs; remove invalid entries from the other pill; show a toast. - Choices.js supports programmatically setting/hiding options via its API. **Edge cases**: - Team with zero work logs ever (e.g. just created) → invisible when any project is selected. Correct behaviour. - Deleted project/team references (`SET_NULL` in the DB) → already filtered out by the `filter(project__isnull=False, team__isnull=False)` clause. - If the URL specifies a team that isn't valid for the current project selection (shouldn't happen in practice, but if someone edits the URL) → the team still renders on the report's selected-period data (backend doesn't know about cross-filter); the pill shows it normally; next Apply will clean it up. ## 3. PDF stays identical The "Download PDF" button uses `?{{ query_string }}` — whatever filters are in the URL flow into the PDF. No changes to `generate_report_pdf` or the PDF template. ## 4. CSS — new rules In `static/css/custom.css`: - `.filter-pill--editable` — pointer cursor, hover tint, ▾ chevron alignment - `.filter-pill--dirty` — accent-orange outline, small pulsing dot (subtle) - `.filter-popover` — absolute-positioned below the pill, `--bg-card` + `--border-default` + shadow (same tokens as work log payroll modal) - `.filter-popover__footer` — right-aligned Cancel + OK buttons - `.apply-filters-btn` — primary button, slides in from right edge when dirty - `.filter-toast` — small accent-orange toast at the top of the report, auto-dismisses ~80 lines. ## 5. JS — one scoped module Inline `