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>
This commit is contained in:
parent
89c42d25a3
commit
fe85c9d7fd
@ -333,6 +333,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# === CHAPTER IV — Team × Project Activity === #}
|
||||||
|
<h5 class="chapter-heading mb-3"><span class="chapter-num">IV</span>Team × Project Activity</h5>
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 fw-bold"><i class="fas fa-th me-2" style="color: var(--accent);"></i>Distinct Work Days per Team × Project</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if team_project_activity.rows %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm mb-0 report-numeric">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Team</th>
|
||||||
|
{% for col in team_project_activity.columns %}
|
||||||
|
<th class="text-end">{{ col.name }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
<th class="text-end fw-bold">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in team_project_activity.rows %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-medium">{{ row.team_name }}</td>
|
||||||
|
{% for col in team_project_activity.columns %}
|
||||||
|
<td class="text-end">
|
||||||
|
{% with days=row.cells_by_project_id|dictlookup:col.id %}
|
||||||
|
{% if days %}{{ days }}{% else %}<span class="text-muted">—</span>{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
<td class="text-end fw-semibold">{{ row.row_total }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr class="table-total-row">
|
||||||
|
<td class="fw-bold">Total</td>
|
||||||
|
{% for col in team_project_activity.columns %}
|
||||||
|
<td class="text-end fw-bold">{{ team_project_activity.col_totals|dictlookup:col.id }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
<td class="text-end fw-bold">{{ team_project_activity.grand_total }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}<p class="text-muted text-center py-3 mb-0">No team × project activity in this period.</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Bottom Action Bar -->
|
<!-- Bottom Action Bar -->
|
||||||
<div class="d-flex justify-content-between align-items-center d-print-none">
|
<div class="d-flex justify-content-between align-items-center d-print-none">
|
||||||
<a href="{% url 'home' %}" class="btn btn-outline-secondary"><i class="fas fa-arrow-left me-1"></i>Back to Dashboard</a>
|
<a href="{% url 'home' %}" class="btn btn-outline-secondary"><i class="fas fa-arrow-left me-1"></i>Back to Dashboard</a>
|
||||||
|
|||||||
@ -25,3 +25,20 @@ def money(value):
|
|||||||
# Python's :, format gives comma separators — swap commas for spaces
|
# Python's :, format gives comma separators — swap commas for spaces
|
||||||
formatted = f"{num:,.2f}"
|
formatted = f"{num:,.2f}"
|
||||||
return formatted.replace(",", " ")
|
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
|
||||||
|
|||||||
@ -1721,3 +1721,15 @@ body, .card, .modal-content, .form-control, .form-select,
|
|||||||
.report-numeric th {
|
.report-numeric th {
|
||||||
font-variant-numeric: tabular-nums;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user