feat: sorting/filtering by roles in admin users + OFFICE_MANAGER access
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

- Add sort keys and data-sort-value attributes to 'Upr. firmowe' and 'Rola' columns
- Add filter tabs for MANAGER, OFFICE_MANAGER, company-role NONE and MANAGER
- Add data-company-role attribute to user rows for JS filtering
- Grant OFFICE_MANAGER access to admin_users, assign-company, reset-password, change-role, get-roles endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-10 14:45:06 +02:00
parent e0e0ea2cf6
commit 925c9862c3
3 changed files with 19 additions and 9 deletions

View File

@ -150,7 +150,7 @@ def admin_recommendation_reject(recommendation_id):
@bp.route('/users') @bp.route('/users')
@login_required @login_required
@role_required(SystemRole.ADMIN) @role_required(SystemRole.OFFICE_MANAGER)
def admin_users(): def admin_users():
"""Admin panel for user management""" """Admin panel for user management"""
db = SessionLocal() db = SessionLocal()
@ -405,7 +405,7 @@ def admin_user_update(user_id):
@bp.route('/users/<int:user_id>/assign-company', methods=['POST']) @bp.route('/users/<int:user_id>/assign-company', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN) @role_required(SystemRole.OFFICE_MANAGER)
def admin_user_assign_company(user_id): def admin_user_assign_company(user_id):
"""Assign a company to a user""" """Assign a company to a user"""
db = SessionLocal() db = SessionLocal()
@ -584,7 +584,7 @@ def admin_user_delete(user_id):
@bp.route('/users/<int:user_id>/reset-password', methods=['POST']) @bp.route('/users/<int:user_id>/reset-password', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN) @role_required(SystemRole.OFFICE_MANAGER)
def admin_user_reset_password(user_id): def admin_user_reset_password(user_id):
"""Generate password reset token and optionally send email""" """Generate password reset token and optionally send email"""
db = SessionLocal() db = SessionLocal()

View File

