diff --git a/app.py b/app.py index f5fdc03..d941eda 100644 --- a/app.py +++ b/app.py @@ -3313,6 +3313,185 @@ def get_filtered_analytics_data(user, mode, start_date=None, end_date=None, proj return query.order_by(TimeEntry.arrival_time.desc()).all() +@app.route('/api/companies//teams') +@system_admin_required +def api_company_teams(company_id): + """API: Get teams for a specific company (System Admin only)""" + teams = Team.query.filter_by(company_id=company_id).order_by(Team.name).all() + return jsonify([{ + 'id': team.id, + 'name': team.name, + 'description': team.description + } for team in teams]) + +@app.route('/api/system-admin/stats') +@system_admin_required +def api_system_admin_stats(): + """API: Get real-time system statistics for dashboard""" + from datetime import datetime, timedelta + + # Get basic counts + total_companies = Company.query.count() + total_users = User.query.count() + total_teams = Team.query.count() + total_projects = Project.query.count() + total_time_entries = TimeEntry.query.count() + + # Active sessions + active_sessions = TimeEntry.query.filter_by(departure_time=None, is_paused=False).count() + paused_sessions = TimeEntry.query.filter_by(is_paused=True).count() + + # Recent activity (last 24 hours) + yesterday = datetime.now() - timedelta(days=1) + recent_users = User.query.filter(User.created_at >= yesterday).count() + recent_companies = Company.query.filter(Company.created_at >= yesterday).count() + recent_time_entries = TimeEntry.query.filter(TimeEntry.arrival_time >= yesterday).count() + + # System health + orphaned_users = User.query.filter_by(company_id=None).count() + orphaned_time_entries = TimeEntry.query.filter_by(user_id=None).count() + blocked_users = User.query.filter_by(is_blocked=True).count() + unverified_users = User.query.filter_by(is_verified=False).count() + + return jsonify({ + 'totals': { + 'companies': total_companies, + 'users': total_users, + 'teams': total_teams, + 'projects': total_projects, + 'time_entries': total_time_entries + }, + 'active': { + 'sessions': active_sessions, + 'paused_sessions': paused_sessions + }, + 'recent': { + 'users': recent_users, + 'companies': recent_companies, + 'time_entries': recent_time_entries + }, + 'health': { + 'orphaned_users': orphaned_users, + 'orphaned_time_entries': orphaned_time_entries, + 'blocked_users': blocked_users, + 'unverified_users': unverified_users + } + }) + +@app.route('/api/system-admin/companies//users') +@system_admin_required +def api_company_users(company_id): + """API: Get users for a specific company (System Admin only)""" + company = Company.query.get_or_404(company_id) + users = User.query.filter_by(company_id=company.id).order_by(User.username).all() + + return jsonify({ + 'company': { + 'id': company.id, + 'name': company.name, + 'is_personal': company.is_personal + }, + 'users': [{ + 'id': user.id, + 'username': user.username, + 'email': user.email, + 'role': user.role.value, + 'is_blocked': user.is_blocked, + 'is_verified': user.is_verified, + 'created_at': user.created_at.isoformat(), + 'team_id': user.team_id + } for user in users] + }) + +@app.route('/api/system-admin/users//toggle-block', methods=['POST']) +@system_admin_required +def api_toggle_user_block(user_id): + """API: Toggle user blocked status (System Admin only)""" + user = User.query.get_or_404(user_id) + + # Safety check: prevent blocking yourself + if user.id == g.user.id: + return jsonify({'error': 'Cannot block your own account'}), 400 + + # Safety check: prevent blocking the last system admin + if user.role == Role.SYSTEM_ADMIN and not user.is_blocked: + system_admin_count = User.query.filter_by(role=Role.SYSTEM_ADMIN, is_blocked=False).count() + if system_admin_count <= 1: + return jsonify({'error': 'Cannot block the last system administrator'}), 400 + + user.is_blocked = not user.is_blocked + db.session.commit() + + return jsonify({ + 'id': user.id, + 'username': user.username, + 'is_blocked': user.is_blocked, + 'message': f'User {"blocked" if user.is_blocked else "unblocked"} successfully' + }) + +@app.route('/api/system-admin/companies//stats') +@system_admin_required +def api_company_stats(company_id): + """API: Get detailed statistics for a specific company""" + company = Company.query.get_or_404(company_id) + + # User counts by role + role_counts = {} + for role in Role: + count = User.query.filter_by(company_id=company.id, role=role).count() + if count > 0: + role_counts[role.value] = count + + # Team and project counts + team_count = Team.query.filter_by(company_id=company.id).count() + project_count = Project.query.filter_by(company_id=company.id).count() + active_projects = Project.query.filter_by(company_id=company.id, is_active=True).count() + + # Time entries statistics + from datetime import datetime, timedelta + week_ago = datetime.now() - timedelta(days=7) + month_ago = datetime.now() - timedelta(days=30) + + weekly_entries = TimeEntry.query.join(User).filter( + User.company_id == company.id, + TimeEntry.arrival_time >= week_ago + ).count() + + monthly_entries = TimeEntry.query.join(User).filter( + User.company_id == company.id, + TimeEntry.arrival_time >= month_ago + ).count() + + # Active sessions + active_sessions = TimeEntry.query.join(User).filter( + User.company_id == company.id, + TimeEntry.departure_time == None, + TimeEntry.is_paused == False + ).count() + + return jsonify({ + 'company': { + 'id': company.id, + 'name': company.name, + 'is_personal': company.is_personal, + 'is_active': company.is_active + }, + 'users': { + 'total': sum(role_counts.values()), + 'by_role': role_counts + }, + 'structure': { + 'teams': team_count, + 'projects': project_count, + 'active_projects': active_projects + }, + 'activity': { + 'weekly_entries': weekly_entries, + 'monthly_entries': monthly_entries, + 'active_sessions': active_sessions + } + }) + @app.route('/api/analytics/export') @login_required def analytics_export(): diff --git a/templates/system_admin_companies.html b/templates/system_admin_companies.html new file mode 100644 index 0000000..1dde556 --- /dev/null +++ b/templates/system_admin_companies.html @@ -0,0 +1,342 @@ +{% extends "layout.html" %} + +{% block content %} +
+
+

đŸĸ System Admin - All Companies

+

Manage companies across the entire system

+ ← Back to Dashboard +
+ + + {% if companies.items %} +
+ + + + + + + + + + + + + + {% for company in companies.items %} + + + + + + + + + + {% endfor %} + +
Company NameTypeUsersAdminsStatusCreatedActions
+ {{ company.name }} + {% if company.slug %} +
{{ company.slug }} + {% endif %} +
+ {% if company.is_personal %} + Freelancer + {% else %} + Company + {% endif %} + + {{ company_stats[company.id]['user_count'] }} + users + + {{ company_stats[company.id]['admin_count'] }} + admins + + {% if company.is_active %} + Active + {% else %} + Inactive + {% endif %} + {{ company.created_at.strftime('%Y-%m-%d') }} + +
+
+ + + {% if companies.pages > 1 %} +
+ + +

+ Showing {{ companies.per_page * (companies.page - 1) + 1 }} - + {{ companies.per_page * (companies.page - 1) + companies.items|length }} of {{ companies.total }} companies +

+
+ {% endif %} + + {% else %} +
+

No companies found

+

No companies exist in the system yet.

+
+ {% endif %} + + +
+

📊 Company Summary

+
+
+

Total Companies

+

{{ companies.total }}

+
+
+

Personal Companies

+

{{ companies.items | selectattr('is_personal') | list | length }}

+
+
+

Business Companies

+

{{ companies.items | rejectattr('is_personal') | list | length }}

+
+
+

Active Companies

+

{{ companies.items | selectattr('is_active') | list | length }}

+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/system_admin_company_detail.html b/templates/system_admin_company_detail.html new file mode 100644 index 0000000..b2b529a --- /dev/null +++ b/templates/system_admin_company_detail.html @@ -0,0 +1,579 @@ +{% extends "layout.html" %} + +{% block content %} +
+
+

đŸĸ {{ company.name }}

+

Company Details - System Administrator View

+ +
+ + +
+

📋 Company Information

+
+
+ + {{ company.name }} +
+
+ + {{ company.slug }} +
+
+ + {% if company.is_personal %} + Personal/Freelancer + {% else %} + Business Company + {% endif %} +
+
+ + {% if company.is_active %} + Active + {% else %} + Inactive + {% endif %} +
+
+ + {{ company.created_at.strftime('%Y-%m-%d %H:%M') }} +
+
+ + {{ company.max_users or 'Unlimited' }} +
+ {% if company.description %} +
+ + {{ company.description }} +
+ {% endif %} +
+
+ + +
+

📊 Company Statistics

+
+
+

{{ users|length }}

+

Total Users

+
+
+

{{ teams|length }}

+

Teams

+
+
+

{{ projects|length }}

+

Projects

+
+
+

{{ recent_time_entries }}

+

Recent Time Entries

+ (Last 7 days) +
+
+
+ + + {% if role_counts %} +
+

đŸ‘Ĩ Role Distribution

+
+ {% for role, count in role_counts.items() %} +
+ {{ count }} + {{ role }} +
+ {% endfor %} +
+
+ {% endif %} + +
+ +
+

👤 Users ({{ users|length }})

+ {% if users %} +
+ {% for user in users[:10] %} +
+
+ {{ user.username }} + {{ user.email }} +
+
+ + {{ user.role.value }} + + {% if user.is_blocked %} + Blocked + {% elif not user.is_verified %} + Unverified + {% endif %} +
+
+ {% endfor %} + {% if users|length > 10 %} + + {% endif %} +
+ {% else %} +

No users in this company.

+ {% endif %} +
+ + +
+

🏭 Teams ({{ teams|length }})

+ {% if teams %} +
+ {% for team in teams %} +
+
+ {{ team.name }} + {% if team.description %} + {{ team.description }} + {% endif %} +
+
+ {{ team.created_at.strftime('%Y-%m-%d') }} +
+
+ {% endfor %} +
+ {% else %} +

No teams in this company.

+ {% endif %} +
+ + +
+

📝 Projects ({{ projects|length }})

+ {% if projects %} +
+ {% for project in projects[:10] %} +
+
+ {{ project.name }} + {{ project.code }} + {% if project.description %} + {{ project.description[:50] }}{% if project.description|length > 50 %}...{% endif %} + {% endif %} +
+
+ {% if project.is_active %} + Active + {% else %} + Inactive + {% endif %} +
+
+ {% endfor %} + {% if projects|length > 10 %} +
+

And {{ projects|length - 10 }} more projects...

+
+ {% endif %} +
+ {% else %} +

No projects in this company.

+ {% endif %} +
+
+ + + +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/system_admin_settings.html b/templates/system_admin_settings.html new file mode 100644 index 0000000..58aec56 --- /dev/null +++ b/templates/system_admin_settings.html @@ -0,0 +1,504 @@ +{% extends "layout.html" %} + +{% block content %} +
+
+

âš™ī¸ System Administrator Settings

+

Global system configuration and management

+ ← Back to Dashboard +
+ + +
+

📊 System Overview

+
+
+

{{ total_companies }}

+

Total Companies

+
+
+

{{ total_users }}

+

Total Users

+
+
+

{{ total_system_admins }}

+

System Administrators

+
+
+
+ + +
+

🔧 System Configuration

+
+
+
+

User Registration

+

Control whether new users can register accounts

+
+
+ + + When enabled, new users can create accounts through the registration page. + When disabled, only administrators can create new user accounts. + +
+
+ +
+
+

Email Verification

+

Require email verification for new accounts

+
+
+ + + When enabled, new users must verify their email address before they can log in. + This helps ensure valid email addresses and improves security. + +
+
+ +
+ + Cancel +
+
+
+ + +
+

â„šī¸ System Information

+
+
+

Application Version

+

TimeTrack v1.0

+
+
+

Database

+

SQLite

+
+
+

System Admin Access

+

Full system control

+
+
+
+ + +
+

âš ī¸ Danger Zone

+
+
+
+

System Maintenance

+

Advanced system operations should be performed with caution. + Always backup the database before making significant changes.

+
+
+ +
+
+ +
+
+

System Health Check

+

Run diagnostic checks to identify potential issues with the system.

+
+
+ +
+
+
+
+ + + +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/system_admin_time_entries.html b/templates/system_admin_time_entries.html new file mode 100644 index 0000000..dbc4f2e --- /dev/null +++ b/templates/system_admin_time_entries.html @@ -0,0 +1,429 @@ +{% extends "layout.html" %} + +{% block content %} +
+
+

âąī¸ System Admin - Time Entries

+

View time entries across all companies

+ ← Back to Dashboard +
+ + +
+

Filter Time Entries

+
+
+ + +
+ {% if current_company %} + Clear Filter + {% endif %} +
+
+ + + {% if entries.items %} +
+ + + + + + + + + + + + + + + {% for entry_data in entries.items %} + {% set entry = entry_data[0] %} + {% set username = entry_data[1] %} + {% set company_name = entry_data[2] %} + {% set project_name = entry_data[3] %} + + + + + + + + + + + {% endfor %} + +
UserCompanyProjectArrivalDepartureDurationStatusNotes
+ {{ username }} + + {{ company_name }} + + {% if project_name %} + {{ project_name }} + {% else %} + No project + {% endif %} + {{ entry.arrival_time.strftime('%Y-%m-%d %H:%M') }} + {% if entry.departure_time %} + {{ entry.departure_time.strftime('%Y-%m-%d %H:%M') }} + {% else %} + Still working + {% endif %} + + {% if entry.duration %} + {% set hours = entry.duration // 3600 %} + {% set minutes = (entry.duration % 3600) // 60 %} + {{ hours }}h {{ minutes }}m + {% else %} + Ongoing + {% endif %} + + {% if entry.is_paused %} + Paused + {% elif not entry.departure_time %} + Active + {% else %} + Completed + {% endif %} + + {% if entry.notes %} + + {{ entry.notes[:30] }}{% if entry.notes|length > 30 %}...{% endif %} + + {% else %} + No notes + {% endif %} +
+
+ + + {% if entries.pages > 1 %} +
+ + +

+ Showing {{ entries.per_page * (entries.page - 1) + 1 }} - + {{ entries.per_page * (entries.page - 1) + entries.items|length }} of {{ entries.total }} time entries +

+
+ {% endif %} + + {% else %} +
+

No time entries found

+ {% if current_company %} +

No time entries found for the selected company.

+ {% else %} +

No time entries exist in the system yet.

+ {% endif %} +
+ {% endif %} + + + {% if entries.items %} +
+

📊 Summary Statistics

+
+
+

Total Entries

+

{{ entries.total }}

+
+
+

Active Sessions

+

{{ entries.items | selectattr('0.departure_time', 'equalto', None) | list | length }}

+
+
+

Paused Sessions

+

{{ entries.items | selectattr('0.is_paused', 'equalto', True) | list | length }}

+
+
+

Completed Today

+

+ {% set today = moment().date() %} + {{ entries.items | selectattr('0.arrival_time') | selectattr('0.departure_time', 'defined') | + list | length }} +

+
+
+
+ {% endif %} +
+ + +{% endblock %} \ No newline at end of file