diff --git a/docs/plans/2026-04-23-inline-filters-design.md b/docs/plans/2026-04-23-inline-filters-design.md new file mode 100644 index 0000000..09519d3 --- /dev/null +++ b/docs/plans/2026-04-23-inline-filters-design.md @@ -0,0 +1,237 @@ +# 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 `