@ -314,7 +314,7 @@ def admin_users_bulk_create():
@bp.route('/users-api/change-role', methods=['POST']) @bp.route('/users-api/change-role', methods=['POST'])
@login_required @login_required
@role_required(SystemRole.ADMIN) @role_required(SystemRole.OFFICE_MANAGER)
def admin_users_change_role(): def admin_users_change_role():
"""Change user's system role.""" """Change user's system role."""
db = SessionLocal() db = SessionLocal()
@ -372,7 +372,7 @@ def admin_users_change_role():
@bp.route('/users-api/roles', methods=['GET']) @bp.route('/users-api/roles', methods=['GET'])
@login_required @login_required
@role_required(SystemRole.ADMIN) @role_required(SystemRole.OFFICE_MANAGER)
def admin_users_get_roles(): def admin_users_get_roles():
"""Get list of available roles for dropdown.""" """Get list of available roles for dropdown."""
roles = [ roles = [

View File

@ -1174,6 +1174,11 @@
{% if locked_count > 0 %} {% if locked_count > 0 %}
<button class="filter-tab" data-filter="locked" style="color: #DC2626;">Zablokowane ({{ locked_count }})</button> <button class="filter-tab" data-filter="locked" style="color: #DC2626;">Zablokowane ({{ locked_count }})</button>
{% endif %} {% endif %}
<span style="border-left:1px solid var(--border);margin:0 4px;"></span>
<button class="filter-tab" data-filter="role-manager">Kadra zarządz.</button>
<button class="filter-tab" data-filter="role-office">Kier. Biura</button>
<button class="filter-tab" data-filter="company-role-none">Bez upr. firm.</button>
<button class="filter-tab" data-filter="company-role-manager">Zarządzający</button>
</div> </div>
{% if users %} {% if users %}
@ -1183,8 +1188,8 @@
<th data-sort-key="id" data-sort-type="number">ID</th> <th data-sort-key="id" data-sort-type="number">ID</th>
<th data-sort-key="name" data-sort-type="string">Użytkownik</th> <th data-sort-key="name" data-sort-type="string">Użytkownik</th>
<th data-sort-key="company" data-sort-type="string">Firma</th> <th data-sort-key="company" data-sort-type="string">Firma</th>
<th>Upr. firmowe</th> <th data-sort-key="company_role" data-sort-type="number">Upr. firmowe</th>
<th>Rola</th> <th data-sort-key="role" data-sort-type="number">Rola</th>
<th data-sort-key="created" data-sort-type="date" class="sort-desc">Utworzono</th> <th data-sort-key="created" data-sort-type="date" class="sort-desc">Utworzono</th>
<th data-sort-key="last_login" data-sort-type="date">Ostatnie logowanie</th> <th data-sort-key="last_login" data-sort-type="date">Ostatnie logowanie</th>
<th>Status</th> <th>Status</th>
@ -1195,6 +1200,7 @@
{% for user in users %} {% for user in users %}
<tr data-user-id="{{ user.id }}" <tr data-user-id="{{ user.id }}"
data-role="{{ user.role }}" data-role="{{ user.role }}"
data-company-role="{{ user.company_role or 'NONE' }}"
data-is-verified="{{ 'true' if user.is_verified else 'false' }}" data-is-verified="{{ 'true' if user.is_verified else 'false' }}"
data-pending-company="{{ 'true' if user.company_id and user.company_role == 'NONE' else 'false' }}" data-pending-company="{{ 'true' if user.company_id and user.company_role == 'NONE' else 'false' }}"
data-locked="{{ 'true' if user.locked_until and user.locked_until > now else 'false' }}"> data-locked="{{ 'true' if user.locked_until and user.locked_until > now else 'false' }}">
@ -1215,7 +1221,7 @@
<span style="color: var(--text-secondary);">-</span> <span style="color: var(--text-secondary);">-</span>
{% endif %} {% endif %}
</td> </td>
<td> <td data-sort-value="{{ {'NONE':0,'VIEWER':10,'EMPLOYEE':20,'MANAGER':30}.get(user.company_role, 0) }}">
{% if user.company %} {% if user.company %}
<select class="role-select" style="font-size: var(--font-size-sm);" <select class="role-select" style="font-size: var(--font-size-sm);"
data-user-id="{{ user.id }}" data-user-id="{{ user.id }}"
@ -1229,7 +1235,7 @@
<span style="color: var(--text-secondary);">-</span> <span style="color: var(--text-secondary);">-</span>
{% endif %} {% endif %}
</td> </td>
<td> <td data-sort-value="{{ {'UNAFFILIATED':10,'MEMBER':20,'EMPLOYEE':30,'MANAGER':40,'OFFICE_MANAGER':50,'ADMIN':100}.get(user.role, 0) }}">
<select class="role-select" <select class="role-select"
data-user-id="{{ user.id }}" data-user-id="{{ user.id }}"
onchange="changeUserRole({{ user.id }}, this.value)" onchange="changeUserRole({{ user.id }}, this.value)"
@ -1947,6 +1953,10 @@ Lub format CSV, Excel, lista emaili..."></textarea>
else if (filter === 'unverified') show = !isVerified; else if (filter === 'unverified') show = !isVerified;
else if (filter === 'pending-company') show = isPendingCompany; else if (filter === 'pending-company') show = isPendingCompany;
else if (filter === 'locked') show = row.dataset.locked === 'true'; else if (filter === 'locked') show = row.dataset.locked === 'true';
else if (filter === 'role-manager') show = row.dataset.role === 'MANAGER';
else if (filter === 'role-office') show = row.dataset.role === 'OFFICE_MANAGER';
else if (filter === 'company-role-none') show = row.dataset.companyRole === 'NONE' && row.querySelector('.user-company');
else if (filter === 'company-role-manager') show = row.dataset.companyRole === 'MANAGER';
row.style.display = show ? '' : 'none'; row.style.display = show ? '' : 'none';
}); });