diff --git a/app.py b/app.py index 9bb570e..d9adc9d 100644 --- a/app.py +++ b/app.py @@ -40,6 +40,7 @@ from routes.system_admin import system_admin_bp from routes.announcements import announcements_bp from routes.export import export_bp from routes.export_api import export_api_bp +from routes.organization import organization_bp # Import auth decorators from routes.auth from routes.auth import login_required, admin_required, system_admin_required, role_required, company_required @@ -105,6 +106,7 @@ app.register_blueprint(system_admin_bp) app.register_blueprint(announcements_bp) app.register_blueprint(export_bp) app.register_blueprint(export_api_bp) +app.register_blueprint(organization_bp) # Import and register invitations blueprint from routes.invitations import invitations_bp diff --git a/routes/company.py b/routes/company.py index ab49cc2..73f0d68 100644 --- a/routes/company.py +++ b/routes/company.py @@ -218,27 +218,8 @@ def admin_company(): @admin_required @company_required def company_users(): - """List all users in the company with detailed information""" - users = User.query.filter_by(company_id=g.company.id).order_by(User.created_at.desc()).all() - - # Calculate user statistics - user_stats = { - 'total': len(users), - 'verified': len([u for u in users if u.is_verified]), - 'unverified': len([u for u in users if not u.is_verified]), - 'blocked': len([u for u in users if u.is_blocked]), - 'active': len([u for u in users if not u.is_blocked and u.is_verified]), - 'admins': len([u for u in users if u.role == Role.ADMIN]), - 'supervisors': len([u for u in users if u.role == Role.SUPERVISOR]), - 'team_leaders': len([u for u in users if u.role == Role.TEAM_LEADER]), - 'team_members': len([u for u in users if u.role == Role.TEAM_MEMBER]), - } - - return render_template('company_users.html', - title='Company Users', - company=g.company, - users=users, - stats=user_stats) + """Redirect to the unified organization management page""" + return redirect(url_for('organization.admin_organization')) # Setup company route (separate from company blueprint due to different URL) diff --git a/routes/organization.py b/routes/organization.py new file mode 100644 index 0000000..badad01 --- /dev/null +++ b/routes/organization.py @@ -0,0 +1,231 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, g +from models import db, User, Team, Role, Company +from routes.auth import login_required, admin_required, company_required +from sqlalchemy import or_ + +# Create the blueprint +organization_bp = Blueprint('organization', __name__) + +@organization_bp.route('/admin/organization') +@login_required +@company_required +@admin_required +def admin_organization(): + """Comprehensive organization management interface""" + company = g.user.company + + # Get all teams and users for the company + teams = Team.query.filter_by(company_id=company.id).order_by(Team.name).all() + users = User.query.filter_by(company_id=company.id).order_by(User.username).all() + + return render_template('admin_organization.html', + title='Organization Management', + teams=teams, + users=users, + Role=Role) + +@organization_bp.route('/api/organization/teams/', methods=['GET', 'PUT', 'DELETE']) +@login_required +@company_required +@admin_required +def api_team(team_id): + """API endpoint for team operations""" + team = Team.query.filter_by(id=team_id, company_id=g.user.company_id).first_or_404() + + if request.method == 'GET': + return jsonify({ + 'id': team.id, + 'name': team.name, + 'description': team.description, + 'members': [{'id': u.id, 'username': u.username} for u in team.users] + }) + + elif request.method == 'PUT': + data = request.get_json() + team.name = data.get('name', team.name) + team.description = data.get('description', team.description) + + try: + db.session.commit() + return jsonify({'success': True, 'message': 'Team updated successfully'}) + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': str(e)}), 400 + + elif request.method == 'DELETE': + # Unassign all users from the team + for user in team.users: + user.team_id = None + + db.session.delete(team) + db.session.commit() + return jsonify({'success': True, 'message': 'Team deleted successfully'}) + +@organization_bp.route('/api/organization/teams', methods=['POST']) +@login_required +@company_required +@admin_required +def api_create_team(): + """API endpoint to create a new team""" + data = request.get_json() + + team = Team( + name=data.get('name'), + description=data.get('description'), + company_id=g.user.company_id + ) + + try: + db.session.add(team) + db.session.commit() + return jsonify({'success': True, 'message': 'Team created successfully', 'team_id': team.id}) + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': str(e)}), 400 + +@organization_bp.route('/api/organization/users/', methods=['GET', 'PUT', 'DELETE']) +@login_required +@company_required +@admin_required +def api_user(user_id): + """API endpoint for user operations""" + user = User.query.filter_by(id=user_id, company_id=g.user.company_id).first_or_404() + + if request.method == 'GET': + return jsonify({ + 'id': user.id, + 'username': user.username, + 'email': user.email, + 'role': user.role.name if user.role else 'TEAM_MEMBER', + 'team_id': user.team_id, + 'is_blocked': user.is_blocked + }) + + elif request.method == 'PUT': + data = request.get_json() + + # Update user fields + if 'email' in data: + user.email = data['email'] + if 'role' in data: + user.role = Role[data['role']] + if 'team_id' in data: + user.team_id = data['team_id'] if data['team_id'] else None + if 'is_blocked' in data: + user.is_blocked = data['is_blocked'] + if 'password' in data and data['password']: + user.set_password(data['password']) + + try: + db.session.commit() + return jsonify({'success': True, 'message': 'User updated successfully'}) + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': str(e)}), 400 + + elif request.method == 'DELETE': + if user.id == g.user.id: + return jsonify({'success': False, 'message': 'Cannot delete your own account'}), 400 + + db.session.delete(user) + db.session.commit() + return jsonify({'success': True, 'message': 'User deleted successfully'}) + +@organization_bp.route('/api/organization/users', methods=['POST']) +@login_required +@company_required +@admin_required +def api_create_user(): + """API endpoint to create a new user""" + data = request.get_json() + + # Check if username already exists + if User.query.filter_by(username=data.get('username')).first(): + return jsonify({'success': False, 'message': 'Username already exists'}), 400 + + user = User( + username=data.get('username'), + email=data.get('email'), + company_id=g.user.company_id, + role=Role[data.get('role', 'TEAM_MEMBER')], + team_id=data.get('team_id') if data.get('team_id') else None + ) + user.set_password(data.get('password')) + + try: + db.session.add(user) + db.session.commit() + return jsonify({'success': True, 'message': 'User created successfully', 'user_id': user.id}) + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': str(e)}), 400 + +@organization_bp.route('/api/organization/users//toggle-status', methods=['POST']) +@login_required +@company_required +@admin_required +def api_toggle_user_status(user_id): + """Toggle user active/blocked status""" + user = User.query.filter_by(id=user_id, company_id=g.user.company_id).first_or_404() + + if user.id == g.user.id: + return jsonify({'success': False, 'message': 'Cannot block your own account'}), 400 + + user.is_blocked = not user.is_blocked + db.session.commit() + + status = 'blocked' if user.is_blocked else 'unblocked' + return jsonify({'success': True, 'message': f'User {status} successfully'}) + +@organization_bp.route('/api/organization/users//assign-team', methods=['POST']) +@login_required +@company_required +@admin_required +def api_assign_team(user_id): + """Assign user to a team""" + user = User.query.filter_by(id=user_id, company_id=g.user.company_id).first_or_404() + data = request.get_json() + + team_id = data.get('team_id') + if team_id: + team = Team.query.filter_by(id=team_id, company_id=g.user.company_id).first_or_404() + user.team_id = team.id + else: + user.team_id = None + + db.session.commit() + return jsonify({'success': True, 'message': 'Team assignment updated'}) + +@organization_bp.route('/api/organization/search', methods=['GET']) +@login_required +@company_required +@admin_required +def api_organization_search(): + """Search users and teams""" + query = request.args.get('q', '').lower() + + if not query: + return jsonify({'users': [], 'teams': []}) + + # Search users + users = User.query.filter( + User.company_id == g.user.company_id, + or_( + User.username.ilike(f'%{query}%'), + User.email.ilike(f'%{query}%') + ) + ).limit(10).all() + + # Search teams + teams = Team.query.filter( + Team.company_id == g.user.company_id, + or_( + Team.name.ilike(f'%{query}%'), + Team.description.ilike(f'%{query}%') + ) + ).limit(10).all() + + return jsonify({ + 'users': [{'id': u.id, 'username': u.username, 'email': u.email} for u in users], + 'teams': [{'id': t.id, 'name': t.name, 'description': t.description} for t in teams] + }) \ No newline at end of file diff --git a/routes/teams.py b/routes/teams.py index 48c69de..3b2ac91 100644 --- a/routes/teams.py +++ b/routes/teams.py @@ -16,9 +16,8 @@ teams_bp = Blueprint('teams', __name__, url_prefix='/admin/teams') @admin_required @company_required def admin_teams(): - team_repo = TeamRepository() - teams = team_repo.get_with_member_count(g.user.company_id) - return render_template('admin_teams.html', title='Team Management', teams=teams) + # Redirect to the new unified organization management page + return redirect(url_for('organization.admin_organization')) @teams_bp.route('/create', methods=['GET', 'POST']) diff --git a/routes/users.py b/routes/users.py index c7c928d..2f95ff1 100644 --- a/routes/users.py +++ b/routes/users.py @@ -38,9 +38,8 @@ def get_available_roles(): @admin_required @company_required def admin_users(): - user_repo = UserRepository() - users = user_repo.get_by_company(g.user.company_id) - return render_template('admin_users.html', title='User Management', users=users) + # Redirect to the new unified organization management page + return redirect(url_for('organization.admin_organization')) @users_bp.route('/create', methods=['GET', 'POST']) diff --git a/templates/admin_company.html b/templates/admin_company.html index 57f363c..d0569c8 100644 --- a/templates/admin_company.html +++ b/templates/admin_company.html @@ -31,7 +31,7 @@
{{ stats.total_teams }}
Teams
- Manage + Manage
{{ stats.total_projects }}
@@ -139,19 +139,11 @@
- -
+
+
-

