15 Commits

Author SHA1 Message Date
Konrad du Plessis
c26d2e07d0 fix(report): auto-open Choices dropdown + make date hint readable
Two small Checkpoint-1 polish items from Konrad:

(1) 'The projects and team dropdowns open empty, and only after you
    click in the text box do the options appear.'

    Choices.js's default open state has the dropdown closed (is-active
    absent) — the user normally has to click the search input to reveal
    options. But a pill click clearly means 'show me the list,' so we
    now call `showDropdown(true)` on the Choices instance right after
    the popover opens. Deferred via setTimeout(0) so it runs AFTER any
    cross-filter destroy/recreate has settled in the same tick.

(2) 'In the date selection I accidentally saw there is text that is way
    too dark below the From date selector — "Leave blank for a single
    month".'

    The inline style was `opacity: 0.75` on top of Bootstrap's default
    `.form-text` colour (inherits --bs-secondary-color — very dark on
    our dark theme). Replaced with `color: var(--text-tertiary)` + full
    opacity so the hint is readable in both dark and light themes.
    This matches CLAUDE.md's convention of always using theme tokens
    for text that should sit in the 'hint' legibility band.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:35:20 +02:00
Konrad du Plessis
f6975bfb2f feat(report): 'Last Activity' column in All Time Projects table
Konrad's Checkpoint-1 feedback:
  'Inside the all time projects table, can we have a column with the
  last transaction date for a project? It will make it easier to find
  data for projects. It is nice to have the filter, but you can still
  skip around looking for when the last transaction was.'

Added a 'last_activity' entry to each alltime_projects row in
_build_report_context — computed as max(WorkLog.date) grouped by
project name (respects the same project_ids/team_ids filters already
applied to all_time_logs). Rendered in both the on-screen report
(report.html) and the PDF (report_pdf.html) as a new 'Last Activity'
column sitting between 'Start' and 'Working Days'.

Existing ChapterOneEnrichmentTests extended with a last_activity
assertion locking in the 'most recent log date' semantics.

No other tests touched. 47/47 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:30:56 +02:00
Konrad du Plessis
1d00a3a68f refactor(report): retire the Generate Report modal (Task 5)
Per the plan at docs/plans/2026-04-23-inline-filters-plan.md Task 5, the
now-redundant configuration modal goes away:

core/templates/core/_report_config_modal.html → deleted (160 lines)
core/templates/core/index.html:
  - Dashboard 'Generate Report' tile → plain link to
    /report/?from_month={% now 'Y-m' %}&to_month={% now 'Y-m' %} so the
    click lands on the report page with the current month pre-filled.
  - Modal {% include %} at EOF removed.
core/templates/core/report.html:
  - Both 'New Report' buttons (header + bottom action bar) deleted;
    comments updated to say the pills ARE the new-report interface.
  - {% include 'core/_report_config_modal.html' %} removed.
  - Stale 'Task 5 will delete...' comment on the Choices.js CDN block
    updated.

Konrad's exact ask (Checkpoint 1 feedback):
  'Does it make sense to have this popup window for reports? Don't you
  think clicking on generate report should just default to current month
  and open the report page where users can adjust report filters?'
  → Yes. The pills do exactly that, one click in.

Verification:
  grep -rn 'reportConfigModal\|_report_config_modal' core/ returns 0 hits.
  47/47 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:27:21 +02:00
Konrad du Plessis
c1937cd89d style(report): shrink '(optional)' hint and tooltip 'Single month select' on Until
Checkpoint-1 polish (Konrad):
  - 'From (optional)' — the parenthetical is now ~50% smaller (0.6rem)
    so the label's primary text dominates, matching his intent of a
    subordinate hint rather than a competing word.
  - 'Until' — adds a small info-circle icon with a Bootstrap tooltip
    reading 'Single month select'. Inline small-font text was my first
    attempt but wrapped to two lines inside the narrow column; the icon
    tooltip keeps the label tidy while the hint is one hover away.
    Bootstrap tooltip auto-init (base.html) handles the binding —
    matches CLAUDE.md's global tooltip pattern.

No functional change. 47/47 tests still green (no view code touched).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:12:45 +02:00
Konrad du Plessis
3fa3cdcf35 style(report): swap date picker columns to 'From (optional) | Until'
Konrad's clarification on the Checkpoint-1 UX revision: the visual order
should follow English reading — "from X until Y" reads left-to-right, so
"From" belongs on the left and "Until" on the right. Previous commit
71f8558 placed Until on the left because it's the always-filled anchor,
but that fights the natural sentence order and was confusing.

Optionality is unchanged:
  - Until (right, always filled) = anchor month
  - From (left, optional) = blank means single-month report

No JS change needed — input IDs (popoverFromMonth / popoverToMonth) stay
the same; only column positions in the <div class="row"> were swapped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:07:39 +02:00
Konrad du Plessis
71f8558ff5 feat(report): Until-primary date picker + date-scoped project/team lists
Checkpoint 1 second-round UX feedback (Konrad, 2026-04-23):

