mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-28 23:49:30 -06:00
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:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user