diff --git a/app/routes/admin.py b/app/routes/admin.py index 4baa2b98..7d49d4f5 100644 --- a/app/routes/admin.py +++ b/app/routes/admin.py @@ -551,6 +551,8 @@ def get_upload_folder(): def admin_dashboard(): """Admin dashboard""" from app.config import Config + from datetime import datetime, timedelta + from sqlalchemy import func, case # Get system statistics total_users = User.query.count() @@ -584,6 +586,73 @@ def admin_dashboard(): # Log error but continue - OIDC user count is not critical for dashboard display current_app.logger.warning(f"Failed to count OIDC users: {e}", exc_info=True) + # Calculate chart data for last 30 days + from datetime import time as time_class + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=30) + + # User activity over time (daily user counts who created entries) + user_activity_data = [] + for i in range(30): + date = (end_date - timedelta(days=i)).date() + date_start = datetime.combine(date, time_class(0, 0, 0)) + date_end = datetime.combine(date, time_class(23, 59, 59, 999999)) + + # Count distinct users who logged time on this date + user_count = ( + db.session.query(func.count(func.distinct(TimeEntry.user_id))) + .filter( + TimeEntry.end_time.isnot(None), + TimeEntry.start_time >= date_start, + TimeEntry.start_time <= date_end + ) + .scalar() or 0 + ) + + user_activity_data.append({ + 'date': date.strftime('%Y-%m-%d'), + 'count': user_count + }) + + user_activity_data.reverse() # Oldest to newest + + # Project status distribution + project_status_data = {} + status_counts = ( + db.session.query(Project.status, func.count(Project.id)) + .group_by(Project.status) + .all() + ) + for status, count in status_counts: + project_status_data[status or 'none'] = count + + # Daily time entry hours for last 30 days + time_entries_daily = [] + for i in range(30): + date = (end_date - timedelta(days=i)).date() + date_start = datetime.combine(date, time_class(0, 0, 0)) + date_end = datetime.combine(date, time_class(23, 59, 59, 999999)) + + # Get total hours for this day + total_seconds = ( + db.session.query(func.sum(TimeEntry.duration_seconds)) + .filter( + TimeEntry.end_time.isnot(None), + TimeEntry.start_time >= date_start, + TimeEntry.start_time <= date_end + ) + .scalar() or 0 + ) + + hours = round(total_seconds / 3600, 2) if total_seconds else 0 + + time_entries_daily.append({ + 'date': date.strftime('%Y-%m-%d'), + 'hours': hours + }) + + time_entries_daily.reverse() # Oldest to newest + # Build stats object expected by the template stats = { "total_users": total_users, @@ -591,11 +660,19 @@ def admin_dashboard(): "total_projects": total_projects, "active_projects": active_projects, "total_entries": total_entries, + "active_timers": active_timers, "total_hours": TimeEntry.get_total_hours_for_period(), "billable_hours": TimeEntry.get_total_hours_for_period(billable_only=True), "last_backup": None, } + # Chart data + chart_data = { + 'user_activity': user_activity_data, + 'project_status': project_status_data, + 'time_entries_daily': time_entries_daily, + } + return render_template( "admin/dashboard.html", stats=stats, @@ -605,6 +682,7 @@ def admin_dashboard(): oidc_configured=oidc_configured, oidc_auth_method=auth_method, oidc_users_count=oidc_users_count, + chart_data=chart_data, ) diff --git a/app/templates/admin/dashboard.html b/app/templates/admin/dashboard.html index b7a54c9d..086f64eb 100644 --- a/app/templates/admin/dashboard.html +++ b/app/templates/admin/dashboard.html @@ -1,6 +1,5 @@ {% extends "base.html" %} -{% from "components/cards.html" import info_card %} -{% from "components/ui.html" import page_header, breadcrumb_nav, button, filter_badge %} +{% from "components/ui.html" import page_header, breadcrumb_nav, button, filter_badge, empty_state %} {% block content %} {% set breadcrumbs = [ @@ -15,100 +14,517 @@ actions_html=None ) }} +
- {{ info_card("Total Users", stats.total_users, "All time") }} - {{ info_card("Active Users", stats.active_users, "Currently active") }} - {{ info_card("Total Projects", stats.total_projects, "All time") }} - {{ info_card("Active Projects", stats.active_projects, "Currently active") }} + +
+
+
+
+

{{ _('Total Users') }}

+

{{ stats.total_users }}

+

{{ _('All time') }}

+
+
+ +
+
+
+ + +
+
+
+
+

{{ _('Active Users') }}

+

{{ stats.active_users }}

+

{{ _('Currently active') }}

