From fe85c9d7fd0233e54036eb539e5fce6927d4cd31 Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Wed, 22 Apr 2026 23:51:46 +0200 Subject: [PATCH] =?UTF-8?q?Report:=20Chapter=20IV=20=E2=80=94=20Team=20?= =?UTF-8?q?=C3=97=20Project=20Activity=20pivot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- core/templates/core/report.html | 47 ++++++++++++++++++++++++++++++++ core/templatetags/format_tags.py | 17 ++++++++++++ static/css/custom.css | 12 ++++++++ 3 files changed, 76 insertions(+) diff --git a/core/templates/core/report.html b/core/templates/core/report.html index 743af73..d58a9d7 100644 --- a/core/templates/core/report.html +++ b/core/templates/core/report.html @@ -333,6 +333,53 @@ +{# === CHAPTER IV — Team × Project Activity === #} +
IVTeam × Project Activity
+
+
+
Distinct Work Days per Team × Project
+
+
+ {% if team_project_activity.rows %} +
+ + + + + {% for col in team_project_activity.columns %} + + {% endfor %} + + + + + {% for row in team_project_activity.rows %} + + + {% for col in team_project_activity.columns %} + + {% endfor %} + + + {% endfor %} + + + {% for col in team_project_activity.columns %} + + {% endfor %} + + + +
Team{{ col.name }}Total
{{ row.team_name }} + {% with days=row.cells_by_project_id|dictlookup:col.id %} + {% if days %}{{ days }}{% else %}{% endif %} + {% endwith %} + {{ row.row_total }}
Total{{ team_project_activity.col_totals|dictlookup:col.id }}{{ team_project_activity.grand_total }}
+
+ {% else %}

No team × project activity in this period.

{% endif %} +
+
+
Back to Dashboard diff --git a/core/templatetags/format_tags.py b/core/templatetags/format_tags.py index c80d202..5c2ec9b 100644 --- a/core/templatetags/format_tags.py +++ b/core/templatetags/format_tags.py @@ -25,3 +25,20 @@ def money(value): # Python's :, format gives comma separators — swap commas for spaces formatted = f"{num:,.2f}" return formatted.replace(",", " ") + + +@register.filter +def dictlookup(d, key): + """Look up a dict value by a dynamic key. + + Plain-English: in Django templates, `{{ mydict.foo }}` only works when + `foo` is a literal key. For `mydict[col.id]` with a variable key you need + a filter — this one. Returns None if the key is missing or the input + isn't a dict. + """ + if d is None: + return None + try: + return d.get(key) + except (AttributeError, TypeError): + return None diff --git a/static/css/custom.css b/static/css/custom.css index 93cfcc6..f103bf9 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1721,3 +1721,15 @@ body, .card, .modal-content, .form-control, .form-select, .report-numeric th { font-variant-numeric: tabular-nums; } + +/* === Pivot-table footer totals (Chapter IV) === */ +/* + Bold, slightly-lifted row at the bottom of the Team × Project pivot + that holds the column totals + grand total. The 2px top border + visually separates totals from the data rows; the inset background + is the same --bg-inset used by other "slightly raised" surfaces. +*/ +.table-total-row td { + border-top: 2px solid var(--border-default) !important; + background: var(--bg-inset); +}