(1) "The until option must be auto filled (and used for single month) and
    the from date must be optional — this makes more sense and less clicks
    if the user wants to eg check the last 3 months."

    → Inverted the month pickers. "Until" is now the always-filled anchor
    (defaults to URL to_month, falling back to the current YYYY-MM when no
    filter is set). "From (optional)" is the disclosure; blank = single
    month (JS submits from_month = to_month). Visual order swapped so
    Until sits on the left as the primary action. Matches the admin mental
    model: "I want data ending now, maybe going back N months."

(2) "Is it possible to show only teams and projects that has transactions
    within the selected dates — filter out teams and projects that has no
    log for any of the dates chosen?"

    → The pill pickers AND the cross-filter (project_team_pairs_json) are
    now scoped to the current date range. A team/project with zero logs in
    the window doesn't clutter the lists. The (project_id, team_id) pair
    map follows the same rule — cross-filter disables options that never
    paired inside THIS window.

    Guarantee: entries that are currently in the URL's ?project= / ?team=
    selection are always unioned back in, so the user's own picks can
    never disappear from the list even when they'd otherwise be out of
    scope (e.g. picking a project, then narrowing the date range to a
    period with no logs on that project).

Design-doc note at lines 108-112 of 2026-04-23-inline-filters-design.md
originally said "Scope = entire history" — Konrad's real-usage feedback
overrides that decision. Will be recorded in the Task 6 "Shipped" block.

Tests: two new ones lock in the behaviour —
  - test_pickers_and_pairs_are_date_scoped: out-of-range project/team
    absent from both the picker lists and the pair map
  - test_url_selected_projects_survive_even_out_of_range: URL selection
    unioned in regardless of date window
Plus existing 3 tests still green. 47/47 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:25:45 +02:00
Konrad du Plessis
ffb3ef6800 refactor(report): auto-submit on OK + sticky footer + optional until-month
Checkpoint 1 UX feedback (Konrad, 2026-04-23) surfaced three friction points
that all traced back to the same over-engineered "multi-stage commit" model:

1. When Choices.js opened its dropdown, it covered the popover's OK button.
   User had to click in a thin strip "outside the multi-select but inside
   the dropdown pane" to close Choices.js before OK became reachable.

2. Changing only a project/team didn't light up the global Apply button
   (dirty-state diff bug on multi-selects), and even when it did, clicking
   Apply didn't actually update the report tables. Also the Apply button
   sat at the far right of the pill strip — easy to miss on desktop.

3. Single-month reports required changing BOTH From and To pickers; for a
   low-frequency admin tool, that's a tax on the most common flow.

Instead of patching three bugs, collapsed the entire pending/dirty/Apply
model. Each popover's OK now:
  - Rebuilds the URL from its OWN inputs only (keeping other filters intact)
  - Navigates → full SSR page reload → report re-renders
The user reads the result of their change immediately; there's no "did I
remember to click Apply?" step.

Side-effect wins:
  - 'dirty state', 'pending state', 'updateAllPillsDirty', 'revert...',
    cross-filter auto-removal, and the toast system all become unnecessary.
    Net -187 lines across template + CSS.
  - The bug from (2) self-disappears because there's no dirty-diff step.
  - Sticky popover footer (position: sticky; bottom: 0; z-index: 2) pins
    OK to the popover edge even when Choices.js expands — solves (1).
  - The To month picker is labelled "Until (optional)" with "Leave blank
    for a single month" hint. Blank on submit → to_month = from_month.
    Single-month URLs round-trip with a blank To input (so the form and
    the data agree).

Cross-filter preserved: on popover open, the OTHER pill's URL selection
still disables invalid dropdown options. Just no runtime auto-remove —
unnecessary because the next OK submits and the server takes over.

Tested in the browser via preview MCP:
  - All three pills open popovers on click
  - Range URL shows both month pickers filled
  - Single-month URL shows To blank
  - OK with blank To → navigates to from_month=X&to_month=X
  - Sticky footer keeps OK in viewport when Choices.js is open
  - 45/45 tests still pass (no backend contract change)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:48:53 +02:00
Konrad du Plessis
6d2c72f6d1 JS: pill-popover interactive module + Choices.js CDN in report.html
Main interactive layer for the inline-filters feature. Appends two
blocks to report.html (inside {% block content %}, before the final
{% endblock %}):

1. Choices.js CDN <link> + <script> (admin-only gated, SRI-hashed) —
   moved here because Task 5 will delete _report_config_modal.html,
   which previously loaded the CDN. Keeping this on the report page
   directly means the pills stay functional after modal retirement.

2. A 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 the orange outline + pulsing dot; Apply button slides in
   - 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 Work-Log Payroll feature.

Graceful fallback: if Choices.js CDN fails to load, the module bails
early with a console warning; native <select multiple> still works
inside the popovers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:56:13 +02:00
Konrad du Plessis
acbad1558e Template: interactive filter-pill markup + popover shells
Replaces the three static filter pills with clickable buttons and
inline popover shells below each one. Popovers remain hidden by
default (hidden attribute) — the JS module in Task 4 will wire up
open/close, dirty state, and Apply behaviour.

