Corp/backend/templates/employees/components/department_modals.html
Игорь Надеин 026bc46816 feat(department-roles): complete LDAP architecture redesign with RoleAssignment model
BREAKING CHANGES:
- Removed OU=Roles from LDAP structure, role groups now created directly in department OU
- Role groups use ROLE_<Name> naming convention (similar to DEP_, POS_)

New features:
- RoleAssignment model: allows assigning roles to ANY employee, not just department members
- New API endpoints: /assign/, /revoke/, /assignments/ on DepartmentRoleViewSet
- Permission system updated: has_dept_perm() checks RoleAssignment first
- Employee profile shows external roles with 'внешняя роль' badge
- Employee list filter includes role-only employees with department_relation field
- Role assignment modal now searches all employees via AJAX

LDAP changes:
- _ensure_role_group(): creates ROLE_<Name> group in department OU
- assign_role(), revoke_role(): manage RoleAssignment + LDAP sync
- rename_role_group(), delete_role_group(): full lifecycle support
- Removed all references to OU=Roles

Migrations:
- 0035: Create RoleAssignment model
- 0036: Data migration from EmployeeDepartment.role

Tests:
- 7 new tests for role assignment (all passing)

Docs:
- 9 detailed reports on architecture, models, API, LDAP, tests
2025-12-30 10:35:34 +03:00

