From 1e7d8cf5756d7d263f60199061ef7aa7f606c485 Mon Sep 17 00:00:00 2001 From: Dries Peeters Date: Sun, 30 Nov 2025 11:00:03 +0100 Subject: [PATCH] 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. --- app/routes/clients.py | 31 ++++++++++++- app/templates/clients/view.html | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/app/routes/clients.py b/app/routes/clients.py index 2578978..b7453fb 100644 --- a/app/routes/clients.py +++ b/app/routes/clients.py @@ -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, ) diff --git a/app/templates/clients/view.html b/app/templates/clients/view.html index a9318ff..65295c2 100644 --- a/app/templates/clients/view.html +++ b/app/templates/clients/view.html @@ -221,6 +221,83 @@ + + +
+

{{ _('Recent Hours History') }}

+ {% if recent_time_entries %} +
+ + + + + + + + + + + + + {% for entry in recent_time_entries %} + + + + + + + + + {% endfor %} + +
{{ _('Date') }}{{ _('Project') }}{{ _('Task') }}{{ _('User') }}{{ _('Duration') }}{{ _('Notes') }}
+
+ {{ entry.start_time|user_datetime('%Y-%m-%d') }} +
+
+ {{ entry.start_time|user_datetime('%H:%M') }} + {% if entry.end_time %} + - {{ entry.end_time|user_datetime('%H:%M') }} + {% endif %} +
+
+ {% if entry.project %} + + {{ entry.project.name }} + + {% else %} + {{ _('Direct') }} + {% endif %} + + {% if entry.task %} + {{ entry.task.name }} + {% else %} + - + {% endif %} + + {{ entry.user.display_name if entry.user else _('N/A') }} + + {{ "%.2f"|format(entry.duration_hours) }}h + + {% if entry.notes %} + + {{ entry.notes[:50] }}{% if entry.notes|length > 50 %}...{% endif %} + + {% else %} + - + {% endif %} +
+
+

+ {{ _('Showing last %(count)s entries', count=recent_time_entries|length) }} | + {{ _('Total hours') }}: {{ "%.2f"|format(recent_time_entries|sum(attribute='duration_hours')) }}h +

+
+
+ {% else %} +

{{ _('No recent time entries found.') }}

+ {% endif %} +