Manage Users

-

User accounts & permissions

-
-
- - -
-
-

Manage Teams

-

Organize company structure

+

Manage Organization

+

Users, teams & structure

diff --git a/templates/admin_organization.html b/templates/admin_organization.html new file mode 100644 index 0000000..e2aa7eb --- /dev/null +++ b/templates/admin_organization.html @@ -0,0 +1,1651 @@ +{% extends "layout.html" %} + +{% block content %} +
+ + + + +
+
+
{{ teams|length if teams else 0 }}
+
Teams
+
+
+
{{ users|length if users else 0 }}
+
Total Users
+
+
+
{{ users|selectattr('is_blocked', 'equalto', false)|list|length if users else 0 }}
+
Active Users
+
+
+
{{ users|selectattr('team_id', 'none')|list|length if users else 0 }}
+
Unassigned
+
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+ +
+ {% if teams %} +
+ {% for team in teams %} +
+
+
+

{{ team.name }}

+ {% if team.description %} +

{{ team.description }}

+ {% endif %} +
+
+ + +
+
+ +
+
+ + {{ team.users|length }} members +
+ {% if team.lead %} +
+ + Lead: {{ team.lead.username }} +
+ {% endif %} +
+ +
+ {% if team.users %} +
+ {% for user in team.users[:5] %} +
+ {{ user.username }} +
+ {{ user.username }} + {{ user.role.value if user.role else 'Team Member' }} +
+
+ + +
+
+ {% endfor %} + {% if team.users|length > 5 %} +
+ +{{ team.users|length - 5 }} more members +
+ {% endif %} +
+ {% else %} +
+ +