195 lines
10 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{# ---------- МОДАЛ «Удалить участника» ---------- #}
{% if user.is_staff or user.is_superuser or can_manage or dept.head.id == user.id %}
<div class="modal fade" id="removeMemberModal-{{ dept.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi-person-dash me-2"></i>Удалить участника</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<form method="post" action="{% url 'employees:department_remove_member' dept.id %}">
{% csrf_token %}
<input type="hidden" name="employee_id" id="removeMemberChosen-{{ dept.id }}">
<input type="hidden" name="member_id" id="removeMemberLink-{{ dept.id }}">
<div class="modal-body">
<div class="mb-3">
<input id="removeMemberSearch-{{ dept.id }}" type="search"
class="form-control" placeholder="Поиск по ФИО или email" autocomplete="off">
<div class="form-text">Отображаются участники этого отдела.</div>
</div>
<div id="removeMemberResults-{{ dept.id }}"
class="list-group list-group-flush border rounded">
{% for li in links %}
{% with emp=li.employee %}
<button type="button"
class="list-group-item list-group-item-action d-flex align-items-center gap-2 py-2"
data-id="{{ emp.id }}"
data-link-id="{{ li.id }}"
data-name="{{ emp.display_name|default:emp.last_name }}"
data-email="{{ emp.email|default:'' }}">
<span class="card-icon"
style="width:32px;height:32px;flex:0 0 32px;border-radius:50%;overflow:hidden;display:flex;align-items:center;justify-content:center;">
{% if emp.avatar %}<img src="{{ emp.avatar }}" alt="" style="width:100%;height:100%;object-fit:cover;">{% else %}<i class="bi-person"></i>{% endif %}
</span>
<span class="d-flex flex-column text-start">
<span class="fw-semibold">{{ emp.display_name|default:emp.last_name }}</span>
{% if emp.email %}<span class="text-muted small">{{ emp.email }}</span>{% endif %}
</span>
</button>
{% endwith %}
{% empty %}
<div class="list-group-item text-secondary small py-3">В отделе пока нет участников.</div>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link" data-bs-dismiss="modal">
<i class="bi-x-lg me-1"></i>Отмена
</button>
<button type="submit" id="removeMemberSubmit-{{ dept.id }}" class="btn btn-outline-danger" disabled>
<i class="bi-person-dash me-1"></i>Удалить
</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
{# ---------- МОДАЛ «Добавить участника» ---------- #}
{% if user.is_staff or user.is_superuser or can_manage or dept.head.id == user.id %}
{% with exclude_ids=members_exclude_ids %}
<div class="modal fade" id="addMemberModal-{{ dept.id }}" tabindex="-1" aria-hidden="true"
data-exclude="{% if exclude_ids %}{{ exclude_ids|join:',' }}{% endif %}">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi-person-plus me-2"></i>Добавить участника</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<form method="post" action="{% url 'employees:department_add_member' dept.id %}">
{% csrf_token %}
<input type="hidden" name="employee_id" id="addMemberChosen-{{ dept.id }}">
<div class="modal-body">
<div class="mb-3">
<input id="addMemberSearch-{{ dept.id }}" type="search"
class="form-control" placeholder="Поиск по ФИО или email" autocomplete="off"
data-add-member-q>
<div class="form-text">Показываются только те, кто ещё не состоит в отделе.</div>
</div>
<div id="addMemberResults-{{ dept.id }}" class="list-group list-group-flush border rounded"
data-add-member-list>
<div class="list-group-item text-muted small py-3">Начните вводить для поиска…</div>
</div>
<template id="addMemberItemTpl-{{ dept.id }}">
<button type="button" class="list-group-item list-group-item-action d-flex align-items-center gap-2 py-2" data-id="">
<span class="card-icon" style="width:32px;height:32px;flex:0 0 32px;border-radius:50%;overflow:hidden;display:flex;align-items:center;justify-content:center;">
<i class="bi-person"></i>
</span>
<span class="d-flex flex-column text-start">
<span class="fw-semibold js-name"></span>
<span class="text-muted small js-email"></span>
</span>
</button>
</template>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link" data-bs-dismiss="modal">
<i class="bi-x-lg me-1"></i>Отмена
</button>
<button type="submit" id="addMemberSubmit-{{ dept.id }}" class="btn btn-success"
data-add-member-submit disabled>
<i class="bi-person-plus me-1"></i>Добавить
</button>
</div>
</form>
</div>
</div>
</div>
{% endwith %}
{% endif %}
{# ---------- МОДАЛ «Назначить роль» ---------- #}
{% if roles %}
{% if user.is_superuser or user.is_staff or can_assign_roles or dept.head.id == user.id %}
<div class="modal fade" id="setRoleModal-{{ dept.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi-award me-2"></i>Назначить роль</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
{# порядок аргументов: emp_id, dept_id #}
<form method="post"
action="{% url 'employees:set_member_role' 0 dept.id %}"
data-action-template="{% url 'employees:set_member_role' 0 dept.id %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<input type="hidden" name="employee_id" id="setRoleChosen-{{ dept.id }}">
<div class="modal-body">
<div class="row g-3">
<div class="col-md-6">
<label for="setRoleSearch-{{ dept.id }}" class="form-label">Сотрудник</label>
<input id="setRoleSearch-{{ dept.id }}" type="search"
class="form-control" placeholder="Поиск по ФИО или email" autocomplete="off"
data-set-role-q>
<div class="form-text">Можно назначить роль любому сотруднику компании.</div>
<div id="setRoleResults-{{ dept.id }}"
class="list-group list-group-flush border rounded mt-2"
style="max-height:320px;overflow:auto;"
data-set-role-list>
<div class="list-group-item text-muted small py-3">Начните вводить для поиска…</div>
</div>
<template id="setRoleItemTpl-{{ dept.id }}">
<button type="button" class="list-group-item list-group-item-action d-flex align-items-center gap-2 py-2" data-id="">
<span class="card-icon" style="width:32px;height:32px;flex:0 0 32px;border-radius:50%;overflow:hidden;display:flex;align-items:center;justify-content:center;">
<i class="bi-person"></i>
</span>
<span class="d-flex flex-column text-start">
<span class="fw-semibold js-name"></span>
<span class="text-muted small js-email"></span>
</span>
</button>
</template>
</div>
<div class="col-md-6">
<label for="setRoleSelect-{{ dept.id }}" class="form-label">Роль</label>
<select name="role_id" id="setRoleSelect-{{ dept.id }}" class="form-select">
<option value="">— Без роли —</option>
{% for r in roles %}
<option value="{{ r.id }}">{{ r.name }}</option>
{% endfor %}
</select>
<div class="form-text">Выберите участника слева и роль здесь.</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link" data-bs-dismiss="modal">
<i class="bi-x-lg me-1"></i>Отмена
</button>
<button type="submit" id="setRoleSubmit-{{ dept.id }}" class="btn btn-primary" disabled>
<i class="bi-check2 me-1"></i>Сохранить
</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
{% endif %}