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>
This commit is contained in:
Konrad du Plessis 2026-04-23 13:30:56 +02:00
parent 0bbf2caae5
commit f6975bfb2f
4 changed files with 20 additions and 0 deletions

View File

@ -565,6 +565,7 @@
<tr>
<th>Project</th>
<th>Start</th>
<th>Last Activity</th>
<th class="r">Working Days</th>
<th class="r">Total Cost</th>
<th class="r">Avg R / Working Day</th>
@ -573,6 +574,7 @@
<tr>
<td class="name">{{ item.project }}</td>
<td>{% if item.start_date %}{{ item.start_date|date:"d M Y" }}{% else %}<span style="color:#cbd5e1;">&mdash;</span>{% endif %}</td>
<td>{% if item.last_activity %}{{ item.last_activity|date:"d M Y" }}{% else %}<span style="color:#cbd5e1;">&mdash;</span>{% endif %}</td>
<td class="r">{% if item.working_days %}{{ item.working_days }}{% else %}<span style="color:#cbd5e1;">&mdash;</span>{% endif %}</td>
<td class="total">R&nbsp;{{ item.total|money }}</td>
<td class="r">{% if item.working_days %}R&nbsp;{{ item.avg_per_working_day|money }}{% else %}<span style="color:#cbd5e1;">&mdash;</span>{% endif %}</td>

View File

@ -245,6 +245,7 @@
<tr>
<th>Project</th>
<th>Start</th>
<th>Last Activity</th>
<th class="text-end">Working Days</th>
<th class="text-end">Total Cost</th>
<th class="text-end">Avg R / Working Day</th>
@ -255,6 +256,7 @@
<tr>
<td class="fw-medium">{{ item.project }}</td>
<td>{% if item.start_date %}{{ item.start_date|date:"d M Y" }}{% else %}<span class="text-muted">&mdash;</span>{% endif %}</td>
<td>{% if item.last_activity %}{{ item.last_activity|date:"d M Y" }}{% else %}<span class="text-muted">&mdash;</span>{% endif %}</td>
<td class="text-end">{{ item.working_days|default:"—" }}</td>
<td class="text-end fw-semibold">R {{ item.total|money }}</td>
<td class="text-end">{% if item.working_days %}R {{ item.avg_per_working_day|money }}{% else %}<span class="text-muted">&mdash;</span>{% endif %}</td>

View File

@ -692,6 +692,12 @@ class ChapterOneEnrichmentTests(TestCase):
self.assertEqual(by_name['C1']['working_days'], 4)
self.assertEqual(by_name['C1']['avg_per_working_day'], Decimal('200.00'))
self.assertEqual(by_name['C1']['start_date'], datetime.date(2026, 1, 1))
# last_activity = the most recent WorkLog.date (4th of March here)
self.assertEqual(
by_name['C1']['last_activity'], datetime.date(2026, 3, 4),
'alltime_projects rows should expose the most-recent log date '
'so the report can show "Last Activity" per project'
)
# =============================================================================

View File

@ -2138,6 +2138,15 @@ def _build_report_context(start_date, end_date, project_ids=None, team_ids=None)
start_dates = dict(
Project.objects.values_list('name', 'start_date')
)
# Lookup the most recent WorkLog.date for each project (for the new
# "Last Activity" column — helps Konrad spot which projects are dormant
# without having to scroll through date pickers).
last_activity = dict(
all_time_logs.filter(project__isnull=False)
.values('project__name')
.annotate(last=Max('date'))
.values_list('project__name', 'last')
)
alltime_projects = []
for row in alltime_projects_raw:
name = row['project']
@ -2149,6 +2158,7 @@ def _build_report_context(start_date, end_date, project_ids=None, team_ids=None)
'worker_days': row['worker_days'],
'total': total,
'start_date': start_dates.get(name), # may be None
'last_activity': last_activity.get(name), # may be None
'working_days': wdays,
'avg_per_working_day': avg,
})