""" System Administrator routes """ from flask import Blueprint, render_template, request, redirect, url_for, flash, g, current_app from models import (db, Company, User, Role, Team, Project, TimeEntry, SystemSettings, SystemEvent, BrandingSettings, Task, SubTask, TaskDependency, Sprint, Comment, UserPreferences, UserDashboard, WorkConfig, CompanySettings, CompanyWorkConfig, ProjectCategory, Note, NoteFolder, NoteShare, Announcement, CompanyInvitation) from routes.auth import system_admin_required from flask import session from sqlalchemy import func from datetime import datetime, timedelta import logging import os logger = logging.getLogger(__name__) system_admin_bp = Blueprint('system_admin', __name__, url_prefix='/system-admin') @system_admin_bp.route('/dashboard') @system_admin_required def system_admin_dashboard(): """System Administrator Dashboard - view all data across companies""" # Global statistics 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() # System admin count system_admins = User.query.filter_by(role=Role.SYSTEM_ADMIN).count() regular_admins = User.query.filter_by(role=Role.ADMIN).count() # Recent activity (last 7 days) week_ago = datetime.now() - timedelta(days=7) recent_users = User.query.filter(User.created_at >= week_ago).count() recent_companies = Company.query.filter(Company.created_at >= week_ago).count() recent_time_entries = TimeEntry.query.filter(TimeEntry.arrival_time >= week_ago).count() # Top companies by user count top_companies = db.session.query( Company.name, Company.id, db.func.count(User.id).label('user_count') ).join(User).group_by(Company.id).order_by(db.func.count(User.id).desc()).limit(5).all() # Recent companies recent_companies_list = Company.query.order_by(Company.created_at.desc()).limit(5).all() # System health checks 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() return render_template('system_admin_dashboard.html', title='System Administrator Dashboard', total_companies=total_companies, total_users=total_users, total_teams=total_teams, total_projects=total_projects, total_time_entries=total_time_entries, system_admins=system_admins, regular_admins=regular_admins, recent_users=recent_users, recent_companies=recent_companies, recent_time_entries=recent_time_entries, top_companies=top_companies, recent_companies_list=recent_companies_list, orphaned_users=orphaned_users, orphaned_time_entries=orphaned_time_entries, blocked_users=blocked_users) @system_admin_bp.route('/companies') @system_admin_required def system_admin_companies(): """System admin view of all companies""" # Get filter parameters search_query = request.args.get('search', '') # Base query query = Company.query # Apply search filter if search_query: query = query.filter( db.or_( Company.name.ilike(f'%{search_query}%'), Company.slug.ilike(f'%{search_query}%') ) ) # Get all companies companies = query.order_by(Company.created_at.desc()).all() # Create a paginated response object from flask_sqlalchemy import Pagination page = request.args.get('page', 1, type=int) per_page = 20 # Paginate companies companies_paginated = query.paginate(page=page, per_page=per_page, error_out=False) # Calculate statistics for each company company_stats = {} for company in companies_paginated.items: company_stats[company.id] = { 'user_count': User.query.filter_by(company_id=company.id).count(), 'admin_count': User.query.filter_by(company_id=company.id, role=Role.ADMIN).count(), '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(), } return render_template('system_admin_companies.html', title='System Admin - Companies', companies=companies_paginated, company_stats=company_stats, search_query=search_query) @system_admin_bp.route('/companies/') @system_admin_required def system_admin_company_detail(company_id): """System admin detailed view of a specific company""" company = Company.query.get_or_404(company_id) # Get recent time entries count week_ago = datetime.now() - timedelta(days=7) recent_time_entries = TimeEntry.query.join(User).filter( User.company_id == company.id, TimeEntry.arrival_time >= week_ago ).count() # Get role distribution 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 # Get users list users = User.query.filter_by(company_id=company.id).order_by(User.created_at.desc()).all() # Get teams list teams = Team.query.filter_by(company_id=company.id).all() # Get projects list projects = Project.query.filter_by(company_id=company.id).order_by(Project.created_at.desc()).all() return render_template('system_admin_company_detail.html', title=f'Company Details - {company.name}', company=company, users=users, teams=teams, projects=projects, recent_time_entries=recent_time_entries, role_counts=role_counts) @system_admin_bp.route('/companies//delete', methods=['POST']) @system_admin_required def delete_company(company_id): """System Admin: Delete a company and all its data""" company = Company.query.get_or_404(company_id) company_name = company.name try: # Delete all related data in the correct order to avoid foreign key constraints # Delete comments (must be before tasks) Comment.query.filter(Comment.task_id.in_( db.session.query(Task.id).join(Project).filter(Project.company_id == company_id) )).delete(synchronize_session=False) # Delete subtasks SubTask.query.filter(SubTask.task_id.in_( db.session.query(Task.id).join(Project).filter(Project.company_id == company_id) )).delete(synchronize_session=False) # Delete task dependencies TaskDependency.query.filter( TaskDependency.blocked_task_id.in_( db.session.query(Task.id).join(Project).filter(Project.company_id == company_id) ) | TaskDependency.blocking_task_id.in_( db.session.query(Task.id).join(Project).filter(Project.company_id == company_id) ) ).delete(synchronize_session=False) # Delete tasks Task.query.filter(Task.project_id.in_( db.session.query(Project.id).filter(Project.company_id == company_id) )).delete(synchronize_session=False) # Delete sprints Sprint.query.filter(Sprint.project_id.in_( db.session.query(Project.id).filter(Project.company_id == company_id) )).delete(synchronize_session=False) # Delete time entries TimeEntry.query.filter(TimeEntry.user_id.in_( db.session.query(User.id).filter(User.company_id == company_id) )).delete(synchronize_session=False) # Delete projects Project.query.filter_by(company_id=company_id).delete() # Delete teams Team.query.filter_by(company_id=company_id).delete() # Delete user preferences, dashboards, and work configs UserPreferences.query.filter(UserPreferences.user_id.in_( db.session.query(User.id).filter(User.company_id == company_id) )).delete(synchronize_session=False) UserDashboard.query.filter(UserDashboard.user_id.in_( db.session.query(User.id).filter(User.company_id == company_id) )).delete(synchronize_session=False) WorkConfig.query.filter(WorkConfig.user_id.in_( db.session.query(User.id).filter(User.company_id == company_id) )).delete(synchronize_session=False) # Delete notes and note-related data user_ids_subquery = db.session.query(User.id).filter(User.company_id == company_id).subquery() # Delete note shares NoteShare.query.filter(NoteShare.created_by_id.in_(user_ids_subquery)).delete(synchronize_session=False) # Delete notes Note.query.filter(Note.created_by_id.in_(user_ids_subquery)).delete(synchronize_session=False) # Delete note folders NoteFolder.query.filter(NoteFolder.created_by_id.in_(user_ids_subquery)).delete(synchronize_session=False) # Delete announcements Announcement.query.filter(Announcement.created_by_id.in_(user_ids_subquery)).delete(synchronize_session=False) # Delete invitations CompanyInvitation.query.filter( (CompanyInvitation.invited_by_id.in_(user_ids_subquery)) | (CompanyInvitation.accepted_by_user_id.in_(user_ids_subquery)) ).delete(synchronize_session=False) # Delete system events associated with users from this company SystemEvent.query.filter(SystemEvent.user_id.in_(user_ids_subquery)).delete(synchronize_session=False) # Clear branding settings updated_by references BrandingSettings.query.filter(BrandingSettings.updated_by_id.in_(user_ids_subquery)).update( {BrandingSettings.updated_by_id: None}, synchronize_session=False) # Delete users User.query.filter_by(company_id=company_id).delete() # Delete company settings and work config CompanySettings.query.filter_by(company_id=company_id).delete() CompanyWorkConfig.query.filter_by(company_id=company_id).delete() # Delete project categories ProjectCategory.query.filter_by(company_id=company_id).delete() # Finally, delete the company db.session.delete(company) db.session.commit() flash(f'Company "{company_name}" and all its data have been permanently deleted.', 'success') logger.info(f"System admin {g.user.username} deleted company {company_name} (ID: {company_id})") except Exception as e: db.session.rollback() logger.error(f"Error deleting company {company_id}: {str(e)}") flash(f'Error deleting company: {str(e)}', 'error') return redirect(url_for('system_admin.system_admin_company_detail', company_id=company_id)) return redirect(url_for('system_admin.system_admin_companies')) @system_admin_bp.route('/time-entries') @system_admin_required def system_admin_time_entries(): """System Admin: View time entries across all companies""" page = request.args.get('page', 1, type=int) company_filter = request.args.get('company', '') per_page = 50 # Build query properly with explicit joins query = db.session.query( TimeEntry, User.username, Company.name.label('company_name'), Project.name.label('project_name') ).join( User, TimeEntry.user_id == User.id ).join( Company, User.company_id == Company.id ).outerjoin( Project, TimeEntry.project_id == Project.id ) # Apply company filter if company_filter: query = query.filter(Company.id == company_filter) # Order by arrival time (newest first) query = query.order_by(TimeEntry.arrival_time.desc()) # Paginate entries = query.paginate(page=page, per_page=per_page, error_out=False) # Get companies for filter dropdown companies = Company.query.order_by(Company.name).all() # Get today's date for the template today = datetime.now().date() return render_template('system_admin_time_entries.html', title='System Admin - Time Entries', entries=entries, companies=companies, current_company=company_filter, today=today) @system_admin_bp.route('/settings', methods=['GET', 'POST']) @system_admin_required def system_admin_settings(): """System Admin: Global system settings""" if request.method == 'POST': # Update system settings registration_enabled = request.form.get('registration_enabled') == 'on' email_verification = request.form.get('email_verification_required') == 'on' tracking_script_enabled = request.form.get('tracking_script_enabled') == 'on' tracking_script_code = request.form.get('tracking_script_code', '') # Update or create settings reg_setting = SystemSettings.query.filter_by(key='registration_enabled').first() if reg_setting: reg_setting.value = 'true' if registration_enabled else 'false' else: reg_setting = SystemSettings( key='registration_enabled', value='true' if registration_enabled else 'false', description='Controls whether new user registration is allowed' ) db.session.add(reg_setting) email_setting = SystemSettings.query.filter_by(key='email_verification_required').first() if email_setting: email_setting.value = 'true' if email_verification else 'false' else: email_setting = SystemSettings( key='email_verification_required', value='true' if email_verification else 'false', description='Controls whether email verification is required for new accounts' ) db.session.add(email_setting) tracking_enabled_setting = SystemSettings.query.filter_by(key='tracking_script_enabled').first() if tracking_enabled_setting: tracking_enabled_setting.value = 'true' if tracking_script_enabled else 'false' else: tracking_enabled_setting = SystemSettings( key='tracking_script_enabled', value='true' if tracking_script_enabled else 'false', description='Controls whether custom tracking script is enabled' ) db.session.add(tracking_enabled_setting) tracking_code_setting = SystemSettings.query.filter_by(key='tracking_script_code').first() if tracking_code_setting: tracking_code_setting.value = tracking_script_code else: tracking_code_setting = SystemSettings( key='tracking_script_code', value=tracking_script_code, description='Custom tracking script code (HTML/JavaScript)' ) db.session.add(tracking_code_setting) db.session.commit() flash('System settings updated successfully.', 'success') return redirect(url_for('system_admin.system_admin_settings')) # Get current settings settings = {} all_settings = SystemSettings.query.all() for setting in all_settings: if setting.key == 'registration_enabled': settings['registration_enabled'] = setting.value == 'true' elif setting.key == 'email_verification_required': settings['email_verification_required'] = setting.value == 'true' elif setting.key == 'tracking_script_enabled': settings['tracking_script_enabled'] = setting.value == 'true' elif setting.key == 'tracking_script_code': settings['tracking_script_code'] = setting.value # System statistics total_companies = Company.query.count() total_users = User.query.count() total_system_admins = User.query.filter_by(role=Role.SYSTEM_ADMIN).count() return render_template('system_admin_settings.html', title='System Administrator Settings', settings=settings, total_companies=total_companies, total_users=total_users, total_system_admins=total_system_admins) @system_admin_bp.route('/health') @system_admin_required def system_admin_health(): """System Admin: System health check and event log""" # Get system health summary health_summary = SystemEvent.get_system_health_summary() # Get recent events (last 7 days) recent_events = SystemEvent.get_recent_events(days=7, limit=100) # Get events by severity for quick stats errors = SystemEvent.get_events_by_severity('error', days=7, limit=20) warnings = SystemEvent.get_events_by_severity('warning', days=7, limit=20) # System metrics now = datetime.now() # Database connection test db_healthy = True db_error = None try: db.session.execute('SELECT 1') except Exception as e: db_healthy = False db_error = str(e) SystemEvent.log_event( 'database_check_failed', f'Database health check failed: {str(e)}', 'system', 'error' ) # Application uptime (approximate based on first event) first_event = SystemEvent.query.order_by(SystemEvent.timestamp.asc()).first() uptime_start = first_event.timestamp if first_event else now uptime_duration = now - uptime_start # Recent activity stats today = now.date() today_events = SystemEvent.query.filter( func.date(SystemEvent.timestamp) == today ).count() # Log the health check SystemEvent.log_event( 'system_health_check', f'System health check performed by {session.get("username", "unknown")}', 'system', 'info', user_id=session.get('user_id'), ip_address=request.remote_addr, user_agent=request.headers.get('User-Agent') ) return render_template('system_admin_health.html', title='System Health Check', health_summary=health_summary, recent_events=recent_events, errors=errors, warnings=warnings, db_healthy=db_healthy, db_error=db_error, uptime_duration=uptime_duration, today_events=today_events) @system_admin_bp.route('/branding', methods=['GET', 'POST']) @system_admin_required def branding(): """System Admin: Branding settings""" if request.method == 'POST': branding = BrandingSettings.get_current() # Handle form data branding.app_name = request.form.get('app_name', g.branding.app_name).strip() branding.logo_alt_text = request.form.get('logo_alt_text', '').strip() branding.primary_color = request.form.get('primary_color', '#007bff').strip() # Handle imprint settings branding.imprint_enabled = 'imprint_enabled' in request.form branding.imprint_title = request.form.get('imprint_title', 'Imprint').strip() branding.imprint_content = request.form.get('imprint_content', '').strip() branding.updated_by_id = g.user.id # Handle logo upload if 'logo_file' in request.files: logo_file = request.files['logo_file'] if logo_file and logo_file.filename: # Create uploads directory if it doesn't exist upload_dir = os.path.join(current_app.static_folder, 'uploads', 'branding') os.makedirs(upload_dir, exist_ok=True) # Save the file with a timestamp to avoid conflicts import time filename = f"logo_{int(time.time())}_{logo_file.filename}" logo_path = os.path.join(upload_dir, filename) logo_file.save(logo_path) branding.logo_filename = filename # Handle favicon upload if 'favicon_file' in request.files: favicon_file = request.files['favicon_file'] if favicon_file and favicon_file.filename: # Create uploads directory if it doesn't exist upload_dir = os.path.join(current_app.static_folder, 'uploads', 'branding') os.makedirs(upload_dir, exist_ok=True) # Save the file with a timestamp to avoid conflicts import time filename = f"favicon_{int(time.time())}_{favicon_file.filename}" favicon_path = os.path.join(upload_dir, filename) favicon_file.save(favicon_path) branding.favicon_filename = filename db.session.commit() flash('Branding settings updated successfully.', 'success') return redirect(url_for('system_admin.branding')) # Get current branding settings branding = BrandingSettings.get_current() return render_template('system_admin_branding.html', title='System Administrator - Branding Settings', branding=branding)