You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2090 lines
124 KiB
2090 lines
124 KiB
import json
|
|
from django import forms
|
|
from django.contrib.auth.models import Group
|
|
from django.utils.html import escape
|
|
from django.utils.safestring import mark_safe
|
|
from django.utils.translation import get_language, get_language_bidi, gettext as _
|
|
|
|
from apps.account.models.role import Role, PERMISSION_CATEGORIES, ALL_PERMISSION_FIELDS
|
|
from apps.profiles.models import Region
|
|
|
|
CATEGORY_ICONS = {
|
|
"Identity & Profiles": "badge",
|
|
"Chat & Communications": "forum",
|
|
"CMS & Articles": "article",
|
|
"LMS & Academics": "school",
|
|
"Meetings": "video_camera_front",
|
|
"Events": "event",
|
|
"Community Projects": "task_alt",
|
|
"Donations & Charity": "volunteer_activism",
|
|
"Support & Tickets": "confirmation_number",
|
|
"Dynamic Forms": "dynamic_form",
|
|
"Diplomacy & Institutional": "public",
|
|
"Administration & Security": "admin_panel_settings",
|
|
}
|
|
|
|
CATEGORY_TITLES_FA = {
|
|
"Identity & Profiles": "هویت و پروفایلها",
|
|
"Chat & Communications": "پیامرسان و ارتباطات",
|
|
"CMS & Articles": "مدیریت محتوا و مقالات",
|
|
"LMS & Academics": "آموزش و امور علمی",
|
|
"Meetings": "جلسات و کنفرانسها",
|
|
"Events": "رویدادها و همایشها",
|
|
"Community Projects": "پروژههای مشارکتی",
|
|
"Donations & Charity": "امور خیریه و هدایا",
|
|
"Support & Tickets": "پشتیبانی و تیکتها",
|
|
"Dynamic Forms": "فرمهای پویا و نظرسنجی",
|
|
"Diplomacy & Institutional": "روابط نهادی و بینالملل",
|
|
"Administration & Security": "مدیریت و امنیت سیستم",
|
|
}
|
|
|
|
|
|
class UserAccessManagementWidget(forms.Widget):
|
|
"""
|
|
State-of-the-art Unfold Admin Access Management Widget.
|
|
Supports seamless bilingual localization (English / Persian) with automatic LTR/RTL direction.
|
|
"""
|
|
|
|
def __init__(self, attrs=None):
|
|
super().__init__(attrs)
|
|
self.instance = None
|
|
|
|
def render(self, name, value, attrs=None, renderer=None):
|
|
instance = getattr(self, "instance", None)
|
|
current_lang = (get_language() or "en").lower()
|
|
is_fa = current_lang.startswith("fa")
|
|
is_bidi = get_language_bidi()
|
|
layout_dir = "rtl" if is_bidi else "ltr"
|
|
|
|
t = {
|
|
# Overview Card
|
|
"overview_title": "خلاصه سطح دسترسی و اختیارات کاربر" if is_fa else "Role & Permissions Overview",
|
|
"overview_sub": "تنظیمات یکپارچه نقش سازمانی، دسترسیهای اختصاصی، نهاد وابسته و مناطق جغرافیایی" if is_fa else "Unified management of user role, custom permissions, affiliated institution, and managed regions",
|
|
"btn_add_new_perm": "+ افزودن دسترسی جدید" if is_fa else "+ Add Permission",
|
|
"btn_open_modal": "تنظیم نقش و دسترسیها (مودال کامل)" if is_fa else "Configure Role & Access",
|
|
"base_role_label": "نقش سازمانی / Base Role" if is_fa else "Base Role",
|
|
"btn_change_role": "تغییر نقش" if is_fa else "Change Role",
|
|
"unassigned_role": "بدون نقش سازمانی" if is_fa else "Unassigned / No Role",
|
|
"all_perms_match": "تمامی اختیارات منطبق بر نقش پایه" if is_fa else "All permissions match base role",
|
|
"custom_perms_suffix": "دسترسی اختصاصی فراتر از نقش" if is_fa else "custom permissions beyond role",
|
|
"revoked_perms_suffix": "اختیار سلبشده" if is_fa else "permissions revoked",
|
|
"additional_perms_label": "دسترسیهای فراتر از نقش / Additional" if is_fa else "Additional Permissions",
|
|
"btn_add_perm": "افزودن دسترسی" if is_fa else "Add Permission",
|
|
"no_custom_perms": "فاقد مجوز اختصاصی (دسترسی استاندارد)" if is_fa else "No custom permissions (Standard role access)",
|
|
"more_suffix": "دیگر" if is_fa else "more",
|
|
"add_perm_hint": "روی دکمه «افزودن دسترسی» کلیک کنید تا اختیارات اختصاصی اضافه شود." if is_fa else "Click 'Add Permission' to grant additional custom permissions.",
|
|
"institution_label": "نهاد وابسته / Institution" if is_fa else "Affiliated Institution",
|
|
"no_institution": "فاقد نهاد وابسته (شخصی/مستقل)" if is_fa else "No affiliated institution (Independent/Personal)",
|
|
"institution_hint": "اختیارات اداری دروننهادی در این مرکز اعمال میشود." if is_fa else "Administrative authority within this institution applies here.",
|
|
"regions_label": "قلمرو جغرافیایی / Managed Regions" if is_fa else "Geographic Jurisdictions (Managed Regions)",
|
|
"regions_unlimited": "دسترسی عمومی / نامحدود" if is_fa else "Global / Unlimited access",
|
|
"regions_hint": "برای مدیران منطقهای به استانها یا کشورهای خاص محدود میشود." if is_fa else "For regional administrators, authority is restricted to assigned regions.",
|
|
"loading": "در حال بارگذاری..." if is_fa else "Loading...",
|
|
|
|
# Modal Dialog
|
|
"modal_title": "پیکربندی جامع دسترسی و اختیارات" if is_fa else "Configure Role & Permissions",
|
|
"modal_sub": "تخصیص نقش سازمانی، مدیریت اختیارات اختصاصی، تعیین نهاد و حوزههای منطقهای" if is_fa else "Assign organizational role, configure custom permissions, set affiliated institution and geographic regions",
|
|
"tab_roles": "نقشها و نوع کاربر" if is_fa else "Roles & User Type",
|
|
"tab_permissions": "مجوزها و اختیارات" if is_fa else "Permissions & Matrix",
|
|
"tab_institutions": "دسترسی به نهادها" if is_fa else "Institution Access",
|
|
"tab_regions": "دسترسی به مناطق" if is_fa else "Region Access",
|
|
|
|
# Tab 1
|
|
"select_role_title": "انتخاب نقش پایه سازمانی (Assigned Role)" if is_fa else "Select Base Organizational Role",
|
|
"select_role_sub": "نقش پایه شامل دستهای از اختیارات پیشفرض تاییدشده است" if is_fa else "Base role includes a bundle of verified default permissions",
|
|
"default_perms_label": "مجوزهای پیشفرض موجود در نقش انتخابی:" if is_fa else "Default permissions granted by selected role:",
|
|
"active_perms_count": "مجوز فعال" if is_fa else "active permissions",
|
|
"no_role_selected": "هیچ نقشی انتخاب نشده است." if is_fa else "No role selected.",
|
|
"role_hint": "برای اعطای مجوزهای بیشتر به کاربر (فراتر از این نقش) یا سلب برخی اختیارات، به تب «مجوزها و اختیارات» مراجعه کنید." if is_fa else "To grant additional permissions (beyond this role) or revoke specific ones, navigate to the 'Permissions & Matrix' tab.",
|
|
"default_role_desc": "نقش سازمانی با مجوزهای پیشفرض" if is_fa else "Organizational role with default permissions",
|
|
"default_perms_chip": "مجوز پیشفرض" if is_fa else "default permissions",
|
|
"user_type_label": "نوع کاربری (User Type)" if is_fa else "User Type",
|
|
"groups_label": "گروههای سیستمی (Django Groups)" if is_fa else "Django Groups",
|
|
|
|
# User types
|
|
"ut_superadmin": "Superadmin (مدیر ارشد سامانه)" if is_fa else "Superadmin (System Administrator)",
|
|
"ut_regional_admin": "Regional Admin (مدیر منطقهای)" if is_fa else "Regional Admin",
|
|
"ut_institution_admin": "Institution Admin (مدیر نهاد)" if is_fa else "Institution Admin",
|
|
"ut_editor": "Editor (ویرایشگر محتوا)" if is_fa else "Editor (Content Editor)",
|
|
"ut_staff": "Staff (کارشناس پشتیبانی و امور اداری)" if is_fa else "Staff (Support & Operations)",
|
|
"ut_client": "Client (کاربر عمومی سامانه)" if is_fa else "Client (Standard User)",
|
|
|
|
# Tab 2
|
|
"matrix_title": "ماتریس دسترسیها و شخصیسازی مجوزها" if is_fa else "Permissions Matrix & Customization",
|
|
"matrix_sub": "مجوزهای نقش پایه بهصورت خودکار ارثبری میشوند. میتوانید مجوز جدید اضافه کنید یا اختیاری را سلب فرمایید." if is_fa else "Permissions from the base role are automatically inherited. You can grant custom permissions or revoke existing ones.",
|
|
"search_placeholder": "جستجو در مجوزها..." if is_fa else "Search permissions...",
|
|
"reset_btn": "پاکسازی" if is_fa else "Reset Overrides",
|
|
"add_perm_panel_title": "افزودن دسترسی جدید به کاربر (Add New Permission)" if is_fa else "Add New Permission to User",
|
|
"add_perm_panel_sub": "یک مجوز سیستمی را انتخاب یا شناسه اختصاصی وارد کنید" if is_fa else "Select a system permission or enter a custom key",
|
|
"select_system_perm_default": "-- انتخاب از تمام مجوزهای سیستم --" if is_fa else "-- Select from all system permissions --",
|
|
"in_role_tag": " (در نقش موجود است)" if is_fa else " (in role)",
|
|
"active_custom_tag": " (فعال اختصاصی)" if is_fa else " (active custom)",
|
|
"btn_grant": "اعطای مجوز" if is_fa else "Grant Permission",
|
|
"custom_key_placeholder": "یا وارد کردن شناسه مجوز دلخواه (مثال: can_export_reports)..." if is_fa else "Or enter custom permission key (e.g. can_export_reports)...",
|
|
"btn_add_key": "افزودن کلید" if is_fa else "Add Key",
|
|
"custom_flags_title": "مجوزهای سفارشی افزودهشده (Custom Granted Flags)" if is_fa else "Custom Granted Flags",
|
|
"delete_perm_title": "حذف این مجوز" if is_fa else "Delete this permission",
|
|
"status_custom_allow": "✓ مجوز اختصاصی (Allow)" if is_fa else "✓ Custom Granted (Allow)",
|
|
"status_custom_deny": "✕ سلب اختیار (Deny)" if is_fa else "✕ Revoked (Deny)",
|
|
"status_inherited_active": "● ارثبری: فعال از نقش" if is_fa else "● Inherited: Active from role",
|
|
"status_inherited_inactive": "○ ارثبری: غیرفعال" if is_fa else "○ Inherited: Inactive",
|
|
"status_custom_flag_allow": "✓ مجوز فعال سفارشی" if is_fa else "✓ Custom active permission",
|
|
"status_custom_flag_deny": "✕ سلب اختیار سفارشی" if is_fa else "✕ Custom revoked permission",
|
|
"btn_inherit": "ارثبری" if is_fa else "Inherit",
|
|
"btn_allow": "مجوز" if is_fa else "Allow",
|
|
"btn_deny": "سلب" if is_fa else "Deny",
|
|
|
|
# Tab 3
|
|
"inst_title": "نهاد وابسته به کاربر (Affiliated Institution)" if is_fa else "Affiliated Institution",
|
|
"inst_sub": "نام مرکز، موسسه، دانشگاه یا حوزه علمیهای که کاربر به نمایندگی از آن فعالیت میکند." if is_fa else "The organization, center, university, or seminary represented by this user.",
|
|
"inst_input_label": "عنوان رسمی نهاد" if is_fa else "Official Institution Name",
|
|
"inst_input_placeholder": "مثال: جامعة المصطفی العالمیة / دانشگاه بینالمللی اهلبیت (ع)" if is_fa else "e.g., Al-Mustafa International University",
|
|
"inst_guidelines_title": "راهنمای اختیارات دروننهادی:" if is_fa else "Internal Institution Guidelines:",
|
|
"inst_guideline_1": "کاربران دارای نوع کاربری Institution Admin بهطور خودکار مدیر صفحه و پروژههای این نهاد خواهند بود." if is_fa else "Users with Institution Admin user type automatically manage this institution's profile and projects.",
|
|
"inst_guideline_2": "برای عضویت چندگانه در نهادها، میتوانید از بخش پایین صفحه (Institution Memberships Inline) استفاده کنید." if is_fa else "For multiple institution memberships, use the Institution Memberships inline below.",
|
|
|
|
# Tab 4
|
|
"regions_title": "تخصیص حوزههای صلاحیت جغرافیایی (Managed Regions)" if is_fa else "Assign Geographic Jurisdictions (Managed Regions)",
|
|
"regions_sub": "اختیارات نظارتی مدیران منطقهای صرفاً بر روی نهادها، کاربران و رویدادهای واقع در مناطق انتخابشده اعمال میگردد." if is_fa else "Oversight authority of regional admins applies only to institutions, users, and events within selected regions.",
|
|
"regions_selected_count": "منطقه انتخابشده" if is_fa else "regions selected",
|
|
|
|
# Footer
|
|
"footer_role": "نقش:" if is_fa else "Role:",
|
|
"footer_no_role": "بدون نقش" if is_fa else "No Role",
|
|
"footer_extra": "دسترسی اختصاصی" if is_fa else "custom permissions",
|
|
"footer_regions": "حوزه منطقهای" if is_fa else "geographic regions",
|
|
"btn_cancel": "انصراف" if is_fa else "Cancel",
|
|
"btn_apply": "ثبت و اعمال تغییرات" if is_fa else "Save & Apply Changes",
|
|
}
|
|
|
|
if isinstance(value, str):
|
|
try:
|
|
overrides = json.loads(value) if value.strip() else {}
|
|
except Exception:
|
|
overrides = {}
|
|
elif isinstance(value, dict):
|
|
overrides = value
|
|
else:
|
|
overrides = {}
|
|
|
|
# 1. Fetch Roles Data
|
|
roles_data = {}
|
|
roles_list = []
|
|
for r in Role.objects.all().order_by("name"):
|
|
r_dict = {
|
|
"id": str(r.id),
|
|
"name": r.name,
|
|
"slug": r.slug or "",
|
|
"description": r.description or "",
|
|
"is_default": r.is_default,
|
|
"is_system": r.is_system,
|
|
"permissions": r.get_permissions_dict(),
|
|
}
|
|
roles_data[str(r.id)] = r_dict
|
|
roles_list.append(r_dict)
|
|
|
|
# 2. Fetch Categories & Permissions Data
|
|
categories_data = []
|
|
all_perms_map = {}
|
|
for cat_label, field_names in PERMISSION_CATEGORIES.items():
|
|
cat_name_str = str(cat_label)
|
|
icon = CATEGORY_ICONS.get(cat_name_str, "admin_panel_settings")
|
|
perms_list = []
|
|
for field_name in field_names:
|
|
try:
|
|
f = Role._meta.get_field(field_name)
|
|
vname = str(f.verbose_name)
|
|
help_txt = str(f.help_text) if hasattr(f, "help_text") else ""
|
|
except Exception:
|
|
vname = field_name.replace("_", " ").title()
|
|
help_txt = ""
|
|
p_item = {
|
|
"name": field_name,
|
|
"label": vname,
|
|
"help": help_txt,
|
|
"category": cat_name_str,
|
|
}
|
|
perms_list.append(p_item)
|
|
all_perms_map[field_name] = p_item
|
|
|
|
display_title = CATEGORY_TITLES_FA.get(cat_name_str, cat_name_str) if is_fa else cat_name_str
|
|
categories_data.append({
|
|
"title": display_title,
|
|
"icon": icon,
|
|
"perms": perms_list,
|
|
})
|
|
|
|
# 3. Fetch Regions Data
|
|
regions_data = []
|
|
try:
|
|
for reg in Region.objects.filter(is_active=True).order_by("name"):
|
|
regions_data.append({
|
|
"id": str(reg.id),
|
|
"name": reg.name,
|
|
"code": getattr(reg, "code", "") or "",
|
|
})
|
|
except Exception:
|
|
pass
|
|
|
|
# 4. Fetch Groups Data
|
|
groups_data = []
|
|
try:
|
|
for grp in Group.objects.all().order_by("name"):
|
|
groups_data.append({
|
|
"id": str(grp.id),
|
|
"name": grp.name,
|
|
})
|
|
except Exception:
|
|
pass
|
|
|
|
# 5. Extract Current User State
|
|
current_role_id = str(instance.role_id) if instance and instance.role_id else ""
|
|
current_user_type = instance.user_type if instance and hasattr(instance, "user_type") else "client"
|
|
current_institution = instance.institution_name or "" if instance and hasattr(instance, "institution_name") else ""
|
|
current_groups = [str(g.id) for g in instance.groups.all()] if instance and instance.pk else []
|
|
current_regions = [str(r.id) for r in instance.managed_regions.all()] if instance and instance.pk else []
|
|
|
|
current_role = roles_data.get(current_role_id)
|
|
current_role_display = current_role["name"] if current_role else t["unassigned_role"]
|
|
current_inst_display = current_institution if current_institution else t["no_institution"]
|
|
|
|
initial_state = {
|
|
"roleId": current_role_id,
|
|
"userType": current_user_type,
|
|
"institution": current_institution,
|
|
"groups": current_groups,
|
|
"regions": current_regions,
|
|
"overrides": overrides,
|
|
}
|
|
|
|
roles_json_str = json.dumps(roles_data)
|
|
categories_json_str = json.dumps(categories_data)
|
|
regions_json_str = json.dumps(regions_data)
|
|
groups_json_str = json.dumps(groups_data)
|
|
initial_json_str = json.dumps(initial_state)
|
|
perms_map_json_str = json.dumps(all_perms_map)
|
|
t_json_str = json.dumps(t)
|
|
|
|
# ─── Render HTML Structure ──────────────────────────────────────────
|
|
html = [
|
|
f'''
|
|
<div id="user-access-manager-root" class="w-full my-2 font-sans select-none" dir="{layout_dir}">
|
|
<!-- Hidden storage input -->
|
|
<textarea name="{escape(name)}" id="id_{escape(name)}" class="hidden">{escape(json.dumps(overrides))}</textarea>
|
|
|
|
<!-- Embedded JSON Data -->
|
|
<script id="access-data-roles" type="application/json">{roles_json_str}</script>
|
|
<script id="access-data-categories" type="application/json">{categories_json_str}</script>
|
|
<script id="access-data-regions" type="application/json">{regions_json_str}</script>
|
|
<script id="access-data-groups" type="application/json">{groups_json_str}</script>
|
|
<script id="access-data-initial" type="application/json">{initial_json_str}</script>
|
|
<script id="access-data-perms-map" type="application/json">{perms_map_json_str}</script>
|
|
<script id="access-data-i18n" type="application/json">{t_json_str}</script>
|
|
|
|
<!-- =================================================================== -->
|
|
<!-- 1. COMPACT ACCESS OVERVIEW CARD (Unfold Design System Aligned) -->
|
|
<!-- =================================================================== -->
|
|
<div class="unfold-access-card select-none" id="access-overview-card">
|
|
<!-- Card Header -->
|
|
<div class="overview-card-header">
|
|
<div class="overview-header-left">
|
|
<div class="overview-header-icon-box">
|
|
<span class="material-symbols-outlined" style="font-size: 24px;">shield_person</span>
|
|
</div>
|
|
<div>
|
|
<h3 class="overview-header-title">{t["overview_title"]}</h3>
|
|
<p class="overview-header-sub">{t["overview_sub"]}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Action Buttons -->
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<!-- Direct Add Permission Button -->
|
|
<button type="button" id="btn-header-add-perm" class="unfold-btn-secondary">
|
|
<span class="material-symbols-outlined" style="font-size: 16px;">add_circle</span>
|
|
<span>{t["btn_add_new_perm"]}</span>
|
|
</button>
|
|
|
|
<!-- Open Modal Trigger Button -->
|
|
<button type="button" id="btn-open-access-modal" class="unfold-btn-primary">
|
|
<span class="material-symbols-outlined" style="font-size: 16px;">tune</span>
|
|
<span>{t["btn_open_modal"]}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Overview Details Grid: 4 Metric Cards -->
|
|
<div class="overview-metric-grid">
|
|
<!-- Box 1: Role & Permissions Status -->
|
|
<div class="overview-metric-box">
|
|
<div class="flex items-center justify-between">
|
|
<span class="overview-metric-label">{t["base_role_label"]}</span>
|
|
<button type="button" id="btn-change-role-card" class="overview-small-btn">{t["btn_change_role"]}</button>
|
|
</div>
|
|
<div class="overview-metric-content" id="overview-role-badge-container">
|
|
<span class="overview-badge overview-badge-role">
|
|
<span class="material-symbols-outlined" style="font-size: 15px;">badge</span>
|
|
<span id="overview-role-name">{escape(current_role_display)}</span>
|
|
</span>
|
|
</div>
|
|
<div id="overview-perm-status" class="overview-metric-hint">
|
|
<!-- Filled by JS -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Box 2: Extra Permissions Pills & ADD BUTTON -->
|
|
<div class="overview-metric-box">
|
|
<div class="flex items-center justify-between">
|
|
<span class="overview-metric-label">{t["additional_perms_label"]}</span>
|
|
<button type="button" id="btn-add-perm-card" class="overview-small-btn">
|
|
<span class="material-symbols-outlined" style="font-size: 13px;">add</span>
|
|
<span>{t["btn_add_perm"]}</span>
|
|
</button>
|
|
</div>
|
|
<div class="overview-metric-content" id="overview-extra-perms-container">
|
|
<span class="overview-metric-hint italic">{t["loading"]}</span>
|
|
</div>
|
|
<span class="overview-metric-hint" id="overview-extra-perms-sub">
|
|
{t["add_perm_hint"]}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Box 3: Affiliated Institution -->
|
|
<div class="overview-metric-box">
|
|
<span class="overview-metric-label">{t["institution_label"]}</span>
|
|
<div class="overview-metric-content" id="overview-institution-name">
|
|
<span class="material-symbols-outlined" style="font-size: 17px; color: var(--color-font-subtle-light, #64748b);">apartment</span>
|
|
<span class="truncate font-semibold" style="font-size: 0.75rem;">{escape(current_inst_display)}</span>
|
|
</div>
|
|
<span class="overview-metric-hint">
|
|
{t["institution_hint"]}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Box 4: Managed Regions -->
|
|
<div class="overview-metric-box">
|
|
<span class="overview-metric-label">{t["regions_label"]}</span>
|
|
<div class="overview-metric-content" id="overview-regions-container">
|
|
<span class="overview-metric-hint italic">{t["loading"]}</span>
|
|
</div>
|
|
<span class="overview-metric-hint" id="overview-regions-sub">
|
|
{t["regions_hint"]}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- =================================================================== -->
|
|
<!-- 2. FULL MODAL DIALOG (Attached directly to <body> via JS) -->
|
|
<!-- Fixed Height: 85vh, Internal Scrolling on Body, Top z-index -->
|
|
<!-- =================================================================== -->
|
|
<style>
|
|
#access-overview-card,
|
|
.unfold-access-card {{
|
|
background-color: var(--color-base-0, #ffffff) !important;
|
|
border: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
border-radius: var(--border-radius, 0.5rem) !important;
|
|
padding: 1.5rem !important;
|
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05) !important;
|
|
margin-top: 0.5rem !important;
|
|
margin-bottom: 1.25rem !important;
|
|
box-sizing: border-box !important;
|
|
transition: border-color 0.15s ease-in-out !important;
|
|
}}
|
|
.dark #access-overview-card,
|
|
.dark .unfold-access-card {{
|
|
background-color: var(--color-base-900, #0f172a) !important;
|
|
border-color: var(--color-base-800, #1e293b) !important;
|
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.4) !important;
|
|
}}
|
|
.field-custom_permissions > div:first-child:has(label[for="id_custom_permissions"]),
|
|
.field-custom_permissions label[for="id_custom_permissions"] {{
|
|
display: none !important;
|
|
}}
|
|
.overview-card-header {{
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
justify-content: space-between !important;
|
|
flex-wrap: wrap !important;
|
|
gap: 1rem !important;
|
|
padding-bottom: 1.25rem !important;
|
|
margin-bottom: 1.25rem !important;
|
|
border-bottom: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
}}
|
|
.dark .overview-card-header {{
|
|
border-bottom-color: var(--color-base-800, #1e293b) !important;
|
|
}}
|
|
.overview-header-left {{
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
gap: 0.875rem !important;
|
|
}}
|
|
.overview-header-icon-box {{
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
justify-content: center !important;
|
|
width: 2.75rem !important;
|
|
height: 2.75rem !important;
|
|
border-radius: var(--border-radius, 0.5rem) !important;
|
|
background-color: rgba(99, 102, 241, 0.1) !important;
|
|
color: var(--color-primary-600, #4f46e5) !important;
|
|
flex-shrink: 0 !important;
|
|
}}
|
|
.dark .overview-header-icon-box {{
|
|
background-color: rgba(99, 102, 241, 0.2) !important;
|
|
color: var(--color-primary-400, #818cf8) !important;
|
|
}}
|
|
.overview-header-title {{
|
|
font-size: 1rem !important;
|
|
font-weight: 700 !important;
|
|
color: var(--color-font-important-light, #0f172a) !important;
|
|
margin: 0 !important;
|
|
line-height: 1.3 !important;
|
|
}}
|
|
.dark .overview-header-title {{
|
|
color: var(--color-font-important-dark, #f8fafc) !important;
|
|
}}
|
|
.overview-header-sub {{
|
|
font-size: 0.75rem !important;
|
|
color: var(--color-font-subtle-light, #64748b) !important;
|
|
margin-top: 0.25rem !important;
|
|
margin-bottom: 0 !important;
|
|
}}
|
|
.dark .overview-header-sub {{
|
|
color: var(--color-font-subtle-dark, #94a3b8) !important;
|
|
}}
|
|
.unfold-btn-primary {{
|
|
display: inline-flex !important;
|
|
align-items: center !important;
|
|
justify-content: center !important;
|
|
gap: 0.5rem !important;
|
|
padding: 0.5rem 1rem !important;
|
|
border-radius: var(--border-radius, 0.375rem) !important;
|
|
background-color: var(--color-primary-600, #4f46e5) !important;
|
|
color: #ffffff !important;
|
|
font-size: 0.75rem !important;
|
|
font-weight: 600 !important;
|
|
border: 1px solid transparent !important;
|
|
cursor: pointer !important;
|
|
transition: all 0.15s ease-in-out !important;
|
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;
|
|
white-space: nowrap !important;
|
|
text-decoration: none !important;
|
|
}}
|
|
.unfold-btn-primary:hover {{
|
|
background-color: var(--color-primary-700, #4338ca) !important;
|
|
color: #ffffff !important;
|
|
}}
|
|
.unfold-btn-secondary {{
|
|
display: inline-flex !important;
|
|
align-items: center !important;
|
|
justify-content: center !important;
|
|
gap: 0.5rem !important;
|
|
padding: 0.5rem 0.875rem !important;
|
|
border-radius: var(--border-radius, 0.375rem) !important;
|
|
background-color: var(--color-base-50, #f8fafc) !important;
|
|
color: var(--color-font-important-light, #0f172a) !important;
|
|
border: 1px solid var(--color-base-300, #cbd5e1) !important;
|
|
font-size: 0.75rem !important;
|
|
font-weight: 600 !important;
|
|
cursor: pointer !important;
|
|
transition: all 0.15s ease-in-out !important;
|
|
white-space: nowrap !important;
|
|
text-decoration: none !important;
|
|
}}
|
|
.unfold-btn-secondary:hover {{
|
|
background-color: var(--color-base-100, #f1f5f9) !important;
|
|
border-color: var(--color-base-400, #94a3b8) !important;
|
|
color: var(--color-font-important-light, #0f172a) !important;
|
|
}}
|
|
.dark .unfold-btn-secondary {{
|
|
background-color: var(--color-base-800, #1e293b) !important;
|
|
color: var(--color-font-important-dark, #f8fafc) !important;
|
|
border-color: var(--color-base-700, #334155) !important;
|
|
}}
|
|
.dark .unfold-btn-secondary:hover {{
|
|
background-color: var(--color-base-700, #334155) !important;
|
|
color: var(--color-font-important-dark, #f8fafc) !important;
|
|
}}
|
|
.overview-metric-grid {{
|
|
display: grid !important;
|
|
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
|
|
gap: 1rem !important;
|
|
}}
|
|
@media (min-width: 640px) {{
|
|
.overview-metric-grid {{
|
|
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
|
}}
|
|
}}
|
|
@media (min-width: 1024px) {{
|
|
.overview-metric-grid {{
|
|
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
|
|
}}
|
|
}}
|
|
.overview-metric-box {{
|
|
display: flex !important;
|
|
flex-direction: column !important;
|
|
justify-content: space-between !important;
|
|
padding: 1.125rem !important;
|
|
border-radius: var(--border-radius, 0.5rem) !important;
|
|
border: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
background-color: var(--color-base-50, #f8fafc) !important;
|
|
min-height: 124px !important;
|
|
box-sizing: border-box !important;
|
|
transition: all 0.15s ease-in-out !important;
|
|
}}
|
|
.dark .overview-metric-box {{
|
|
border-color: var(--color-base-800, #1e293b) !important;
|
|
background-color: rgba(30, 41, 59, 0.4) !important;
|
|
}}
|
|
.overview-metric-box:hover {{
|
|
border-color: var(--color-base-300, #cbd5e1) !important;
|
|
background-color: #ffffff !important;
|
|
}}
|
|
.dark .overview-metric-box:hover {{
|
|
border-color: var(--color-base-700, #334155) !important;
|
|
background-color: rgba(30, 41, 59, 0.7) !important;
|
|
}}
|
|
.overview-metric-label {{
|
|
font-size: 0.6875rem !important;
|
|
font-weight: 600 !important;
|
|
text-transform: uppercase !important;
|
|
letter-spacing: 0.05em !important;
|
|
color: var(--color-font-subtle-light, #64748b) !important;
|
|
}}
|
|
.dark .overview-metric-label {{
|
|
color: var(--color-font-subtle-dark, #94a3b8) !important;
|
|
}}
|
|
.overview-metric-content {{
|
|
margin: 0.5rem 0 !important;
|
|
display: flex !important;
|
|
flex-wrap: wrap !important;
|
|
align-items: center !important;
|
|
gap: 0.375rem !important;
|
|
}}
|
|
.overview-metric-hint {{
|
|
font-size: 0.6875rem !important;
|
|
color: var(--color-font-subtle-light, #64748b) !important;
|
|
line-height: 1.4 !important;
|
|
}}
|
|
.dark .overview-metric-hint {{
|
|
color: var(--color-font-subtle-dark, #94a3b8) !important;
|
|
}}
|
|
.overview-badge {{
|
|
display: inline-flex !important;
|
|
align-items: center !important;
|
|
gap: 0.375rem !important;
|
|
padding: 0.25rem 0.625rem !important;
|
|
border-radius: var(--border-radius, 0.375rem) !important;
|
|
font-size: 0.75rem !important;
|
|
font-weight: 600 !important;
|
|
line-height: 1.25 !important;
|
|
box-sizing: border-box !important;
|
|
}}
|
|
.overview-badge-role {{
|
|
background-color: rgba(99, 102, 241, 0.1) !important;
|
|
border: 1px solid rgba(99, 102, 241, 0.25) !important;
|
|
color: var(--color-primary-700, #4338ca) !important;
|
|
}}
|
|
.dark .overview-badge-role {{
|
|
background-color: rgba(99, 102, 241, 0.2) !important;
|
|
border-color: rgba(99, 102, 241, 0.4) !important;
|
|
color: var(--color-primary-300, #a5b4fc) !important;
|
|
}}
|
|
.overview-badge-success {{
|
|
background-color: rgba(16, 185, 129, 0.1) !important;
|
|
border: 1px solid rgba(16, 185, 129, 0.25) !important;
|
|
color: #047857 !important;
|
|
}}
|
|
.dark .overview-badge-success {{
|
|
background-color: rgba(16, 185, 129, 0.2) !important;
|
|
border-color: rgba(16, 185, 129, 0.4) !important;
|
|
color: #6ee7b7 !important;
|
|
}}
|
|
.overview-badge-extra {{
|
|
background-color: #ffffff !important;
|
|
border: 1px solid var(--color-base-300, #cbd5e1) !important;
|
|
color: var(--color-font-important-light, #0f172a) !important;
|
|
padding: 0.2rem 0.5rem !important;
|
|
border-radius: var(--border-radius, 0.25rem) !important;
|
|
font-size: 0.6875rem !important;
|
|
font-weight: 600 !important;
|
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03) !important;
|
|
}}
|
|
.dark .overview-badge-extra {{
|
|
background-color: var(--color-base-800, #1e293b) !important;
|
|
border-color: var(--color-base-700, #334155) !important;
|
|
color: var(--color-font-important-dark, #f8fafc) !important;
|
|
}}
|
|
.overview-badge-region {{
|
|
background-color: rgba(16, 185, 129, 0.08) !important;
|
|
border: 1px solid rgba(16, 185, 129, 0.2) !important;
|
|
color: #065f46 !important;
|
|
padding: 0.2rem 0.5rem !important;
|
|
border-radius: var(--border-radius, 0.25rem) !important;
|
|
font-size: 0.6875rem !important;
|
|
font-weight: 600 !important;
|
|
}}
|
|
.dark .overview-badge-region {{
|
|
background-color: rgba(16, 185, 129, 0.15) !important;
|
|
border-color: rgba(16, 185, 129, 0.3) !important;
|
|
color: #a7f3d0 !important;
|
|
}}
|
|
.overview-small-btn {{
|
|
display: inline-flex !important;
|
|
align-items: center !important;
|
|
gap: 0.25rem !important;
|
|
padding: 0.2rem 0.5rem !important;
|
|
border-radius: var(--border-radius, 0.25rem) !important;
|
|
font-size: 0.6875rem !important;
|
|
font-weight: 600 !important;
|
|
background-color: var(--color-base-100, #f1f5f9) !important;
|
|
border: 1px solid var(--color-base-300, #cbd5e1) !important;
|
|
color: var(--color-font-important-light, #0f172a) !important;
|
|
cursor: pointer !important;
|
|
transition: all 0.15s ease-in-out !important;
|
|
}}
|
|
.overview-small-btn:hover {{
|
|
background-color: var(--color-base-200, #e2e8f0) !important;
|
|
}}
|
|
.dark .overview-small-btn {{
|
|
background-color: var(--color-base-800, #1e293b) !important;
|
|
border-color: var(--color-base-700, #334155) !important;
|
|
color: var(--color-font-important-dark, #f8fafc) !important;
|
|
}}
|
|
.dark .overview-small-btn:hover {{
|
|
background-color: var(--color-base-700, #334155) !important;
|
|
}}
|
|
#access-modal-overlay.access-modal-backdrop {{
|
|
position: fixed !important;
|
|
top: 0 !important;
|
|
left: 0 !important;
|
|
right: 0 !important;
|
|
bottom: 0 !important;
|
|
width: 100vw !important;
|
|
height: 100vh !important;
|
|
z-index: 2147483647 !important;
|
|
background-color: rgba(15, 23, 42, 0.75) !important;
|
|
backdrop-filter: blur(6px) !important;
|
|
-webkit-backdrop-filter: blur(6px) !important;
|
|
display: none;
|
|
align-items: center !important;
|
|
justify-content: center !important;
|
|
padding: 1.5rem !important;
|
|
box-sizing: border-box !important;
|
|
overflow: hidden !important;
|
|
}}
|
|
#access-modal-overlay.access-modal-backdrop.is-open {{
|
|
display: flex !important;
|
|
}}
|
|
#access-modal-dialog.access-modal-window {{
|
|
width: 95vw !important;
|
|
max-width: 980px !important;
|
|
height: 85vh !important;
|
|
max-height: 85vh !important;
|
|
min-height: 480px !important;
|
|
display: flex !important;
|
|
flex-direction: column !important;
|
|
background-color: var(--color-base-0, #ffffff) !important;
|
|
border-radius: var(--border-radius, 0.75rem) !important;
|
|
border: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.08) !important;
|
|
overflow: hidden !important;
|
|
position: relative !important;
|
|
box-sizing: border-box !important;
|
|
margin: auto !important;
|
|
}}
|
|
.dark #access-modal-dialog.access-modal-window {{
|
|
background-color: var(--color-base-900, #0f172a) !important;
|
|
border-color: var(--color-base-800, #1e293b) !important;
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7) !important;
|
|
}}
|
|
.access-modal-header {{
|
|
flex-shrink: 0 !important;
|
|
padding: 1.125rem 1.5rem !important;
|
|
border-bottom: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
background-color: var(--color-base-50, #f8fafc) !important;
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
justify-content: space-between !important;
|
|
user-select: none !important;
|
|
}}
|
|
.dark .access-modal-header {{
|
|
border-bottom-color: var(--color-base-800, #1e293b) !important;
|
|
background-color: rgba(30, 41, 59, 0.7) !important;
|
|
}}
|
|
.access-modal-tabs-bar {{
|
|
flex-shrink: 0 !important;
|
|
padding: 0.625rem 1.5rem 0.375rem 1.5rem !important;
|
|
border-bottom: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
background-color: var(--color-base-0, #ffffff) !important;
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
gap: 0.5rem !important;
|
|
overflow-x: auto !important;
|
|
user-select: none !important;
|
|
}}
|
|
.dark .access-modal-tabs-bar {{
|
|
border-bottom-color: var(--color-base-800, #1e293b) !important;
|
|
background-color: var(--color-base-900, #0f172a) !important;
|
|
}}
|
|
.access-tab-btn {{
|
|
display: inline-flex !important;
|
|
align-items: center !important;
|
|
gap: 0.5rem !important;
|
|
padding: 0.5rem 1rem !important;
|
|
border-radius: var(--border-radius, 0.375rem) !important;
|
|
font-size: 0.75rem !important;
|
|
font-weight: 500 !important;
|
|
border: 1px solid transparent !important;
|
|
cursor: pointer !important;
|
|
transition: all 0.15s ease-in-out !important;
|
|
background: transparent !important;
|
|
color: var(--color-font-default-light, #475569) !important;
|
|
white-space: nowrap !important;
|
|
}}
|
|
.dark .access-tab-btn {{
|
|
color: var(--color-font-default-dark, #94a3b8) !important;
|
|
}}
|
|
.access-tab-btn:hover {{
|
|
background-color: var(--color-base-100, #f1f5f9) !important;
|
|
color: var(--color-font-important-light, #0f172a) !important;
|
|
}}
|
|
.dark .access-tab-btn:hover {{
|
|
background-color: var(--color-base-800, #1e293b) !important;
|
|
color: var(--color-font-important-dark, #f8fafc) !important;
|
|
}}
|
|
.access-tab-btn.active {{
|
|
background-color: rgba(99, 102, 241, 0.08) !important;
|
|
border-color: rgba(99, 102, 241, 0.3) !important;
|
|
color: var(--color-primary-600, #4f46e5) !important;
|
|
font-weight: 700 !important;
|
|
}}
|
|
.dark .access-tab-btn.active {{
|
|
background-color: rgba(99, 102, 241, 0.2) !important;
|
|
border-color: rgba(99, 102, 241, 0.5) !important;
|
|
color: var(--color-primary-300, #a5b4fc) !important;
|
|
}}
|
|
.access-modal-body {{
|
|
flex: 1 1 0% !important;
|
|
min-height: 0 !important;
|
|
height: auto !important;
|
|
max-height: 100% !important;
|
|
overflow-y: auto !important;
|
|
overflow-x: hidden !important;
|
|
overscroll-behavior: contain !important;
|
|
-webkit-overflow-scrolling: touch !important;
|
|
padding: 1.5rem !important;
|
|
box-sizing: border-box !important;
|
|
}}
|
|
.access-modal-body::-webkit-scrollbar {{
|
|
width: 7px;
|
|
}}
|
|
.access-modal-body::-webkit-scrollbar-track {{
|
|
background: transparent;
|
|
}}
|
|
.access-modal-body::-webkit-scrollbar-thumb {{
|
|
background: rgba(148, 163, 184, 0.5);
|
|
border-radius: 9999px;
|
|
}}
|
|
.access-modal-body::-webkit-scrollbar-thumb:hover {{
|
|
background: rgba(100, 116, 139, 0.8);
|
|
}}
|
|
.dark .access-modal-body::-webkit-scrollbar-thumb {{
|
|
background: rgba(71, 85, 105, 0.6);
|
|
}}
|
|
.dark .access-modal-body::-webkit-scrollbar-thumb:hover {{
|
|
background: rgba(100, 116, 139, 0.9);
|
|
}}
|
|
.access-modal-footer {{
|
|
flex-shrink: 0 !important;
|
|
padding: 1rem 1.5rem !important;
|
|
border-top: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
background-color: var(--color-base-50, #f8fafc) !important;
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
justify-content: space-between !important;
|
|
gap: 1rem !important;
|
|
user-select: none !important;
|
|
flex-wrap: wrap !important;
|
|
}}
|
|
.dark .access-modal-footer {{
|
|
border-top-color: var(--color-base-800, #1e293b) !important;
|
|
background-color: rgba(30, 41, 59, 0.7) !important;
|
|
}}
|
|
.modal-role-card {{
|
|
border-radius: var(--border-radius, 0.5rem) !important;
|
|
padding: 1rem !important;
|
|
border: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
background-color: var(--color-base-0, #ffffff) !important;
|
|
cursor: pointer !important;
|
|
transition: all 0.15s ease-in-out !important;
|
|
box-sizing: border-box !important;
|
|
}}
|
|
.dark .modal-role-card {{
|
|
border-color: var(--color-base-800, #1e293b) !important;
|
|
background-color: rgba(30, 41, 59, 0.3) !important;
|
|
}}
|
|
.modal-role-card:hover {{
|
|
border-color: var(--color-base-300, #cbd5e1) !important;
|
|
background-color: var(--color-base-50, #f8fafc) !important;
|
|
}}
|
|
.dark .modal-role-card:hover {{
|
|
border-color: var(--color-base-700, #334155) !important;
|
|
background-color: rgba(30, 41, 59, 0.6) !important;
|
|
}}
|
|
.modal-role-card.is-selected {{
|
|
border-color: var(--color-primary-600, #4f46e5) !important;
|
|
background-color: rgba(99, 102, 241, 0.05) !important;
|
|
box-shadow: 0 0 0 1px var(--color-primary-600, #4f46e5) !important;
|
|
}}
|
|
.dark .modal-role-card.is-selected {{
|
|
border-color: var(--color-primary-500, #6366f1) !important;
|
|
background-color: rgba(99, 102, 241, 0.15) !important;
|
|
box-shadow: 0 0 0 1px var(--color-primary-500, #6366f1) !important;
|
|
}}
|
|
.modal-region-card {{
|
|
border-radius: var(--border-radius, 0.5rem) !important;
|
|
padding: 0.875rem 1rem !important;
|
|
border: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
background-color: var(--color-base-0, #ffffff) !important;
|
|
cursor: pointer !important;
|
|
transition: all 0.15s ease-in-out !important;
|
|
box-sizing: border-box !important;
|
|
}}
|
|
.dark .modal-region-card {{
|
|
border-color: var(--color-base-800, #1e293b) !important;
|
|
background-color: rgba(30, 41, 59, 0.3) !important;
|
|
}}
|
|
.modal-region-card:hover {{
|
|
border-color: var(--color-base-300, #cbd5e1) !important;
|
|
}}
|
|
.modal-region-card.is-selected {{
|
|
border-color: #10b981 !important;
|
|
background-color: rgba(16, 185, 129, 0.08) !important;
|
|
box-shadow: 0 0 0 1px #10b981 !important;
|
|
}}
|
|
.dark .modal-region-card.is-selected {{
|
|
border-color: #10b981 !important;
|
|
background-color: rgba(16, 185, 129, 0.2) !important;
|
|
box-shadow: 0 0 0 1px #10b981 !important;
|
|
}}
|
|
.modal-group-chip {{
|
|
display: inline-flex !important;
|
|
align-items: center !important;
|
|
gap: 0.375rem !important;
|
|
padding: 0.375rem 0.75rem !important;
|
|
border-radius: var(--border-radius, 0.375rem) !important;
|
|
border: 1px solid var(--color-base-300, #cbd5e1) !important;
|
|
background-color: #ffffff !important;
|
|
color: var(--color-font-important-light, #0f172a) !important;
|
|
font-size: 0.75rem !important;
|
|
font-weight: 500 !important;
|
|
cursor: pointer !important;
|
|
transition: all 0.15s ease-in-out !important;
|
|
}}
|
|
.dark .modal-group-chip {{
|
|
border-color: var(--color-base-700, #334155) !important;
|
|
background-color: var(--color-base-800, #1e293b) !important;
|
|
color: var(--color-font-important-dark, #f8fafc) !important;
|
|
}}
|
|
.modal-group-chip.is-checked {{
|
|
background-color: rgba(99, 102, 241, 0.08) !important;
|
|
border-color: var(--color-primary-600, #4f46e5) !important;
|
|
color: var(--color-primary-700, #4338ca) !important;
|
|
font-weight: 700 !important;
|
|
}}
|
|
.dark .modal-group-chip.is-checked {{
|
|
background-color: rgba(99, 102, 241, 0.2) !important;
|
|
border-color: var(--color-primary-500, #6366f1) !important;
|
|
color: var(--color-primary-300, #a5b4fc) !important;
|
|
}}
|
|
.matrix-cat-card {{
|
|
border-radius: var(--border-radius, 0.5rem) !important;
|
|
border: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
background-color: var(--color-base-0, #ffffff) !important;
|
|
overflow: hidden !important;
|
|
margin-bottom: 0.75rem !important;
|
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03) !important;
|
|
}}
|
|
.dark .matrix-cat-card {{
|
|
border-color: var(--color-base-800, #1e293b) !important;
|
|
background-color: var(--color-base-900, #0f172a) !important;
|
|
}}
|
|
.matrix-cat-header {{
|
|
padding: 0.75rem 1.125rem !important;
|
|
background-color: var(--color-base-50, #f8fafc) !important;
|
|
border-bottom: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
justify-content: space-between !important;
|
|
cursor: pointer !important;
|
|
user-select: none !important;
|
|
}}
|
|
.dark .matrix-cat-header {{
|
|
background-color: rgba(30, 41, 59, 0.5) !important;
|
|
border-bottom-color: var(--color-base-800, #1e293b) !important;
|
|
}}
|
|
.matrix-cat-body {{
|
|
padding: 0.875rem !important;
|
|
display: grid !important;
|
|
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
|
|
gap: 0.5rem !important;
|
|
}}
|
|
@media (min-width: 768px) {{
|
|
.matrix-cat-body {{
|
|
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
|
}}
|
|
}}
|
|
.matrix-perm-row {{
|
|
padding: 0.5rem 0.75rem !important;
|
|
border-radius: var(--border-radius, 0.375rem) !important;
|
|
border: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
background-color: var(--color-base-50, #f8fafc) !important;
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
justify-content: space-between !important;
|
|
transition: all 0.15s ease-in-out !important;
|
|
}}
|
|
.dark .matrix-perm-row {{
|
|
border-color: var(--color-base-800, #1e293b) !important;
|
|
background-color: rgba(30, 41, 59, 0.25) !important;
|
|
}}
|
|
.matrix-perm-row:hover {{
|
|
border-color: var(--color-base-300, #cbd5e1) !important;
|
|
background-color: #ffffff !important;
|
|
}}
|
|
.dark .matrix-perm-row:hover {{
|
|
border-color: var(--color-base-700, #334155) !important;
|
|
background-color: rgba(30, 41, 59, 0.5) !important;
|
|
}}
|
|
.perm-btn-group {{
|
|
display: inline-flex !important;
|
|
border-radius: var(--border-radius, 0.375rem) !important;
|
|
padding: 2px !important;
|
|
background-color: var(--color-base-200, #e2e8f0) !important;
|
|
border: 1px solid var(--color-base-300, #cbd5e1) !important;
|
|
flex-shrink: 0 !important;
|
|
}}
|
|
.dark .perm-btn-group {{
|
|
background-color: var(--color-base-800, #1e293b) !important;
|
|
border-color: var(--color-base-700, #334155) !important;
|
|
}}
|
|
.perm-btn {{
|
|
padding: 0.25rem 0.5rem !important;
|
|
border-radius: var(--border-radius, 0.25rem) !important;
|
|
font-size: 0.6875rem !important;
|
|
font-weight: 500 !important;
|
|
border: none !important;
|
|
cursor: pointer !important;
|
|
transition: all 0.15s ease-in-out !important;
|
|
background: transparent !important;
|
|
color: var(--color-font-default-light, #64748b) !important;
|
|
}}
|
|
.dark .perm-btn {{
|
|
color: var(--color-font-default-dark, #94a3b8) !important;
|
|
}}
|
|
.perm-btn.is-active-inherit {{
|
|
background-color: #ffffff !important;
|
|
color: var(--color-font-important-light, #0f172a) !important;
|
|
font-weight: 700 !important;
|
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;
|
|
}}
|
|
.dark .perm-btn.is-active-inherit {{
|
|
background-color: var(--color-base-700, #334155) !important;
|
|
color: #ffffff !important;
|
|
}}
|
|
.perm-btn.is-active-allow {{
|
|
background-color: #10b981 !important;
|
|
color: #ffffff !important;
|
|
font-weight: 700 !important;
|
|
box-shadow: 0 1px 2px 0 rgba(16, 185, 129, 0.3) !important;
|
|
}}
|
|
.perm-btn.is-active-deny {{
|
|
background-color: #ef4444 !important;
|
|
color: #ffffff !important;
|
|
font-weight: 700 !important;
|
|
box-shadow: 0 1px 2px 0 rgba(239, 68, 68, 0.3) !important;
|
|
}}
|
|
.unfold-panel-box {{
|
|
border-radius: var(--border-radius, 0.5rem) !important;
|
|
padding: 1.125rem !important;
|
|
border: 1px solid var(--color-base-200, #e2e8f0) !important;
|
|
background-color: var(--color-base-50, #f8fafc) !important;
|
|
box-sizing: border-box !important;
|
|
}}
|
|
.dark .unfold-panel-box {{
|
|
border-color: var(--color-base-800, #1e293b) !important;
|
|
background-color: rgba(30, 41, 59, 0.35) !important;
|
|
}}
|
|
.unfold-input-field,
|
|
#modal-perm-search,
|
|
#modal-user-type-select,
|
|
#modal-institution-input,
|
|
#select-system-perm-to-add,
|
|
#input-custom-perm-key {{
|
|
border-radius: var(--border-radius, 0.375rem) !important;
|
|
padding: 0.5rem 0.75rem !important;
|
|
border: 1px solid var(--color-base-300, #cbd5e1) !important;
|
|
background-color: var(--color-base-0, #ffffff) !important;
|
|
color: var(--color-font-important-light, #0f172a) !important;
|
|
font-size: 0.75rem !important;
|
|
outline: none !important;
|
|
box-sizing: border-box !important;
|
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out !important;
|
|
}}
|
|
.dark .unfold-input-field,
|
|
.dark #modal-perm-search,
|
|
.dark #modal-user-type-select,
|
|
.dark #modal-institution-input,
|
|
.dark #select-system-perm-to-add,
|
|
.dark #input-custom-perm-key {{
|
|
border-color: var(--color-base-700, #334155) !important;
|
|
background-color: var(--color-base-800, #1e293b) !important;
|
|
color: var(--color-font-important-dark, #f8fafc) !important;
|
|
}}
|
|
.unfold-input-field:focus,
|
|
#modal-perm-search:focus,
|
|
#modal-user-type-select:focus,
|
|
#modal-institution-input:focus,
|
|
#select-system-perm-to-add:focus,
|
|
#input-custom-perm-key:focus {{
|
|
border-color: var(--color-primary-600, #4f46e5) !important;
|
|
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2) !important;
|
|
}}
|
|
body.access-modal-open,
|
|
html.access-modal-open {{
|
|
overflow: hidden !important;
|
|
height: 100% !important;
|
|
}}
|
|
body.access-modal-open #page,
|
|
body.access-modal-open #main,
|
|
body.access-modal-open #content {{
|
|
overflow: hidden !important;
|
|
}}
|
|
</style>
|
|
|
|
<div id="access-modal-overlay" class="access-modal-backdrop" dir="{layout_dir}">
|
|
<div id="access-modal-dialog" class="access-modal-window">
|
|
|
|
<!-- Modal Header -->
|
|
<div id="access-modal-header" class="access-modal-header">
|
|
<div class="flex items-center gap-3">
|
|
<div class="overview-header-icon-box" style="width: 2.25rem; height: 2.25rem;">
|
|
<span class="material-symbols-outlined" style="font-size: 20px;">admin_panel_settings</span>
|
|
</div>
|
|
<div>
|
|
<h3 class="overview-header-title" style="font-size: 0.9375rem;">{t["modal_title"]}</h3>
|
|
<p class="overview-header-sub">{t["modal_sub"]}</p>
|
|
</div>
|
|
</div>
|
|
<button type="button" id="btn-close-access-modal-x" class="overview-small-btn" style="padding: 0.35rem 0.6rem;">
|
|
<span class="material-symbols-outlined" style="font-size: 18px;">close</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 4 Navigation Tabs -->
|
|
<div id="access-modal-tabs-nav" class="access-modal-tabs-bar">
|
|
<div class="flex items-center gap-1.5 overflow-x-auto" id="access-modal-tabs-container">
|
|
<button type="button" class="access-tab-btn active" data-tab="tab-roles">
|
|
<span class="material-symbols-outlined" style="font-size: 16px;">badge</span>
|
|
<span>{t["tab_roles"]}</span>
|
|
</button>
|
|
<button type="button" class="access-tab-btn" data-tab="tab-permissions">
|
|
<span class="material-symbols-outlined" style="font-size: 16px;">tune</span>
|
|
<span>{t["tab_permissions"]}</span>
|
|
<span id="tab-extra-count-badge" class="hidden overview-badge overview-badge-role" style="padding: 0.1rem 0.4rem; font-size: 0.625rem;">+0</span>
|
|
</button>
|
|
<button type="button" class="access-tab-btn" data-tab="tab-institutions">
|
|
<span class="material-symbols-outlined" style="font-size: 16px;">apartment</span>
|
|
<span>{t["tab_institutions"]}</span>
|
|
</button>
|
|
<button type="button" class="access-tab-btn" data-tab="tab-regions">
|
|
<span class="material-symbols-outlined" style="font-size: 16px;">public</span>
|
|
<span>{t["tab_regions"]}</span>
|
|
<span id="tab-regions-count-badge" class="hidden overview-badge overview-badge-success" style="padding: 0.1rem 0.4rem; font-size: 0.625rem;">0</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Body (SCROLLABLE CONTAINER) -->
|
|
<div class="access-modal-body space-y-6" id="modal-tabs-body">
|
|
|
|
<!-- ========================================== -->
|
|
<!-- TAB 1: ROLES & USER TYPES & GROUPS -->
|
|
<!-- ========================================== -->
|
|
<div id="tab-roles" class="access-tab-content space-y-5">
|
|
<div>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<label class="text-xs font-bold text-font-important-light dark:text-font-important-dark">
|
|
{t["select_role_title"]}
|
|
</label>
|
|
<span class="text-[11px] text-font-subtle-light dark:text-font-subtle-dark">
|
|
{t["select_role_sub"]}
|
|
</span>
|
|
</div>
|
|
<!-- Role Cards Selector Grid -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3" id="modal-role-cards-container">
|
|
<!-- Rendered by JS from roles_data -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Role Inherited Permissions Preview Box -->
|
|
<div class="unfold-panel-box space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-2 text-xs font-bold text-font-important-light dark:text-font-important-dark">
|
|
<span class="material-symbols-outlined text-base text-emerald-600">verified</span>
|
|
<span>{t["default_perms_label"]}</span>
|
|
</div>
|
|
<span class="text-xs font-semibold text-primary-600 dark:text-primary-400" id="role-preview-count">0 {t["active_perms_count"]}</span>
|
|
</div>
|
|
<div class="flex flex-wrap gap-1.5 pt-1" id="role-preview-badges">
|
|
<!-- Filled by JS -->
|
|
</div>
|
|
<p class="text-[11px] text-font-subtle-light dark:text-font-subtle-dark pt-1">
|
|
{t["role_hint"]}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- User Type & Groups Grid -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
|
|
<!-- User Type -->
|
|
<div class="space-y-1.5">
|
|
<label class="text-xs font-bold text-font-important-light dark:text-font-important-dark">{t["user_type_label"]}</label>
|
|
<select id="modal-user-type-select" class="unfold-input-field w-full">
|
|
<option value="superadmin">{t["ut_superadmin"]}</option>
|
|
<option value="regional_admin">{t["ut_regional_admin"]}</option>
|
|
<option value="institution_admin">{t["ut_institution_admin"]}</option>
|
|
<option value="editor">{t["ut_editor"]}</option>
|
|
<option value="staff">{t["ut_staff"]}</option>
|
|
<option value="client">{t["ut_client"]}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Django Groups Pills -->
|
|
<div class="space-y-1.5">
|
|
<label class="text-xs font-bold text-font-important-light dark:text-font-important-dark">{t["groups_label"]}</label>
|
|
<div class="flex flex-wrap gap-2 pt-1" id="modal-groups-container">
|
|
<!-- Rendered by JS -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ========================================== -->
|
|
<!-- TAB 2: PERMISSIONS MATRIX & ADD PERMISSION -->
|
|
<!-- ========================================== -->
|
|
<div id="tab-permissions" class="access-tab-content space-y-4 hidden">
|
|
<!-- Top Filter & Action Bar -->
|
|
<div class="unfold-panel-box flex flex-col md:flex-row md:items-center justify-between gap-3">
|
|
<div>
|
|
<h4 class="text-xs font-bold text-font-important-light dark:text-font-important-dark flex items-center gap-1.5">
|
|
<span class="material-symbols-outlined text-base text-primary-600 dark:text-primary-400">tune</span>
|
|
<span>{t["matrix_title"]}</span>
|
|
</h4>
|
|
<p class="text-[11px] text-font-subtle-light dark:text-font-subtle-dark mt-0.5">
|
|
{t["matrix_sub"]}
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<!-- Search input -->
|
|
<div class="relative w-full md:w-56">
|
|
<input type="text" id="modal-perm-search" placeholder="{t['search_placeholder']}" class="unfold-input-field w-full">
|
|
</div>
|
|
<button type="button" id="modal-reset-overrides-btn" class="unfold-btn-secondary">
|
|
{t["reset_btn"]}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DEDICATED ADD NEW PERMISSION PANEL -->
|
|
<div class="unfold-panel-box space-y-3" id="panel-add-new-permission" style="border-color: rgba(99, 102, 241, 0.3); background-color: rgba(99, 102, 241, 0.03);">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<span class="overview-header-icon-box" style="width: 1.75rem; height: 1.75rem; border-radius: var(--border-radius, 0.25rem);">
|
|
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
|
|
</span>
|
|
<span class="text-xs font-bold text-font-important-light dark:text-font-important-dark">{t["add_perm_panel_title"]}</span>
|
|
</div>
|
|
<span class="text-[11px] text-font-subtle-light dark:text-font-subtle-dark">{t["add_perm_panel_sub"]}</span>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 pt-1">
|
|
<!-- Select from all system permissions -->
|
|
<div class="flex items-center gap-2">
|
|
<select id="select-system-perm-to-add" class="unfold-input-field flex-1">
|
|
<option value="">{t["select_system_perm_default"]}</option>
|
|
<!-- Populated by JS -->
|
|
</select>
|
|
<button type="button" id="btn-add-system-perm" class="unfold-btn-primary">
|
|
{t["btn_grant"]}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Add custom permission code -->
|
|
<div class="flex items-center gap-2">
|
|
<input type="text" id="input-custom-perm-key" placeholder="{t['custom_key_placeholder']}" class="unfold-input-field flex-1">
|
|
<button type="button" id="btn-add-custom-perm" class="unfold-btn-secondary">
|
|
{t["btn_add_key"]}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Categories List -->
|
|
<div class="flex flex-col gap-2" id="modal-categories-list">
|
|
<!-- Populated by JS -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ========================================== -->
|
|
<!-- TAB 3: INSTITUTIONS ACCESS -->
|
|
<!-- ========================================== -->
|
|
<div id="tab-institutions" class="access-tab-content space-y-5 hidden">
|
|
<div class="unfold-panel-box space-y-3">
|
|
<div class="flex items-center gap-2.5">
|
|
<div class="overview-header-icon-box" style="width: 2.25rem; height: 2.25rem;">
|
|
<span class="material-symbols-outlined" style="font-size: 20px;">apartment</span>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-xs font-bold text-font-important-light dark:text-font-important-dark">{t["inst_title"]}</h4>
|
|
<p class="text-[11px] text-font-subtle-light dark:text-font-subtle-dark">{t["inst_sub"]}</p>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1.5 pt-2">
|
|
<label class="text-xs font-semibold text-font-subtle-light dark:text-font-subtle-dark">{t["inst_input_label"]}</label>
|
|
<input type="text" id="modal-institution-input" placeholder="{t['inst_input_placeholder']}" class="unfold-input-field w-full">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="unfold-panel-box space-y-2">
|
|
<h5 class="text-xs font-bold text-font-important-light dark:text-font-important-dark">{t["inst_guidelines_title"]}</h5>
|
|
<ul class="text-xs text-font-subtle-light dark:text-font-subtle-dark space-y-1 list-disc list-inside">
|
|
<li>{t["inst_guideline_1"]}</li>
|
|
<li>{t["inst_guideline_2"]}</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ========================================== -->
|
|
<!-- TAB 4: REGIONS ACCESS (Managed Regions) -->
|
|
<!-- ========================================== -->
|
|
<div id="tab-regions" class="access-tab-content space-y-5 hidden">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h4 class="text-xs font-bold text-font-important-light dark:text-font-important-dark flex items-center gap-1.5">
|
|
<span class="material-symbols-outlined text-base text-emerald-600">public</span>
|
|
<span>{t["regions_title"]}</span>
|
|
</h4>
|
|
<p class="text-[11px] text-font-subtle-light dark:text-font-subtle-dark mt-0.5">
|
|
{t["regions_sub"]}
|
|
</p>
|
|
</div>
|
|
<span class="text-xs font-semibold text-emerald-600" id="modal-regions-selected-counter">0 {t["regions_selected_count"]}</span>
|
|
</div>
|
|
|
|
<!-- Regions Cards Grid -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2.5" id="modal-regions-cards-container">
|
|
<!-- Rendered by JS -->
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Modal Footer -->
|
|
<div id="access-modal-footer" class="access-modal-footer">
|
|
<div class="text-xs text-font-subtle-light dark:text-font-subtle-dark flex items-center gap-2 flex-wrap" id="modal-footer-summary">
|
|
<span>{t["footer_role"]} <strong id="footer-role-name" class="text-font-important-light dark:text-font-important-dark">{t["footer_no_role"]}</strong></span>
|
|
<span id="footer-extra-badge" class="hidden font-semibold text-primary-600 dark:text-primary-400">• 0 {t["footer_extra"]}</span>
|
|
<span id="footer-regions-badge" class="hidden font-semibold text-emerald-600 dark:text-emerald-400">• 0 {t["footer_regions"]}</span>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2.5 self-end sm:self-auto">
|
|
<button type="button" id="btn-cancel-access-modal" class="unfold-btn-secondary">
|
|
{t["btn_cancel"]}
|
|
</button>
|
|
<button type="button" id="btn-apply-access-modal" class="unfold-btn-primary">
|
|
<span class="material-symbols-outlined" style="font-size: 16px;">check</span>
|
|
<span>{t["btn_apply"]}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- =================================================================== -->
|
|
<!-- JAVASCRIPT CONTROLLER -->
|
|
<!-- =================================================================== -->
|
|
<script>
|
|
(function() {{
|
|
const rolesEl = document.getElementById('access-data-roles');
|
|
const categoriesEl = document.getElementById('access-data-categories');
|
|
const regionsEl = document.getElementById('access-data-regions');
|
|
const groupsEl = document.getElementById('access-data-groups');
|
|
const initialEl = document.getElementById('access-data-initial');
|
|
const permsMapEl = document.getElementById('access-data-perms-map');
|
|
const i18nEl = document.getElementById('access-data-i18n');
|
|
|
|
const rolesData = rolesEl ? JSON.parse(rolesEl.textContent || '{{}}') : {{}};
|
|
const categoriesData = categoriesEl ? JSON.parse(categoriesEl.textContent || '[]') : [];
|
|
const regionsData = regionsEl ? JSON.parse(regionsEl.textContent || '[]') : [];
|
|
const groupsData = groupsEl ? JSON.parse(groupsEl.textContent || '[]') : [];
|
|
const initialData = initialEl ? JSON.parse(initialEl.textContent || '{{}}') : {{}};
|
|
const permsMap = permsMapEl ? JSON.parse(permsMapEl.textContent || '{{}}') : {{}};
|
|
const i18n = i18nEl ? JSON.parse(i18nEl.textContent || '{{}}') : {{}};
|
|
|
|
// Working State
|
|
let state = {{
|
|
roleId: initialData.roleId || "",
|
|
userType: initialData.userType || "client",
|
|
institution: initialData.institution || "",
|
|
groups: Array.isArray(initialData.groups) ? [...initialData.groups] : [],
|
|
regions: Array.isArray(initialData.regions) ? [...initialData.regions] : [],
|
|
overrides: initialData.overrides ? Object.assign({{}}, initialData.overrides) : {{}},
|
|
}};
|
|
|
|
const hiddenInput = document.getElementById('id_{escape(name)}');
|
|
|
|
// ─── DOM Elements ───────────────────────────────────────────
|
|
const modalOverlay = document.getElementById('access-modal-overlay');
|
|
const modalDialog = document.getElementById('access-modal-dialog');
|
|
const btnOpenModal = document.getElementById('btn-open-access-modal');
|
|
const btnHeaderAddPerm = document.getElementById('btn-header-add-perm');
|
|
const btnAddPermCard = document.getElementById('btn-add-perm-card');
|
|
const btnChangeRoleCard = document.getElementById('btn-change-role-card');
|
|
const btnCloseX = document.getElementById('btn-close-access-modal-x');
|
|
const btnCancel = document.getElementById('btn-cancel-access-modal');
|
|
const btnApply = document.getElementById('btn-apply-access-modal');
|
|
|
|
// Overview elements
|
|
const ovRoleName = document.getElementById('overview-role-name');
|
|
const ovPermStatus = document.getElementById('overview-perm-status');
|
|
const ovExtraPerms = document.getElementById('overview-extra-perms-container');
|
|
const ovInstitution = document.getElementById('overview-institution-name');
|
|
const ovRegions = document.getElementById('overview-regions-container');
|
|
|
|
// Modal elements
|
|
const roleCardsContainer = document.getElementById('modal-role-cards-container');
|
|
const rolePreviewCount = document.getElementById('role-preview-count');
|
|
const rolePreviewBadges = document.getElementById('role-preview-badges');
|
|
const userTypeSelect = document.getElementById('modal-user-type-select');
|
|
const groupsContainer = document.getElementById('modal-groups-container');
|
|
const permSearchInput = document.getElementById('modal-perm-search');
|
|
const resetOverridesBtn = document.getElementById('modal-reset-overrides-btn');
|
|
const categoriesListContainer = document.getElementById('modal-categories-list');
|
|
const institutionInput = document.getElementById('modal-institution-input');
|
|
const regionsContainer = document.getElementById('modal-regions-cards-container');
|
|
const regionsSelectedCounter = document.getElementById('modal-regions-selected-counter');
|
|
|
|
// Add New Permission elements
|
|
const selectSystemPerm = document.getElementById('select-system-perm-to-add');
|
|
const btnAddSystemPerm = document.getElementById('btn-add-system-perm');
|
|
const inputCustomPermKey = document.getElementById('input-custom-perm-key');
|
|
const btnAddCustomPerm = document.getElementById('btn-add-custom-perm');
|
|
|
|
// Footer & Tab badges
|
|
const tabExtraCountBadge = document.getElementById('tab-extra-count-badge');
|
|
const tabRegionsCountBadge = document.getElementById('tab-regions-count-badge');
|
|
const footerRoleName = document.getElementById('footer-role-name');
|
|
const footerExtraBadge = document.getElementById('footer-extra-badge');
|
|
const footerRegionsBadge = document.getElementById('footer-regions-badge');
|
|
|
|
// ─── Teleport Modal to <body> ───────────────────────────────
|
|
function ensureModalAttachedToBody() {{
|
|
const overlay = document.getElementById('access-modal-overlay');
|
|
if (overlay && document.body && overlay.parentElement !== document.body) {{
|
|
document.body.appendChild(overlay);
|
|
}}
|
|
}}
|
|
|
|
// ─── Helper: Get Active Role Permissions ────────────────────
|
|
function getActiveRolePerms() {{
|
|
if (state.roleId && rolesData[state.roleId]) {{
|
|
return rolesData[state.roleId].permissions || {{}};
|
|
}}
|
|
return {{}};
|
|
}}
|
|
|
|
// ─── Helper: Get Extra & Denied Permissions ─────────────────
|
|
function getExtraAndDeniedPerms() {{
|
|
const rolePerms = getActiveRolePerms();
|
|
const extra = [];
|
|
const denied = [];
|
|
for (const [key, val] of Object.entries(state.overrides)) {{
|
|
if (val === true && !rolePerms[key]) {{
|
|
extra.push(key);
|
|
}} else if (val === false && rolePerms[key]) {{
|
|
denied.push(key);
|
|
}}
|
|
}}
|
|
return {{ extra, denied }};
|
|
}}
|
|
|
|
// ─── Tab Switching Helper ───────────────────────────────────
|
|
function switchTab(targetTabId) {{
|
|
document.querySelectorAll('.access-tab-btn').forEach(b => {{
|
|
b.classList.remove('active');
|
|
}});
|
|
const activeBtn = document.querySelector(`.access-tab-btn[data-tab="${{targetTabId}}"]`);
|
|
if (activeBtn) {{
|
|
activeBtn.classList.add('active');
|
|
}}
|
|
|
|
document.querySelectorAll('.access-tab-content').forEach(c => {{
|
|
c.style.display = 'none';
|
|
c.classList.add('hidden');
|
|
}});
|
|
const targetContent = document.getElementById(targetTabId);
|
|
if (targetContent) {{
|
|
targetContent.style.display = 'block';
|
|
targetContent.classList.remove('hidden');
|
|
}}
|
|
|
|
// Reset scroll inside modal body
|
|
const modalBody = document.getElementById('modal-tabs-body');
|
|
if (modalBody) modalBody.scrollTop = 0;
|
|
}}
|
|
|
|
function openModalToTab(targetTabId) {{
|
|
ensureModalAttachedToBody();
|
|
renderModal();
|
|
const overlay = document.getElementById('access-modal-overlay');
|
|
if (overlay) {{
|
|
overlay.classList.add('is-open');
|
|
overlay.style.display = 'flex';
|
|
document.body.classList.add('access-modal-open');
|
|
document.documentElement.classList.add('access-modal-open');
|
|
}}
|
|
switchTab(targetTabId);
|
|
}}
|
|
|
|
function closeModal() {{
|
|
const overlay = document.getElementById('access-modal-overlay');
|
|
if (overlay) {{
|
|
overlay.classList.remove('is-open');
|
|
overlay.style.display = 'none';
|
|
document.body.classList.remove('access-modal-open');
|
|
document.documentElement.classList.remove('access-modal-open');
|
|
}}
|
|
}}
|
|
|
|
// ─── Render: Update Overview Card on Page ───────────────────
|
|
function renderOverviewCard() {{
|
|
const role = rolesData[state.roleId];
|
|
const roleName = role ? role.name : (i18n.unassigned_role || "Unassigned / No Role");
|
|
if (ovRoleName) ovRoleName.textContent = roleName;
|
|
|
|
const {{ extra, denied }} = getExtraAndDeniedPerms();
|
|
|
|
if (ovPermStatus) {{
|
|
if (extra.length === 0 && denied.length === 0) {{
|
|
ovPermStatus.innerHTML = `
|
|
<span class="overview-badge overview-badge-success">
|
|
<span class="material-symbols-outlined text-xs">verified</span>
|
|
<span>${{escape(i18n.all_perms_match || "All permissions match base role")}}</span>
|
|
</span>
|
|
`;
|
|
}} else if (extra.length > 0) {{
|
|
ovPermStatus.innerHTML = `
|
|
<span class="overview-badge overview-badge-role">
|
|
<span class="material-symbols-outlined text-xs">auto_awesome</span>
|
|
<span>+ ${{extra.length}} ${{escape(i18n.custom_perms_suffix || "custom permissions beyond role")}}</span>
|
|
</span>
|
|
`;
|
|
}} else {{
|
|
ovPermStatus.innerHTML = `
|
|
<span class="overview-badge" style="background-color: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.25); color: #b45309;">
|
|
<span>${{denied.length}} ${{escape(i18n.revoked_perms_suffix || "permissions revoked")}}</span>
|
|
</span>
|
|
`;
|
|
}}
|
|
}}
|
|
|
|
// Extra perms chips container
|
|
if (ovExtraPerms) {{
|
|
if (extra.length === 0) {{
|
|
ovExtraPerms.innerHTML = `<span class="text-xs text-slate-400 italic">${{escape(i18n.no_custom_perms || "No custom permissions (Standard role access)")}}</span>`;
|
|
}} else {{
|
|
const chips = extra.slice(0, 3).map(k => {{
|
|
const p = permsMap[k];
|
|
const label = p ? p.label : k;
|
|
return `<span class="overview-badge overview-badge-extra">${{escape(label)}}</span>`;
|
|
}}).join(' ');
|
|
const more = extra.length > 3 ? `<span class="text-[10px] text-slate-400 font-semibold">+ ${{extra.length - 3}} ${{escape(i18n.more_suffix || "more")}}</span>` : '';
|
|
ovExtraPerms.innerHTML = `<div class="flex flex-wrap items-center gap-1.5">${{chips}} ${{more}}</div>`;
|
|
}}
|
|
}}
|
|
|
|
// Institution name
|
|
if (ovInstitution) {{
|
|
ovInstitution.innerHTML = `
|
|
<span class="material-symbols-outlined text-base text-slate-400">apartment</span>
|
|
<span class="truncate">${{escape(state.institution || (i18n.no_institution || "No affiliated institution"))}}</span>
|
|
`;
|
|
}}
|
|
|
|
// Managed regions
|
|
if (ovRegions) {{
|
|
if (state.regions.length === 0) {{
|
|
ovRegions.innerHTML = `<span class="text-xs text-slate-400 italic">${{escape(i18n.regions_unlimited || "Global / Unlimited access")}}</span>`;
|
|
}} else {{
|
|
const rChips = state.regions.slice(0, 3).map(id => {{
|
|
const reg = regionsData.find(r => r.id === id);
|
|
return `<span class="overview-badge overview-badge-region">${{escape(reg ? reg.name : id)}}</span>`;
|
|
}}).join(' ');
|
|
const rMore = state.regions.length > 3 ? `<span class="text-[10px] text-slate-400 font-semibold">+ ${{state.regions.length - 3}} ${{escape(i18n.more_suffix || "more")}}</span>` : '';
|
|
ovRegions.innerHTML = `<div class="flex flex-wrap items-center gap-1.5">${{rChips}} ${{rMore}}</div>`;
|
|
}}
|
|
}}
|
|
}}
|
|
|
|
// ─── Render: Modal Content ──────────────────────────────────
|
|
function renderModal() {{
|
|
// 1. Roles Cards in Tab 1
|
|
if (roleCardsContainer) {{
|
|
let rolesHtml = '';
|
|
for (const [rId, r] of Object.entries(rolesData)) {{
|
|
const isSelected = (state.roleId === rId);
|
|
const permsCount = Object.values(r.permissions || {{}}).filter(Boolean).length;
|
|
rolesHtml += `
|
|
<div class="modal-role-card ${{isSelected ? 'is-selected' : ''}}" data-role-id="${{escape(rId)}}">
|
|
<div class="flex items-center justify-between mb-1.5">
|
|
<div class="flex items-center gap-2">
|
|
<span class="material-symbols-outlined text-base ${{isSelected ? 'text-indigo-600 dark:text-indigo-400' : 'text-slate-400'}}">badge</span>
|
|
<span class="text-xs font-bold text-slate-800 dark:text-slate-100">${{escape(r.name)}}</span>
|
|
</div>
|
|
${{isSelected ? '<span class="material-symbols-outlined text-indigo-600 dark:text-indigo-400 text-base">check_circle</span>' : ''}}
|
|
</div>
|
|
<p class="text-[11px] text-slate-500 dark:text-slate-400 line-clamp-2 leading-relaxed">${{escape(r.description || (i18n.default_role_desc || "Organizational role with default permissions"))}}</p>
|
|
<span class="inline-block mt-2 text-[10px] font-semibold text-slate-500 dark:text-slate-400 bg-slate-100 dark:bg-slate-700/60 px-2 py-0.5 rounded-full">${{permsCount}} ${{escape(i18n.default_perms_chip || "default permissions")}}</span>
|
|
</div>
|
|
`;
|
|
}}
|
|
roleCardsContainer.innerHTML = rolesHtml;
|
|
}}
|
|
|
|
// 2. Role Preview in Tab 1
|
|
const role = rolesData[state.roleId];
|
|
if (role) {{
|
|
const grantedPerms = Object.entries(role.permissions || {{}}).filter(([_, v]) => v).map(([k]) => k);
|
|
if (rolePreviewCount) rolePreviewCount.textContent = `${{grantedPerms.length}} ${{i18n.active_perms_count || "active permissions"}}`;
|
|
if (rolePreviewBadges) {{
|
|
rolePreviewBadges.innerHTML = grantedPerms.map(k => {{
|
|
const p = permsMap[k];
|
|
return `<span class="overview-badge overview-badge-extra"><span class="material-symbols-outlined text-xs text-emerald-600">check</span>${{escape(p ? p.label : k)}}</span>`;
|
|
}}).join(' ');
|
|
}}
|
|
}} else {{
|
|
if (rolePreviewCount) rolePreviewCount.textContent = `0 ${{i18n.active_perms_count || "active permissions"}}`;
|
|
if (rolePreviewBadges) rolePreviewBadges.innerHTML = `<span class="text-xs text-slate-400 italic">${{escape(i18n.no_role_selected || "No role selected.")}}</span>`;
|
|
}}
|
|
|
|
// 3. User Type Select
|
|
if (userTypeSelect) userTypeSelect.value = state.userType;
|
|
|
|
// 4. Groups in Tab 1
|
|
if (groupsContainer) {{
|
|
groupsContainer.innerHTML = groupsData.map(g => {{
|
|
const isChecked = state.groups.includes(g.id);
|
|
return `
|
|
<label class="modal-group-chip ${{isChecked ? 'is-checked' : ''}}" data-group-id="${{escape(g.id)}}">
|
|
<input type="checkbox" class="hidden modal-group-input" value="${{escape(g.id)}}" ${{isChecked ? 'checked' : ''}}>
|
|
<span class="material-symbols-outlined text-sm ${{isChecked ? 'text-indigo-600 dark:text-indigo-400' : 'text-slate-400'}}">${{isChecked ? 'check_circle' : 'add_circle'}}</span>
|
|
<span>${{escape(g.name)}}</span>
|
|
</label>
|
|
`;
|
|
}}).join('');
|
|
}}
|
|
|
|
// 5. Populate System Permissions Dropdown for Quick Add
|
|
if (selectSystemPerm) {{
|
|
const rolePerms = getActiveRolePerms();
|
|
let optionsHtml = `<option value="">${{escape(i18n.select_system_perm_default || "-- Select from all system permissions --")}}</option>`;
|
|
for (const cat of categoriesData) {{
|
|
optionsHtml += `<optgroup label="${{escape(cat.title)}}">`;
|
|
for (const p of cat.perms) {{
|
|
const isInherited = !!rolePerms[p.name];
|
|
const hasAllow = state.overrides[p.name] === true;
|
|
const extraLabel = isInherited ? (i18n.in_role_tag || " (in role)") : (hasAllow ? (i18n.active_custom_tag || " (active custom)") : "");
|
|
optionsHtml += `<option value="${{escape(p.name)}}">${{escape(p.label)}}${{extraLabel}}</option>`;
|
|
}}
|
|
optionsHtml += `</optgroup>`;
|
|
}}
|
|
selectSystemPerm.innerHTML = optionsHtml;
|
|
}}
|
|
|
|
// 6. Permissions Matrix in Tab 2
|
|
if (categoriesListContainer) {{
|
|
const rolePerms = getActiveRolePerms();
|
|
let catHtml = '';
|
|
|
|
// Render Standard Categories
|
|
for (const cat of categoriesData) {{
|
|
catHtml += `
|
|
<div class="matrix-cat-card">
|
|
<div class="matrix-cat-header">
|
|
<div class="flex items-center gap-2">
|
|
<span class="material-symbols-outlined text-indigo-600 dark:text-indigo-400 text-base">${{escape(cat.icon)}}</span>
|
|
<span class="font-bold text-xs text-slate-800 dark:text-slate-200">${{escape(cat.title)}}</span>
|
|
<span class="text-[10px] px-2 py-0.5 rounded-full bg-slate-200/60 dark:bg-slate-700 text-slate-600 dark:text-slate-400 font-medium">${{cat.perms.length}}</span>
|
|
</div>
|
|
<span class="material-symbols-outlined text-slate-400 text-base transition-transform cat-chevron">expand_more</span>
|
|
</div>
|
|
<div class="matrix-cat-body">
|
|
`;
|
|
|
|
for (const p of cat.perms) {{
|
|
const pName = p.name;
|
|
const pLabel = p.label;
|
|
const roleVal = !!rolePerms[pName];
|
|
const hasOverride = state.overrides.hasOwnProperty(pName);
|
|
const overrideVal = hasOverride ? state.overrides[pName] : null;
|
|
|
|
let statusText = '';
|
|
if (overrideVal === true) {{
|
|
statusText = `<span class="text-emerald-600 dark:text-emerald-400 font-bold">${{escape(i18n.status_custom_allow || "✓ Custom Granted (Allow)")}}</span>`;
|
|
}} else if (overrideVal === false) {{
|
|
statusText = `<span class="text-red-600 dark:text-red-400 font-bold">${{escape(i18n.status_custom_deny || "✕ Revoked (Deny)")}}</span>`;
|
|
}} else {{
|
|
statusText = roleVal
|
|
? `<span class="text-indigo-600 dark:text-indigo-400 font-semibold">${{escape(i18n.status_inherited_active || "● Inherited: Active from role")}}</span>`
|
|
: `<span class="text-slate-400">${{escape(i18n.status_inherited_inactive || "○ Inherited: Inactive")}}</span>`;
|
|
}}
|
|
|
|
catHtml += `
|
|
<div class="matrix-perm-row" data-perm-name="${{escape(pName)}}">
|
|
<div class="flex flex-col pr-1 min-w-0">
|
|
<span class="text-xs font-semibold text-slate-800 dark:text-slate-200 truncate" title="${{escape(pLabel)}}">${{escape(pLabel)}}</span>
|
|
<span class="text-[10px] mt-0.5 perm-status-label">${{statusText}}</span>
|
|
</div>
|
|
<div class="perm-btn-group">
|
|
<button type="button" data-val="inherit" class="perm-btn ${{overrideVal === null ? 'is-active-inherit' : ''}}">${{escape(i18n.btn_inherit || "Inherit")}}</button>
|
|
<button type="button" data-val="allow" class="perm-btn ${{overrideVal === true ? 'is-active-allow' : ''}}">${{escape(i18n.btn_allow || "Allow")}}</button>
|
|
<button type="button" data-val="deny" class="perm-btn ${{overrideVal === false ? 'is-active-deny' : ''}}">${{escape(i18n.btn_deny || "Deny")}}</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}}
|
|
|
|
catHtml += `
|
|
</div>
|
|
</div>
|
|
`;
|
|
}}
|
|
|
|
// Render Custom Extra Permission Flags (Keys not in standard categories)
|
|
const standardKeys = new Set(Object.keys(permsMap));
|
|
const customFlags = Object.keys(state.overrides).filter(k => !standardKeys.has(k));
|
|
if (customFlags.length > 0) {{
|
|
catHtml += `
|
|
<div class="matrix-cat-card" style="border-color: rgba(99, 102, 241, 0.3);">
|
|
<div class="matrix-cat-header" style="background-color: rgba(99, 102, 241, 0.08);">
|
|
<div class="flex items-center gap-2">
|
|
<span class="material-symbols-outlined text-indigo-600 dark:text-indigo-400 text-base">auto_awesome</span>
|
|
<span class="font-bold text-xs text-indigo-900 dark:text-indigo-200">${{escape(i18n.custom_flags_title || "Custom Granted Flags")}}</span>
|
|
<span class="text-[10px] px-2 py-0.5 rounded-full bg-indigo-600 text-white font-bold">${{customFlags.length}}</span>
|
|
</div>
|
|
<span class="material-symbols-outlined text-indigo-400 text-base transition-transform cat-chevron">expand_more</span>
|
|
</div>
|
|
<div class="matrix-cat-body">
|
|
`;
|
|
for (const cKey of customFlags) {{
|
|
const isAllow = state.overrides[cKey] === true;
|
|
catHtml += `
|
|
<div class="matrix-perm-row" style="background-color: rgba(99, 102, 241, 0.04); border-color: rgba(99, 102, 241, 0.2);">
|
|
<div class="flex flex-col pr-1 min-w-0">
|
|
<span class="font-mono font-bold text-slate-800 dark:text-slate-100 truncate">${{escape(cKey)}}</span>
|
|
<span class="text-[10px] text-emerald-600 font-semibold">${{isAllow ? escape(i18n.status_custom_flag_allow || "✓ Custom active permission") : escape(i18n.status_custom_flag_deny || "✕ Custom revoked permission")}}</span>
|
|
</div>
|
|
<button type="button" class="btn-remove-custom-perm overview-small-btn" data-perm-key="${{escape(cKey)}}" title="${{escape(i18n.delete_perm_title || "Delete this permission")}}">
|
|
<span class="material-symbols-outlined text-xs">delete</span>
|
|
</button>
|
|
</div>
|
|
`;
|
|
}}
|
|
catHtml += `
|
|
</div>
|
|
</div>
|
|
`;
|
|
}}
|
|
|
|
categoriesListContainer.innerHTML = catHtml;
|
|
}}
|
|
|
|
// 7. Institution Input in Tab 3
|
|
if (institutionInput) institutionInput.value = state.institution;
|
|
|
|
// 8. Regions in Tab 4
|
|
if (regionsContainer) {{
|
|
regionsContainer.innerHTML = regionsData.map(reg => {{
|
|
const isChecked = state.regions.includes(reg.id);
|
|
return `
|
|
<div class="modal-region-card ${{isChecked ? 'is-selected' : ''}}" data-region-id="${{escape(reg.id)}}">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<span class="material-symbols-outlined text-base ${{isChecked ? 'text-emerald-600 dark:text-emerald-400' : 'text-slate-400'}}">public</span>
|
|
<span class="text-xs font-semibold">${{escape(reg.name)}}</span>
|
|
</div>
|
|
<span class="material-symbols-outlined text-sm ${{isChecked ? 'text-emerald-600 dark:text-emerald-400' : 'text-slate-300'}}">${{isChecked ? 'check_circle' : 'circle'}}</span>
|
|
</div>
|
|
${{reg.code ? `<span class="inline-block text-[10px] font-normal text-slate-400 mt-1 uppercase">${{escape(reg.code)}}</span>` : ''}}
|
|
</div>
|
|
`;
|
|
}}).join('');
|
|
}}
|
|
if (regionsSelectedCounter) regionsSelectedCounter.textContent = `${{state.regions.length}} ${{i18n.regions_selected_count || "regions selected"}}`;
|
|
|
|
// 9. Footer & Badges Summary
|
|
const {{ extra, denied }} = getExtraAndDeniedPerms();
|
|
if (tabExtraCountBadge) {{
|
|
if (extra.length > 0) {{
|
|
tabExtraCountBadge.textContent = `+${{extra.length}}`;
|
|
tabExtraCountBadge.classList.remove('hidden');
|
|
}} else {{
|
|
tabExtraCountBadge.classList.add('hidden');
|
|
}}
|
|
}}
|
|
if (tabRegionsCountBadge) {{
|
|
if (state.regions.length > 0) {{
|
|
tabRegionsCountBadge.textContent = `${{state.regions.length}}`;
|
|
tabRegionsCountBadge.classList.remove('hidden');
|
|
}} else {{
|
|
tabRegionsCountBadge.classList.add('hidden');
|
|
}}
|
|
}}
|
|
|
|
if (footerRoleName) {{
|
|
footerRoleName.textContent = role ? role.name : (i18n.footer_no_role || "No Role");
|
|
}}
|
|
if (footerExtraBadge) {{
|
|
if (extra.length > 0) {{
|
|
footerExtraBadge.textContent = `• ${{extra.length}} ${{i18n.footer_extra || "custom permissions"}}`;
|
|
footerExtraBadge.classList.remove('hidden');
|
|
}} else {{
|
|
footerExtraBadge.classList.add('hidden');
|
|
}}
|
|
}}
|
|
if (footerRegionsBadge) {{
|
|
if (state.regions.length > 0) {{
|
|
footerRegionsBadge.textContent = `• ${{state.regions.length}} ${{i18n.footer_regions || "geographic regions"}}`;
|
|
footerRegionsBadge.classList.remove('hidden');
|
|
}} else {{
|
|
footerRegionsBadge.classList.add('hidden');
|
|
}}
|
|
}}
|
|
}}
|
|
|
|
// ─── Event Handlers ─────────────────────────────────────────
|
|
|
|
// Open Modal (General)
|
|
if (btnOpenModal) {{
|
|
btnOpenModal.addEventListener('click', function(e) {{
|
|
e.preventDefault();
|
|
openModalToTab('tab-roles');
|
|
}});
|
|
}}
|
|
|
|
// Open Modal directly to Permissions tab from Overview Card buttons
|
|
if (btnHeaderAddPerm) {{
|
|
btnHeaderAddPerm.addEventListener('click', function(e) {{
|
|
e.preventDefault();
|
|
openModalToTab('tab-permissions');
|
|
}});
|
|
}}
|
|
if (btnAddPermCard) {{
|
|
btnAddPermCard.addEventListener('click', function(e) {{
|
|
e.preventDefault();
|
|
openModalToTab('tab-permissions');
|
|
}});
|
|
}}
|
|
|
|
// Open Modal directly to Roles tab from Overview Card
|
|
if (btnChangeRoleCard) {{
|
|
btnChangeRoleCard.addEventListener('click', function(e) {{
|
|
e.preventDefault();
|
|
openModalToTab('tab-roles');
|
|
}});
|
|
}}
|
|
|
|
// Close Modal
|
|
if (btnCloseX) btnCloseX.addEventListener('click', closeModal);
|
|
if (btnCancel) btnCancel.addEventListener('click', closeModal);
|
|
|
|
// Backdrop click to close
|
|
if (modalOverlay) {{
|
|
modalOverlay.addEventListener('click', function(e) {{
|
|
if (e.target === modalOverlay) {{
|
|
closeModal();
|
|
}}
|
|
}});
|
|
}}
|
|
|
|
// Wheel isolation to guarantee background never scrolls while interacting with modal
|
|
if (modalDialog) {{
|
|
modalDialog.addEventListener('wheel', function(e) {{
|
|
e.stopPropagation();
|
|
}}, {{ passive: false }});
|
|
}}
|
|
if (modalOverlay) {{
|
|
modalOverlay.addEventListener('wheel', function(e) {{
|
|
if (e.target === modalOverlay) {{
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}}
|
|
}}, {{ passive: false }});
|
|
}}
|
|
|
|
// Escape key to close
|
|
document.addEventListener('keydown', function(e) {{
|
|
if (e.key === 'Escape') {{
|
|
const overlay = document.getElementById('access-modal-overlay');
|
|
if (overlay && (overlay.classList.contains('is-open') || overlay.style.display === 'flex')) {{
|
|
closeModal();
|
|
}}
|
|
}}
|
|
}});
|
|
|
|
// Add System Permission from Dropdown in Tab 2
|
|
if (btnAddSystemPerm) {{
|
|
btnAddSystemPerm.addEventListener('click', function() {{
|
|
if (!selectSystemPerm) return;
|
|
const permKey = selectSystemPerm.value;
|
|
if (permKey) {{
|
|
state.overrides[permKey] = true; // Grant explicitly!
|
|
renderModal();
|
|
selectSystemPerm.value = "";
|
|
}}
|
|
}});
|
|
}}
|
|
|
|
// Add Custom Permission Flag Key in Tab 2
|
|
if (btnAddCustomPerm) {{
|
|
btnAddCustomPerm.addEventListener('click', function() {{
|
|
if (!inputCustomPermKey) return;
|
|
const rawKey = inputCustomPermKey.value.trim().toLowerCase();
|
|
const safeKey = rawKey.replace(/[^a-z0-9_]/g, '_');
|
|
if (safeKey) {{
|
|
state.overrides[safeKey] = true; // Grant explicitly!
|
|
renderModal();
|
|
inputCustomPermKey.value = "";
|
|
}}
|
|
}});
|
|
}}
|
|
|
|
// Tab Switching & Delegated Actions
|
|
document.addEventListener('click', function(e) {{
|
|
const tabBtn = e.target.closest('.access-tab-btn');
|
|
if (tabBtn) {{
|
|
const targetId = tabBtn.dataset.tab;
|
|
switchTab(targetId);
|
|
return;
|
|
}}
|
|
|
|
// Remove custom flag button
|
|
const btnRemoveCustom = e.target.closest('.btn-remove-custom-perm');
|
|
if (btnRemoveCustom) {{
|
|
const cKey = btnRemoveCustom.dataset.permKey;
|
|
if (cKey) {{
|
|
delete state.overrides[cKey];
|
|
renderModal();
|
|
}}
|
|
return;
|
|
}}
|
|
|
|
// Role Card Selection in Tab 1
|
|
const roleCard = e.target.closest('.modal-role-card');
|
|
if (roleCard) {{
|
|
state.roleId = roleCard.dataset.roleId;
|
|
renderModal();
|
|
return;
|
|
}}
|
|
|
|
// Group Pill in Tab 1
|
|
const groupChip = e.target.closest('.modal-group-chip');
|
|
if (groupChip) {{
|
|
const gId = groupChip.dataset.groupId;
|
|
if (state.groups.includes(gId)) {{
|
|
state.groups = state.groups.filter(id => id !== gId);
|
|
}} else {{
|
|
state.groups.push(gId);
|
|
}}
|
|
renderModal();
|
|
return;
|
|
}}
|
|
|
|
// Region Card in Tab 4
|
|
const regionCard = e.target.closest('.modal-region-card');
|
|
if (regionCard) {{
|
|
const rId = regionCard.dataset.regionId;
|
|
if (state.regions.includes(rId)) {{
|
|
state.regions = state.regions.filter(id => id !== rId);
|
|
}} else {{
|
|
state.regions.push(rId);
|
|
}}
|
|
renderModal();
|
|
return;
|
|
}}
|
|
|
|
// Permission 3-Way Button in Tab 2
|
|
const permBtn = e.target.closest('.perm-btn');
|
|
if (permBtn) {{
|
|
const row = permBtn.closest('.matrix-perm-row');
|
|
if (!row) return;
|
|
const pName = row.dataset.permName;
|
|
const val = permBtn.dataset.val;
|
|
|
|
if (val === 'inherit') {{
|
|
delete state.overrides[pName];
|
|
}} else if (val === 'allow') {{
|
|
state.overrides[pName] = true;
|
|
}} else if (val === 'deny') {{
|
|
state.overrides[pName] = false;
|
|
}}
|
|
renderModal();
|
|
return;
|
|
}}
|
|
|
|
// Category Accordion
|
|
const catHeader = e.target.closest('.matrix-cat-header');
|
|
if (catHeader) {{
|
|
const card = catHeader.closest('.matrix-cat-card');
|
|
if (!card) return;
|
|
const body = card.querySelector('.matrix-cat-body');
|
|
const chevron = catHeader.querySelector('.cat-chevron');
|
|
if (body) {{
|
|
const isHidden = body.classList.toggle('hidden');
|
|
if (chevron) chevron.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)';
|
|
}}
|
|
return;
|
|
}}
|
|
}});
|
|
|
|
// Permission Search
|
|
if (permSearchInput) {{
|
|
permSearchInput.addEventListener('input', function() {{
|
|
const q = this.value.toLowerCase().trim();
|
|
document.querySelectorAll('.matrix-perm-row').forEach(row => {{
|
|
const text = row.textContent.toLowerCase();
|
|
row.style.display = text.includes(q) ? 'flex' : 'none';
|
|
}});
|
|
document.querySelectorAll('.matrix-cat-card').forEach(card => {{
|
|
const visible = card.querySelectorAll('.matrix-perm-row:not([style*="display: none"])');
|
|
card.style.display = visible.length > 0 ? 'block' : 'none';
|
|
}});
|
|
}});
|
|
}}
|
|
|
|
// Reset Overrides
|
|
if (resetOverridesBtn) {{
|
|
resetOverridesBtn.addEventListener('click', function() {{
|
|
state.overrides = {{}};
|
|
renderModal();
|
|
}});
|
|
}}
|
|
|
|
// Institution Input
|
|
if (institutionInput) {{
|
|
institutionInput.addEventListener('input', function() {{
|
|
state.institution = this.value;
|
|
}});
|
|
}}
|
|
|
|
// User Type Select
|
|
if (userTypeSelect) {{
|
|
userTypeSelect.addEventListener('change', function() {{
|
|
state.userType = this.value;
|
|
}});
|
|
}}
|
|
|
|
// Apply Changes (Sync to Django Form Inputs)
|
|
if (btnApply) {{
|
|
btnApply.addEventListener('click', function() {{
|
|
// 1. Sync overrides to hidden textarea
|
|
if (hiddenInput) {{
|
|
hiddenInput.value = JSON.stringify(state.overrides);
|
|
}}
|
|
|
|
// 2. Sync Role dropdown
|
|
const formRoleSelect = document.querySelector('select[name="role"]');
|
|
if (formRoleSelect) {{
|
|
formRoleSelect.value = state.roleId;
|
|
formRoleSelect.dispatchEvent(new Event('change', {{ bubbles: true }}));
|
|
}}
|
|
|
|
// 3. Sync User Type
|
|
const formUserTypeSelect = document.querySelector('select[name="user_type"]');
|
|
if (formUserTypeSelect) {{
|
|
formUserTypeSelect.value = state.userType;
|
|
formUserTypeSelect.dispatchEvent(new Event('change', {{ bubbles: true }}));
|
|
}}
|
|
|
|
// 4. Sync Institution
|
|
const formInstInput = document.querySelector('input[name="institution_name"]');
|
|
if (formInstInput) {{
|
|
formInstInput.value = state.institution;
|
|
formInstInput.dispatchEvent(new Event('change', {{ bubbles: true }}));
|
|
}}
|
|
|
|
// 5. Sync Groups checkboxes
|
|
document.querySelectorAll('input[name="groups"]').forEach(cb => {{
|
|
cb.checked = state.groups.includes(cb.value);
|
|
cb.dispatchEvent(new Event('change', {{ bubbles: true }}));
|
|
}});
|
|
|
|
// 6. Sync Regions checkboxes
|
|
document.querySelectorAll('input[name="managed_regions"]').forEach(cb => {{
|
|
cb.checked = state.regions.includes(cb.value);
|
|
cb.dispatchEvent(new Event('change', {{ bubbles: true }}));
|
|
}});
|
|
|
|
// 7. Update Overview Card on Page
|
|
renderOverviewCard();
|
|
|
|
// 8. Close Modal
|
|
closeModal();
|
|
}});
|
|
}}
|
|
|
|
// Initial render on load
|
|
function init() {{
|
|
ensureModalAttachedToBody();
|
|
renderOverviewCard();
|
|
|
|
// Hide the redundant, crowded raw rows from Unfold's change form
|
|
// while keeping them alive in the DOM for Django POST submission!
|
|
const crowdedSelectors = [
|
|
'.field-role',
|
|
'.field-user_type',
|
|
'.field-groups',
|
|
'.field-managed_regions'
|
|
];
|
|
crowdedSelectors.forEach(sel => {{
|
|
document.querySelectorAll(sel).forEach(el => {{
|
|
if (el.closest('.tab') || el.classList.contains('form-row')) {{
|
|
el.style.display = 'none';
|
|
}}
|
|
}});
|
|
}});
|
|
}}
|
|
|
|
// Eagerly teleport to body if document.body exists immediately
|
|
if (document.body) {{
|
|
ensureModalAttachedToBody();
|
|
}}
|
|
|
|
if (document.readyState === 'loading') {{
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
}} else {{
|
|
init();
|
|
}}
|
|
window.addEventListener('load', function() {{
|
|
setTimeout(init, 50);
|
|
}});
|
|
}})();
|
|
</script>
|
|
'''
|
|
]
|
|
|
|
return mark_safe("\n".join(html))
|