No members in this team

+ +
+ {% endif %} +
+
+ {% endfor %} +
+ + + {% if users|selectattr('team_id', 'none')|list %} +
+

+ + Unassigned Users +

+
+ {% for user in users|selectattr('team_id', 'none') %} +
+ {{ user.username }} + + +
+ {% endfor %} +
+
+ {% endif %} + {% else %} +
+
+

No teams yet

+

Create your first team to organize your workforce

+ +
+ {% endif %} +
+ + +
+ {% if users %} +
+ + + + + + + + + + + + + + {% for user in users %} + + + + + + + + + + {% endfor %} + +
UserEmailTeamRoleStatusJoinedActions
+
+ {{ user.username }} + {{ user.username }} +
+
{{ user.email if user.email else '-' }} + {% if user.team %} + {{ user.team.name }} + {% else %} + Unassigned + {% endif %} + + + {{ user.role.value if user.role else 'Team Member' }} + + + + {% if user.is_blocked %}Blocked{% else %}Active{% endif %} + + {{ user.created_at.strftime('%Y-%m-%d') }} +
+ + {% if user.id != g.user.id %} + {% if user.is_blocked %} + + {% else %} + + {% endif %} + + {% endif %} +
+
+
+ {% else %} +
+
+

No users yet

+

Start by creating user accounts for your team

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

{{ g.company.name }}

+

{{ users|length }} total employees

+
+
+ + {% if teams %} +
+ {% for team in teams %} +
+
+ +

{{ team.name }}

+
+
+ {{ team.users|length }} members + {% if team.lead %} + Lead: {{ team.lead.username }} + {% endif %} +
+
+ {% for user in team.users[:3] %} + {{ user.username }} + {% endfor %} + {% if team.users|length > 3 %} + +{{ team.users|length - 3 }} + {% endif %} +
+
+ {% endfor %} + + {% if users|selectattr('team_id', 'none')|list %} +
+
+ +

