feat(clients): Add recent hours history to client detail page

Add a "Recent Hours History" section to the client view page that displays
the last 20 time entries for the client. This provides users with quick
visibility into recent work performed for each client.

Changes:
- Update view_client route to fetch recent time entries (directly linked
  to client and through client's projects)
- Add eager loading for user, project, and task relationships to optimize
  query performance
- Display time entries in a table format with date, project, task, user,
  duration, and notes
- Include summary showing total entries and total hours
- Filter to only show completed entries (exclude active timers)

The history section appears below the projects list on the client detail
page, maintaining consistency with the existing UI design and providing
immediate context about recent work activity.
This commit is contained in:
Dries Peeters
2025-11-30 11:00:03 +01:00
parent ac465d9612
commit 1e7d8cf575
2 changed files with 106 additions and 2 deletions

View File

@@ -3,13 +3,15 @@ from flask_babel import gettext as _
from flask_login import login_required, current_user
import app as app_module
from app import db
from app.models import Client, Project, Contact
from datetime import datetime
from app.models import Client, Project, Contact, TimeEntry
from datetime import datetime, timedelta
from decimal import Decimal, InvalidOperation
from app.utils.db import safe_commit
from app.utils.permissions import admin_or_permission_required
from app.utils.timezone import convert_app_datetime_to_user
from app.utils.email import send_client_portal_password_setup_email
from sqlalchemy.orm import joinedload
from sqlalchemy import or_
import csv
import io
import json
@@ -274,6 +276,30 @@ def view_client(client_id):
# Get rendered links from link templates
rendered_links = client.get_rendered_links()
# Get recent time entries for this client
# Include entries directly linked to client and entries through projects
project_ids = [p.id for p in projects]
# Query time entries: either directly linked to client or through client's projects
conditions = [TimeEntry.client_id == client.id] # Direct client entries
if project_ids:
conditions.append(TimeEntry.project_id.in_(project_ids)) # Project entries
time_entries_query = TimeEntry.query.filter(
TimeEntry.end_time.isnot(None) # Only completed entries
).filter(
or_(*conditions)
).options(
joinedload(TimeEntry.user),
joinedload(TimeEntry.project),
joinedload(TimeEntry.task)
).order_by(
TimeEntry.start_time.desc()
).limit(20) # Limit to most recent 20 entries
recent_time_entries = time_entries_query.all()
return render_template(
"clients/view.html",
client=client,
@@ -282,6 +308,7 @@ def view_client(client_id):
primary_contact=primary_contact,
prepaid_overview=prepaid_overview,
rendered_links=rendered_links,
recent_time_entries=recent_time_entries,
)

View File

@@ -221,6 +221,83 @@
</table>
</div>
</div>
<!-- Recent Hours History -->
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow mt-6">
<h2 class="text-lg font-semibold mb-4">{{ _('Recent Hours History') }}</h2>
{% if recent_time_entries %}
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead class="border-b border-border-light dark:border-border-dark">
<tr>
<th class="p-3 text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Date') }}</th>
<th class="p-3 text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Project') }}</th>
<th class="p-3 text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Task') }}</th>
<th class="p-3 text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('User') }}</th>
<th class="p-3 text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Duration') }}</th>
<th class="p-3 text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Notes') }}</th>
</tr>
</thead>
<tbody>
{% for entry in recent_time_entries %}
<tr class="border-b border-border-light dark:border-border-dark hover:bg-background-light dark:hover:bg-background-dark">
<td class="p-3">
<div class="text-sm">
{{ entry.start_time|user_datetime('%Y-%m-%d') }}
</div>
<div class="text-xs text-text-muted-light dark:text-text-muted-dark">
{{ entry.start_time|user_datetime('%H:%M') }}
{% if entry.end_time %}
- {{ entry.end_time|user_datetime('%H:%M') }}
{% endif %}
</div>
</td>
<td class="p-3">
{% if entry.project %}
<a href="{{ url_for('projects.view_project', project_id=entry.project.id) }}" class="text-primary hover:underline text-sm">
{{ entry.project.name }}
</a>
{% else %}
<span class="text-sm text-text-muted-light dark:text-text-muted-dark">{{ _('Direct') }}</span>
{% endif %}
</td>
<td class="p-3">
{% if entry.task %}
<span class="text-sm">{{ entry.task.name }}</span>
{% else %}
<span class="text-sm text-text-muted-light dark:text-text-muted-dark">-</span>
{% endif %}
</td>
<td class="p-3">
<span class="text-sm">{{ entry.user.display_name if entry.user else _('N/A') }}</span>
</td>
<td class="p-3">
<span class="font-medium text-sm">{{ "%.2f"|format(entry.duration_hours) }}h</span>
</td>
<td class="p-3">
{% if entry.notes %}
<span class="text-sm" title="{{ entry.notes }}">
{{ entry.notes[:50] }}{% if entry.notes|length > 50 %}...{% endif %}
</span>
{% else %}
<span class="text-sm text-text-muted-light dark:text-text-muted-dark">-</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="mt-4 pt-4 border-t border-border-light dark:border-border-dark">
<p class="text-sm text-text-muted-light dark:text-text-muted-dark">
{{ _('Showing last %(count)s entries', count=recent_time_entries|length) }} |
{{ _('Total hours') }}: {{ "%.2f"|format(recent_time_entries|sum(attribute='duration_hours')) }}h
</p>
</div>
</div>
{% else %}
<p class="text-text-muted-light dark:text-text-muted-dark text-center py-8">{{ _('No recent time entries found.') }}</p>
{% endif %}
</div>
</div>
</div>