nordabiz/templates/admin/access_overview.html
Maciej Pienczyn ace9c15cd7
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
feat: add SUPERADMIN role to access overview hierarchy table
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 18:36:09 +01:00

597 lines
32 KiB
HTML

{% extends "base.html" %}
{% block title %}Kontrola dostepu - Admin{% endblock %}
{% block content %}
<style>
.access-matrix { width: 100%; border-collapse: collapse; font-size: 13px; }
.access-matrix th, .access-matrix td { padding: 6px 8px; border: 1px solid #e2e8f0; }
.access-matrix th { background: #f8fafc; font-weight: 600; white-space: nowrap; }
.access-matrix td { text-align: center; }
.access-matrix td:first-child { text-align: left; font-weight: 500; }
.access-yes { color: #16a34a; font-weight: bold; }
.access-no { color: #dc2626; }
.access-partial { color: #d97706; font-weight: bold; }
.access-matrix tr.row-highlight { background: #eff6ff; }
.access-matrix tr.row-separator td { background: #1e40af; color: white; font-weight: 600; text-align: left; padding: 8px; }
.role-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.role-SUPERADMIN { background: #fce7f3; color: #9d174d; }
.role-ADMIN { background: #fee2e2; color: #991b1b; }
.role-OFFICE_MANAGER { background: #fef3c7; color: #92400e; }
.role-MANAGER { background: #dbeafe; color: #1e40af; }
.role-EMPLOYEE { background: #dcfce7; color: #166534; }
.role-MEMBER { background: #e0e7ff; color: #3730a3; }
.role-UNAFFILIATED { background: #f1f5f9; color: #475569; }
.section-card { margin-bottom: var(--spacing-lg); }
.section-card h3 { margin: 0 0 var(--spacing-sm) 0; }
.group-header:hover { background: #cbd5e1 !important; }
.group-header td { background: #e2e8f0; }
.group-toggle { display: inline-block; width: 16px; font-size: 11px; transition: transform 0.15s; }
.group-toggle.collapsed { transform: rotate(-90deg); }
.group-row.hidden { display: none; }
</style>
<div class="admin-container">
<div class="page-header">
<h1>Kontrola dostepu do portalu</h1>
<p style="color: var(--text-secondary); margin-top: var(--spacing-xs);">
Pelna matryca uprawnien — wszystkie role, funkcje i konta
</p>
</div>
<!-- 1. ROLE HIERARCHY -->
<div class="card section-card">
<div style="padding: var(--spacing-md);">
<h3>Hierarchia rol ({{ all_users|length }} aktywnych kont)</h3>
<table class="access-matrix">
<thead>
<tr>
<th>Rola</th>
<th>Poziom</th>
<th>Opis</th>
<th>Kont</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="role-badge role-SUPERADMIN">SUPERADMIN</span></td>
<td style="text-align:center;">200</td>
<td style="text-align:left;">Superadministrator — pelne prawa + audyty techniczne</td>
<td>{{ role_counts.get('SUPERADMIN', 0) }}</td>
</tr>
<tr>
<td><span class="role-badge role-ADMIN">ADMIN</span></td>
<td style="text-align:center;">100</td>
<td style="text-align:left;">Administrator portalu — pelne prawa</td>
<td>{{ role_counts.get('ADMIN', 0) }}</td>
</tr>
<tr>
<td><span class="role-badge role-OFFICE_MANAGER">OFFICE_MANAGER</span></td>
<td style="text-align:center;">50</td>
<td style="text-align:left;">Kierownik biura Norda — panel admina</td>
<td>{{ role_counts.get('OFFICE_MANAGER', 0) }}</td>
</tr>
<tr>
<td><span class="role-badge role-MANAGER">MANAGER</span></td>
<td style="text-align:center;">40</td>
<td style="text-align:left;">Kadra zarzadzajaca — pelna kontrola firmy + uzytkownicy</td>
<td>{{ role_counts.get('MANAGER', 0) }}</td>
</tr>
<tr>
<td><span class="role-badge role-EMPLOYEE">EMPLOYEE</span></td>
<td style="text-align:center;">30</td>
<td style="text-align:left;">Pracownik firmy czlonkowskiej — edycja danych firmy</td>
<td>{{ role_counts.get('EMPLOYEE', 0) }}</td>
</tr>
<tr>
<td><span class="role-badge role-MEMBER">MEMBER</span></td>
<td style="text-align:center;">20</td>
<td style="text-align:left;">Czlonek Norda bez firmy — pelny dostep do tresci</td>
<td>{{ role_counts.get('MEMBER', 0) }}</td>
</tr>
<tr>
<td><span class="role-badge role-UNAFFILIATED">UNAFFILIATED</span></td>
<td style="text-align:center;">10</td>
<td style="text-align:left;">Firma spoza Izby — tylko publiczne profile (bez kontaktow)</td>
<td>{{ role_counts.get('UNAFFILIATED', 0) }}</td>
</tr>
<tr>
<td style="color: var(--text-secondary);"><em>Niezalogowany</em></td>
<td style="text-align:center;">0</td>
<td style="text-align:left;">Odwiedzajacy bez konta — strona glowna, katalog, rejestracja</td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 2. FULL ACCESS MATRIX BY ROLE -->
<div class="card section-card">
<div style="padding: var(--spacing-md); overflow-x: auto;">
<h3>Matryca dostepu do funkcji portalu</h3>
<table class="access-matrix">
<thead>
<tr>
<th style="min-width: 220px;">Funkcja</th>
<th>Niezalogowany</th>
<th>UNAFFILIATED</th>
<th>MEMBER</th>
<th>EMPLOYEE</th>
<th>MANAGER</th>
<th>OFFICE_MGR</th>
<th>ADMIN</th>
</tr>
</thead>
<tbody>
<!-- PUBLIC -->
<tr class="row-separator"><td colspan="8">Strony publiczne</td></tr>
<tr>
<td>Katalog firm (lista, profil)</td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Kontakty firmy (email, telefon)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>NordaGPT (chat AI)</td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Aktualnosci, ZOPK, Raporty</td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Wyszukiwarka</td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Rejestracja / logowanie</td>
<td><span class="access-yes">&#10003;</span></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<!-- COMMUNITY -->
<tr class="row-separator"><td colspan="8">Spolecznosc (wymaga logowania)</td></tr>
<tr>
<td>Panel uzytkownika (dashboard)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Forum (czytanie + pisanie)</td>
<td><span class="access-partial">odczyt</span></td>
<td><span class="access-partial">odczyt</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Wiadomosci prywatne</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Tablica ogloszen B2B</td>
<td><span class="access-partial">odczyt</span></td>
<td><span class="access-partial">odczyt</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Kalendarz wydarzen (zapisy)</td>
<td><span class="access-partial">odczyt</span></td>
<td><span class="access-partial">odczyt</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Edukacja</td>
<td><span class="access-partial">odczyt</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Korzysci czlonkowskie</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<!-- COMPANY MANAGEMENT -->
<tr class="row-separator"><td colspan="8">Zarzadzanie firma</td></tr>
<tr>
<td>Edycja profilu firmy</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-partial">kazda</span></td>
<td><span class="access-partial">kazda</span></td>
</tr>
<tr>
<td>Formularz audytu IT (firma)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-partial">*owner</span></td>
</tr>
<!-- RADA IZBY -->
<tr class="row-separator"><td colspan="8">Rada Izby ({{ rada_members|length }} czlonkow)</td></tr>
<tr>
<td>Strefa Rady (/rada)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-partial">rada</span></td>
<td><span class="access-partial">rada</span></td>
<td><span class="access-partial">rada</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Posiedzenia Rady (dokumenty)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-partial">rada</span></td>
<td><span class="access-partial">rada</span></td>
<td><span class="access-partial">rada</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Wydarzenia typu "rada" (lista uczestnikow)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-partial">rada</span></td>
<td><span class="access-partial">rada</span></td>
<td><span class="access-partial">rada</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<!-- AUDITS -->
<tr class="row-separator"><td colspan="8">Audyty (ograniczone)</td></tr>
<tr>
<td>Audyt SEO (admin + per-firma + API)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-partial">*owner</span></td>
</tr>
<tr>
<td>Audyt IT (admin + per-firma + API)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-partial">*owner</span></td>
</tr>
<tr>
<td>Audyt GBP (admin + per-firma + API)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-partial">*owner</span></td>
</tr>
<tr>
<td>Audyt Social Media (admin + per-firma + API)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-partial">*owner</span></td>
</tr>
<tr>
<td>Audyt KRS</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Digital Maturity</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<!-- ADMIN -->
<tr class="row-separator"><td colspan="8">Panel administracyjny</td></tr>
<tr>
<td>Admin bar (nawigacja)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Zarzadzanie (firmy, uzytkownicy, skladki)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Analityka, ZOPK admin, Forum admin</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Moderacja forum</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Bezpieczenstwo, Debug, Status</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-yes">&#10003;</span></td>
<td><span class="access-yes">&#10003;</span></td>
</tr>
<tr>
<td>Kontrola dostepu (ta strona)</td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-no">&#10007;</span></td>
<td><span class="access-partial">*owner</span></td>
</tr>
</tbody>
</table>
<p style="margin-top: var(--spacing-sm); font-size: 12px; color: var(--text-secondary);">
<span class="access-partial">*owner</span> = tylko {{ audit_owner_email }} &nbsp;|&nbsp;
<span class="access-partial">rada</span> = tylko czlonkowie Rady Izby (is_rada_member) &nbsp;|&nbsp;
<span class="access-partial">kazda</span> = moze edytowac dowolna firme &nbsp;|&nbsp;
<span class="access-partial">odczyt</span> = tylko przegladanie
</p>
</div>
</div>
<!-- 3. RADA IZBY MEMBERS -->
{% if rada_members %}
<div class="card section-card">
<div style="padding: var(--spacing-md);">
<h3>Czlonkowie Rady Izby ({{ rada_members|length }})</h3>
<table class="access-matrix">
<thead>
<tr>
<th>Imie i nazwisko</th>
<th>Email</th>
<th>Rola systemowa</th>
<th>Firma</th>
</tr>
</thead>
<tbody>
{% for user in rada_members %}
<tr>
<td><strong>{{ user.name or '—' }}</strong></td>
<td>{{ user.email }}</td>
<td><span class="role-badge role-{{ user.role }}">{{ user.role }}</span></td>
<td>{{ user.company.name if user.company else '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- 4. ALL USERS (grouped by role, collapsible) -->
<div class="card section-card">
<div style="padding: var(--spacing-md); overflow-x: auto;">
<h3>Wszystkie aktywne konta ({{ all_users|length }})</h3>
<p style="font-size: 12px; color: var(--text-secondary); margin-bottom: var(--spacing-sm);">
Kliknij naglowek grupy, aby zwinac/rozwinac &nbsp;|&nbsp;
<a href="#" onclick="toggleAllGroups(true); return false;">Rozwin wszystkie</a> &nbsp;|&nbsp;
<a href="#" onclick="toggleAllGroups(false); return false;">Zwin wszystkie</a>
</p>
<table class="access-matrix" id="users-table">
<thead>
<tr>
<th>Imie i nazwisko</th>
<th>Email</th>
<th>Rola</th>
<th style="text-align: center;">Rada</th>
<th>Firma</th>
<th style="text-align: center;">Audyty</th>
<th style="text-align: center;">Admin</th>
</tr>
</thead>
<tbody>
{% set current_role = namespace(value='') %}
{% for user in all_users %}
{% if user.role != current_role.value %}
{% set current_role.value = user.role %}
<tr class="group-header" data-group="{{ user.role }}" onclick="toggleGroup('{{ user.role }}')" style="cursor: pointer;">
<td colspan="7" style="background: #e2e8f0; font-weight: 700; padding: 8px;">
<span class="group-toggle" id="toggle-{{ user.role }}">&#9660;</span>
<span class="role-badge role-{{ user.role }}" style="margin: 0 8px;">{{ user.role }}</span>
— {{ role_counts.get(user.role, 0) }} kont
</td>
</tr>
{% endif %}
<tr class="group-row group-{{ user.role }}"{% if user.email == audit_owner_email %} style="background: #eff6ff;"{% endif %}>
<td><strong>{{ user.name or '—' }}</strong></td>
<td>{{ user.email }}</td>
<td><span class="role-badge role-{{ user.role }}">{{ user.role }}</span></td>
<td style="text-align: center;">
{% if user.is_rada_member %}
<span class="access-yes">&#10003;</span>
{% else %}
<span style="color: #cbd5e1;"></span>
{% endif %}
</td>
<td>{{ user.company.name if user.company else '—' }}</td>
<td style="text-align: center;">
{% if user.email == audit_owner_email %}
<span class="access-yes">&#10003;</span>
{% else %}
<span class="access-no">&#10007;</span>
{% endif %}
</td>
<td style="text-align: center;">
{% if user.role in ('OFFICE_MANAGER', 'ADMIN') %}
<span class="access-yes">&#10003;</span>
{% else %}
<span class="access-no">&#10007;</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Reversibility note -->
<div class="card" style="background: #fefce8; border: 1px solid #fde68a;">
<div style="padding: var(--spacing-md);">
<h3 style="margin: 0 0 var(--spacing-sm) 0; color: #92400e;">Odwracalnosc</h3>
<p style="margin: 0; color: #78350f;">
Aby przywrocic dostep do audytow dla wszystkich administratorow,
nalezy zmienic funkcje <code>is_audit_owner()</code> w pliku
<code>utils/decorators.py</code> na <code>return current_user.has_role(SystemRole.OFFICE_MANAGER)</code>.
Jeden plik, jedna linia.
</p>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
function toggleGroup(role) {
var rows = document.querySelectorAll('.group-' + role);
var toggle = document.getElementById('toggle-' + role);
var isCollapsed = toggle.classList.contains('collapsed');
rows.forEach(function(row) {
if (isCollapsed) {
row.classList.remove('hidden');
} else {
row.classList.add('hidden');
}
});
toggle.classList.toggle('collapsed');
}
function toggleAllGroups(expand) {
var headers = document.querySelectorAll('.group-header');
headers.forEach(function(header) {
var role = header.getAttribute('data-group');
var toggle = document.getElementById('toggle-' + role);
var rows = document.querySelectorAll('.group-' + role);
if (expand) {
toggle.classList.remove('collapsed');
rows.forEach(function(r) { r.classList.remove('hidden'); });
} else {
toggle.classList.add('collapsed');
rows.forEach(function(r) { r.classList.add('hidden'); });
}
});
}
{% endblock %}