Unassigned

+
+
+ {{ users|selectattr('team_id', 'none')|list|length }} users +
+
+ {% endif %} +
+ {% endif %} +
+
+
+
+ + + + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/admin_teams.html b/templates/admin_teams.html deleted file mode 100644 index 0fa49bc..0000000 --- a/templates/admin_teams.html +++ /dev/null @@ -1,602 +0,0 @@ -{% extends 'layout.html' %} - -{% block content %} -
- - - - - {% if teams %} -
-
-
{{ teams|length if teams else 0 }}
-
Total Teams
-
-
-
{{ teams|map(attribute='users')|map('length')|sum if teams else 0 }}
-
Total Members
-
-
-
{{ (teams|map(attribute='users')|map('length')|sum / teams|length)|round(1) if teams else 0 }}
-
Avg Team Size
-
-
- {% endif %} - - -
- {% if teams %} - -
-
- - -
-
- - -
- {% for team in teams %} -
-
-
- -
-
- {{ team.users|length }} members -
-
- -
-

{{ team.name }}

-

- {{ team.description if team.description else 'No description provided' }} -

- -
-
- - Created {{ team.created_at.strftime('%b %d, %Y') }} -
- {% if team.users %} -
- - Led by {{ team.users[0].username }} -
- {% endif %} -
- - - {% if team.users %} -
- {% for member in team.users[:5] %} -
- {{ member.username[:2].upper() }} -
- {% endfor %} - {% if team.users|length > 5 %} -
- +{{ team.users|length - 5 }} -
- {% endif %} -
- {% endif %} -
- -
- - - Manage Team - -
- -
-
-
- {% endfor %} -
- - - - {% else %} - -
-
-

No Teams Yet

-

Create your first team to start organizing your workforce

- - - Create First Team - -
- {% endif %} -
-
- - - - - -{% endblock %} \ No newline at end of file diff --git a/templates/admin_users.html b/templates/admin_users.html deleted file mode 100644 index abab77a..0000000 --- a/templates/admin_users.html +++ /dev/null @@ -1,1073 +0,0 @@ -{% extends "layout.html" %} - -{% block content %} -
- - - - - {% if users %} -
-
-
{{ users|length }}
-
Total Users
-
-
-
{{ users|selectattr('is_blocked', 'equalto', false)|list|length }}
-
Active Users
-
-
-
{{ users|selectattr('role.name', 'equalto', 'ADMIN')|list|length + users|selectattr('role.name', 'equalto', 'SYSTEM_ADMIN')|list|length }}
-
Administrators
-
-
-
{{ users|selectattr('team_id', 'none')|list|length }}
-
Unassigned
-
-
- {% endif %} - - -
- {% if users %} - -
-
- - -
-
- - -
-
- - -
-
- {% for user in users %} -
-
- {{ user.username }} -
- - {% if user.is_blocked %}Blocked{% else %}Active{% endif %} - -
-
- -
-

{{ user.username }}

-

{{ user.email if user.email else 'No email' }}

- - -
- - -
- {% endfor %} -
-
- - -
-
- - - - - - - - - - - - - - {% for user in users %} - - - - - - - - - - {% endfor %} - -
UserEmailRoleTeamStatusJoinedActions
-
- {{ user.username }} - {{ user.username }} -
-
{{ user.email if user.email else '-' }} - - {{ user.role.value if user.role else 'Team Member' }} - - {{ user.team.name if user.team else 'Unassigned' }} - - {% if user.is_blocked %}Blocked{% else %}Active{% endif %} - - {{ user.created_at.strftime('%Y-%m-%d') }} -
- - - - {% if user.id != g.user.id %} -
- {% if user.is_blocked %} - - {% else %} - - {% endif %} -
- - {% endif %} -
-
-
-
- - - - {% else %} - -
-
-

No Users Yet

-

Create your first user to get started

- - + - Create First User - -
- {% endif %} -
- - - -
- - - - - -{% endblock %} \ No newline at end of file diff --git a/templates/company_users.html b/templates/company_users.html deleted file mode 100644 index b443398..0000000 --- a/templates/company_users.html +++ /dev/null @@ -1,204 +0,0 @@ -{% extends "layout.html" %} - -{% block content %} -
-
-

Company Users - {{ company.name }}

- Create New User -
- - -
-

User Statistics

-
-
-

{{ stats.total }}

-

Total Users

-
-
-