Structure per pill:
- .filter-pill-wrap (position-relative container)
  - <button class="filter-pill filter-pill--editable" data-filter="...">
    with chevron indicating clickability
  - <a class="filter-pill__x"> (existing × clear-filter link, preserved)
  - .filter-popover (the editable widget — date picker for the Date
    pill, Choices.js multi-select for Projects/Teams pills)

Apply + Reset buttons sit in .apply-filters-group at the right end,
initially hidden. A <div id="filter-toast-container"> is pre-placed
for the cross-filter auto-removal notices.

Three json_script blocks embed the data the JS needs:
- projectTeamPairs: (project_id, team_id) pairs for cross-filter
- urlSelectedProjectIds / urlSelectedTeamIds: current URL state for
  dirty diffing + reset

No visible behaviour change yet (no CSS, no JS). Page renders same
as before until Tasks 3-4 light it up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:41:46 +02:00
Konrad du Plessis
fe85c9d7fd Report: Chapter IV — Team × Project Activity pivot
Final chapter of the executive redesign. Renders the
team_project_activity context as a pivot: rows=teams, columns=projects,
cell=COUNT(DISTINCT work-log dates). Zero cells show em-dashes in muted
grey (not '0') so non-zero cells stand out. Row totals, column totals,
and grand total on the bottom row.

Adds a tiny dictlookup template filter (format_tags.py) — Django
templates can't index a dict by a dynamic variable key, and the pivot
cell lookup is cells_by_project_id[col.id]. Defensive None + TypeError
guards so a malformed context can't 500 the page.

.table-total-row CSS: 2px top border + inset background for the footer
row so totals visually separate from the data rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:51:46 +02:00
Konrad du Plessis
89c42d25a3 Report: Chapter III heading + tabular-nums on worker breakdown table
Adds the numbered 'III' chapter heading above the existing Worker
Breakdown card (the widest table in the report). Promotes the table
to .report-numeric (tabular-nums) for perfect column alignment
across dynamic adjustment columns — Inter's tabular-nums variant
keeps the rand amounts pixel-aligned.

No data or structural changes to the breakdown itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:47:44 +02:00
Konrad du Plessis
68c9afd939 Report: Chapter I (lifetime context) + chapter numbering
Replaces the old narrow four-card All-Time/YTD row (dropped in Task 9)
with two wider cards under a numbered 'Chapter I - Lifetime Context'
heading. Projects card gains Start, Working Days, Total Cost, and
Avg R / Working Day columns per the design. Teams card keeps name +
total.

Adds .chapter-heading and .chapter-num CSS for the orange numbered
markers (I, II, III, IV) and .report-numeric class that applies
tabular-nums across the number columns of every report table.

Renames the existing 'Selected Period' heading to Chapter II.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:43:14 +02:00
Konrad du Plessis
9632214f99 Report: hero KPI band (4 cards) replacing All-Time/YTD row
Chapter 0 of the executive redesign: four large cards at the top
showing Paid This Period, Outstanding Now (live, stamped with the
generation time), FoxFitt Avg/Day, and FoxFitt Avg/Month.

Drops the old four-small-cards All-Time/YTD row (YTD specifically
documented as redundant per design doc section 3). All-Time detail
moves into Chapter I in the next task.

New .stat-card--hero variant uses Poppins 1.85rem for the number,
uppercase tracked labels, subtle tertiary sub-lines. tabular-nums
keeps the R-amounts pixel-aligned across cards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:37:39 +02:00
Konrad du Plessis
ea481bfbf4 Report: filter-pill strip with × to clear individual filters
Three pills under the header: date range, project(s), team(s). Shows
comma-joined names when multi-valued (project_name in context is
already a comma-joined string from Task 6). × buttons on the project
and team pills remove just that filter via a rebuilt querystring;
the calendar pill has no × (date range is required).

Helper context keys query_string_without_project / _without_team do
the rebuild in the view via QueryDict.setlist so multi-value keys
are properly stripped (pop() only removes the first occurrence).

Pill CSS uses existing design tokens (--bg-inset, --accent,
--text-primary, --border-default, --text-tertiary, --bg-card-hover)
so dark and light themes work without overrides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:14:48 +02:00
Konrad du Plessis
3c28387dd3 WIP: 2026-04-22 session checkpoint
Complete working state of the session. Will be split into two deploy
phases (safety scaffolding then feature release) before merging to ai-dev.

Includes:
- Security fixes (email creds / SECRET_KEY / DEBUG / CSRF)
- Backup + restore management commands and browser endpoints
- WeasyPrint migration (replaces xhtml2pdf)
- New Worker fields + WorkerCertificate + WorkerWarning models
- Worker / Team / Project friendly management UIs
- Dashboard cert-expiry card + Manage All buttons
- Bootstrap tooltips (global init + theme-aware CSS)
- Django admin template override (taller M2M pickers)
- Money filter for ZAR currency formatting
- Resources dropdown nav
- Massive CLAUDE.md expansion + deploy plan docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:19:15 +02:00