+
+
+ +
+
+
+ + +
+
+
+
+

{{ _('Total Projects') }}

+

{{ stats.total_projects }}

+

{{ _('All time') }}

+
+
+ +
+
+
+ + +
+
+
+
+

{{ _('Active Projects') }}

+

{{ stats.active_projects }}

+

{{ _('Currently active') }}

+
+
+ +
+
+
+ + +
+
+
+
+

{{ _('Total Entries') }}

+

{{ stats.total_entries }}

+

{{ _('Time entries logged') }}

+
+
+ +
+
+
+ + +
+
+
+
+

{{ _('Active Timers') }}

+

{{ stats.active_timers }}

+

{{ _('Currently running') }}

+
+
+ +
+
+
+ + +
+
+
+
+

{{ _('Total Hours') }}

+

{{ "%.1f"|format(stats.total_hours / 3600 if stats.total_hours else 0) }}

+

{{ _('All time tracked') }}

+
+
+ +
+
+
+ + +
+
+
+
+

{{ _('Billable Hours') }}

+

{{ "%.1f"|format(stats.billable_hours / 3600 if stats.billable_hours else 0) }}

+

{{ _('Billable time') }}

+
+
+ +
+
+
+ +
+ +
+

+ + {{ _('User Activity (30 Days)') }} +

+
+ +
+
+ + +
+

+ + {{ _('Project Status') }} +

+
+ +
+
+ + +
+

+ + {{ _('Time Entry Trends (30 Days)') }} +

+
+ +
+
+
+ +
-

{{ _('Admin Sections') }}

+

+ + {{ _('System Health') }} +

+
+ +
+
+ +
+
+
{{ _('Database') }}
+
{{ _('Connected') }}
+
+
+ + +
+
+ +
+
+
{{ _('OIDC Authentication') }}
+
+ {% if oidc_configured %}{{ _('Configured') }}{% else %}{{ _('Not Configured') }}{% endif %} +
+
+
+ + + {% if oidc_enabled %} +
+
+ +
+
+
{{ _('OIDC Users') }}
+
{{ oidc_users_count }} {{ _('users') }}
+
+
+ {% endif %} +
+
+ + +
+

+ + {{ _('Admin Sections') }} +

- - -
Manage Users
+ +
+
+ +
Manage Users
- - -
Roles & Permissions
+ + +
+
+ +
Roles & Permissions
- - -
API Tokens
-
REST API Access
+ + +
+
+ +
API Tokens
+
REST API Access
- - -
Webhooks
-
Outgoing Events
+ + +
+
+ +
Webhooks
+
Outgoing Events
- - -
Integrations
-
OAuth Setup
+ + +
+
+ +
Integrations
+
OAuth Setup
- - -
Email Configuration
-
Test & Configure
+ + +
+
+ +
Email Configuration
+
Test & Configure
- - -
Email Templates
-
Invoice Email Templates
+ + +
+
+ +
Email Templates
+
Invoice Email Templates
- - -
Settings
+ + +
+
+ +
Settings
- - -
System Info
+ + +
+
+ +
System Info
- - -
Custom Fields
-
Global Field Definitions
+ + +
+
+ +
Custom Fields
+
Global Field Definitions
- - -
Link Templates
-
URL Templates for Fields
+ + +
+
+ +
Link Templates
+
URL Templates for Fields
- - -
Salesman Email Mappings
-
Report Distribution
+ + +
+
+ +
Salesman Email Mappings
+
Report Distribution
+
-

{{ _('Recent Activity') }}

- - - - - - - - - - - {% for entry in recent_entries %} - - - - - - - {% else %} - - - - {% endfor %} - -
UserProjectDurationDate
{{ entry.user.username }}{{ entry.project.name }}{{ entry.duration }}{{ entry.start_time|user_date('%Y-%m-%d') }}
No recent activity.
+

+ + {{ _('Recent Activity') }} +

+ {% if recent_entries %} +
+ + + + + + + + + + + {% for entry in recent_entries %} + + + + + + + {% endfor %} + +
{{ _('User') }}{{ _('Project') }}{{ _('Duration') }}{{ _('Date') }}
+
+ + {{ entry.user.username }} +
+
+
+ + {{ entry.project.name if entry.project else _('N/A') }} +
+
+
+ + {{ entry.duration }} +
+
+
+ + {{ entry.start_time|user_date('%Y-%m-%d %H:%M') }} +
+
+
+ {% else %} + {{ empty_state('fas fa-inbox', 'No Recent Activity', 'There are no recent time entries to display.', None, type='no-data') }} + {% endif %}
+ +{% endblock %} + +{% block scripts_extra %} + + {% endblock %}