{{ stats.active }}

-

Active Users

-
-
-

{{ stats.unverified }}

-

Unverified

-
-
-

{{ stats.blocked }}

-

Blocked

-
-
-

{{ stats.admins }}

-

Administrators

-
-
-

{{ stats.supervisors }}

-

Supervisors

-
-
-
- - -
-

User List

- {% if users %} -
- - - - - - - - - - - - - - {% for user in users %} - - - - - - - - - - {% endfor %} - -
UsernameEmailRoleTeamStatusCreatedActions
- {{ user.username }} - {% if user.two_factor_enabled %} - - {% endif %} - {{ user.email }} - - {{ user.role.value }} - - - {% if user.team %} - {{ user.team.name }} - {% else %} - No team - {% endif %} - - - {% if user.is_blocked %}Blocked{% elif not user.is_verified %}Unverified{% else %}Active{% endif %} - - {{ user.created_at.strftime('%Y-%m-%d') }} - Edit - {% if user.id != g.user.id %} -
- {% if user.is_blocked %} - - {% else %} - - {% endif %} -
- - {% endif %} -
-
- {% else %} -
-

No Users Found

-

There are no users in this company yet.

- Add First User -
- {% endif %} -
- - - -
- - - - -{% endblock %} \ No newline at end of file diff --git a/templates/create_user.html b/templates/create_user.html deleted file mode 100644 index 34b4611..0000000 --- a/templates/create_user.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends "layout.html" %} - -{% block content %} -
-

Create New User

- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
- -
- - Cancel -
-
-
-{% endblock %} \ No newline at end of file diff --git a/templates/edit_user.html b/templates/edit_user.html deleted file mode 100644 index 4fb604f..0000000 --- a/templates/edit_user.html +++ /dev/null @@ -1,53 +0,0 @@ -{% extends "layout.html" %} - -{% block content %} -
-

Edit User: {{ user.username }}

- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - -
- - Cancel -
-
-
-{% endblock %} \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index 59ba13f..b1acaa1 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -135,10 +135,9 @@ {% if g.user.role == Role.ADMIN or g.user.role == Role.SYSTEM_ADMIN %} +
  • Organization
  • Company Settings
  • -
  • Manage Users
  • Invitations
  • -
  • Manage Teams
  • Manage Projects
  • {% if g.user.role == Role.SYSTEM_ADMIN %} diff --git a/templates/team_form.html b/templates/team_form.html deleted file mode 100644 index 44d0696..0000000 --- a/templates/team_form.html +++ /dev/null @@ -1,694 +0,0 @@ -{% extends 'layout.html' %} - -{% block content %} -
    - - - - -
    - -
    - -
    -
    -

    - 📝 - Team Details -

    -
    -
    -
    - {% if team %} - - {% endif %} - -
    - - - Choose a descriptive name for your team -
    - -
    - - - Optional: Add details about this team's role -
    - -
    - - {% if not team %} - Cancel - {% endif %} -
    -
    -
    -
    - - {% if team %} - -
    -
    -

    - 📊 - Team Statistics -

    -
    -
    -
    -
    -
    {{ team_members|length if team_members else 0 }}
    -
    Team Members
    -
    -
    -
    {{ team.projects|length if team.projects else 0 }}
    -
    Active Projects
    -
    -
    -
    {{ team.created_at.strftime('%b %Y') if team.created_at else 'N/A' }}
    -
    Created
    -
    -
    -
    -
    - {% endif %} -
    - - - {% if team %} -
    - -
    -
    -

    - 👤 - Team Members -

    - {{ team_members|length if team_members else 0 }} members -
    -
    - {% if team_members %} -
    - {% for member in team_members %} -
    -
    - {{ member.username[:2].upper() }} -
    -
    -
    {{ member.username }}
    -
    - {{ member.email }} - - {{ member.role.value }} - -
    -
    -
    -
    - - - -
    -
    -
    - {% endfor %} -
    - {% else %} -
    -
    -

    No members in this team yet

    -

    Add members using the form below

    -
    - {% endif %} -
    -
    - - -
    -
    -

    - - Add Team Member -

    -
    -
    - {% if available_users %} -
    - - -
    - - - Only users not already in a team are shown -
    - -
    - -
    -
    - {% else %} -
    -
    -

    All users are assigned

    -

    No available users to add to this team

    -
    - {% endif %} -
    -
    -
    - {% else %} - -
    -
    -
    -
    -
    -

    Team Members

    -

    After creating the team, you'll be able to add members and manage team composition from this page.

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