Corp/backend/templates/employees/employee_detail.html
Игорь Надеин 3e4137daa4 feat(ldap): добавлено кэширование LDAP логина с возможностью обновления
- Добавлено поле Employee.username для кэширования LDAP логина
- API endpoint обновлен: поддержка кэша и force_refresh параметра
- Новый дизайн: убран blur, добавлена иконка обновления
- Автоматическая загрузка при инициализации (из кэша или LDAP)
- Визуальная индикация источника данных (cached/fresh)
- Доступно всем пользователям без ограничений
2026-02-06 15:33:48 +03:00

550 lines
30 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.

{# templates/employees/employee_detail.html #}
{% extends "base.html" %}
{% load static django_bootstrap5 %}
{% block title %}
{% if is_me %}Мой профиль{% else %}Профиль сотрудника{% endif %}
{% endblock %}
{% block extra_css %}
{% endblock %}
{% block content %}
<div class="container">
<div class="ios-header section-header d-flex align-items-center gap-2 flex-wrap">
<i class="bi-person-badge fs-2 text-primary"></i>
<h2 class="title ios-title mb-0">
{% if is_me %}Мой профиль{% else %}Профиль сотрудника{% endif %}
</h2>
<div class="ms-auto"></div>
</div>
<div class="row g-3 align-items-start">
<!-- LEFT -->
<div class="col-md-4">
<article class="ios-card">
<div class="p-3 text-center">
<!-- Avatar -->
<div class="ava-wrap mb-3" id="employeeAvatar">
{% if emp.avatar %}
<img src="{{ emp.avatar }}"
alt="Аватар {{ emp.last_name }} {{ emp.first_name }}"
class="ios-ava"
style="object-fit:cover; cursor: pointer;"
data-bs-toggle="modal"
data-bs-target="#avatarModal"
data-avatar-url="{{ emp.avatar }}">
{% else %}
<div class="ios-ava--ph">
{% firstof emp.first_name emp.last_name "?" as initial %}
{{ initial|slice:":1"|upper }}
</div>
{% endif %}
</div>
<!-- Name + edit button -->
<div class="d-flex align-items-center justify-content-center gap-2 mb-1">
<div class="ios-name" id="employeeName">
{{ emp.last_name }} {{ emp.first_name }}{% if emp.patronymic %} {{ emp.patronymic }}{% endif %}
</div>
{% if can_edit %}
<button class="btn-icon"
type="button"
data-bs-toggle="collapse"
data-bs-target="#editFormWrap"
aria-expanded="false"
aria-controls="editFormWrap"
title="Редактировать профиль" aria-label="Редактировать профиль">
<i class="bi-pencil-square"></i>
</button>
<noscript>
<a class="btn btn-link p-0" href="{% url 'employees:employee_edit' emp.id %}">Редактировать</a>
</noscript>
{% endif %}
</div>
{# LDAP Login (кэшируемый, всегда доступен) #}
<div class="ldap-login-field" id="ldapLoginField" data-employee-id="{{ emp.id }}">
<div class="ldap-login-container">
<div class="ldap-login-text">
<i class="bi-person-badge ldap-icon"></i>
<span class="ldap-login-value">Загрузка...</span>
</div>
<button type="button" class="ldap-refresh-btn" data-bs-toggle="tooltip"
data-bs-placement="top" title="Обновить LDAP логин из Active Directory">
<i class="bi-arrow-repeat refresh-icon"></i>
</button>
</div>
</div>
<div class="ios-role">
{% if emp.position.name %}
{{ emp.position.name }}
{% elif emp.position_label %}
{{ emp.position_label }}
{% elif emp.position %}
Должность #{{ emp.position }}
{% endif %}
</div>
<!-- Contacts -->
<div class="mt-3 text-start">
<div class="ios-acc">
<button class="ios-collapse-trigger"
type="button"
data-bs-toggle="collapse"
data-bs-target="#contactsCollapse"
aria-expanded="false"
aria-controls="contactsCollapse">
<span class="title"><i class="bi-person-lines-fill me-1"></i>Контакты</span>
<i class="bi-chevron-down ios-collapse-caret"></i>
</button>
<div id="contactsCollapse" class="collapse">
<div class="ios-acc-body">
<div class="ios-list">
{% if emp.email %}
<div class="ios-cell">
<i class="bi-envelope"></i>
<a href="mailto:{{ emp.email }}">{{ emp.email }}</a>
</div>
{% endif %}
{% if emp.phone_number %}
<div class="ios-cell">
<i class="bi-telephone"></i>
<a href="tel:{{ emp.phone_number }}">{{ emp.phone_number }}</a>
</div>
{% endif %}
{% if emp.telegram %}<div class="ios-cell"><i class="bi-telegram"></i><span>{{ emp.telegram }}</span></div>{% endif %}
{% if emp.whatsapp %}<div class="ios-cell"><i class="bi-whatsapp"></i><span>{{ emp.whatsapp }}</span></div>{% endif %}
{% if emp.wechat %}<div class="ios-cell"><i class="bi-wechat"></i><span>{{ emp.wechat }}</span></div>{% endif %}
{% if not emp.email and not emp.phone_number and not emp.telegram and not emp.whatsapp and not emp.wechat %}
<div class="ios-cell"><span class="text-secondary">Контакты не указаны</span></div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{# === ОТДЕЛЫ: роль скрыта; редактор по кнопке-карандашу (только для начальников) === #}
<div class="mt-3 text-start">
<div class="ios-acc">
<button class="ios-collapse-trigger"
type="button"
data-bs-toggle="collapse"
data-bs-target="#departmentsCollapse"
aria-expanded="false"
aria-controls="departmentsCollapse">
<span class="title"><i class="bi-diagram-3 me-1"></i>Отделы</span>
<i class="bi-chevron-down ios-collapse-caret"></i>
</button>
<div id="departmentsCollapse" class="collapse">
<div class="ios-acc-body">
<div class="ios-list">
{% if emp_depts and emp_depts|length %}
{% for d in emp_depts %}
{% if d.name %}
<div class="ios-cell dept-item"
data-dept-id="{{ d.id|default:'' }}"
data-role-id="{{ d.role_id|default:'' }}"
data-is-head="{{ d.is_head|yesno:'1,0' }}"
data-via-assignment="{{ d.via_assignment|yesno:'1,0' }}">
<div class="dept-line">
{% if d.id %}
<a class="text-decoration-none dept-name" href="{% url 'employees:department_detail' d.id %}">{{ d.name }}</a>
{% else %}
<span class="dept-name">{{ d.name }}</span>
{% endif %}
<span class="dept-role">
{% if d.role_name %}— {{ d.role_name }}{% endif %}
</span>
{% if d.via_assignment %}
<span class="badge bg-info text-white ms-2" style="font-size: 0.7rem;"
data-bs-toggle="tooltip"
data-bs-title="Роль назначена без членства в отделе">
<i class="bi-link-45deg me-1"></i>внешняя роль
</span>
{% endif %}
</div>
<div class="d-flex align-items-center gap-2">
{% if d.is_head %}
<span class="badge-head" data-bs-toggle="tooltip" data-bs-title="Руководитель отдела">
<i class="bi-award-fill" aria-hidden="true"></i><span class="lbl">Руководитель</span>
</span>
{% endif %}
{% if d.can_edit_role %}
<button class="badge-gear" type="button"
data-bs-toggle="collapse" data-bs-target="#roleEditor-{{ d.id }}"
aria-expanded="false" aria-controls="roleEditor-{{ d.id }}"
title="Изменить роль сотрудника в этом отделе" aria-label="Изменить роль">
<i class="bi-gear-fill"></i><span class="lbl">Роль</span>
</button>
{% endif %}
</div>
{% if d.can_edit_role %}
<div id="roleEditor-{{ d.id }}" class="collapse dept-editor-wrap">
<div class="dept-editor">
<form method="post"
action="{% url 'employees:set_member_role' emp_id=emp.id dept_id=d.id %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<select name="role_id" class="form-select form-select-sm dept-role-select">
<option value="" {% if not d.role_id %}selected{% endif %}>Без роли</option>
{% for r in d.roles_choices %}
{% if r.name %}
<option value="{{ r.id }}" {% if d.role_id == r.id %}selected{% endif %}>{{ r.name }}</option>
{% endif %}
{% endfor %}
</select>
<button type="submit" class="btn btn-sm btn-primary dept-role-save" disabled>OK</button>
<button type="button" class="btn btn-sm btn-ghost"
data-bs-toggle="collapse" data-bs-target="#roleEditor-{{ d.id }}">Отмена</button>
</form>
</div>
</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
{% else %}
<div class="ios-cell"><span class="text-secondary">Отделы не указаны</span></div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{# === КАДРОВЫЕ СОБЫТИЯ (для аутентифицированных) === #}
{% if request.user.is_authenticated %}
{% with acts_src=employee_actions|default:emp.actions %}
{% with acts=acts_src.results|default:acts_src %}
{% if acts %}
<div class="mt-3 text-start">
<div class="ios-acc">
<button class="ios-collapse-trigger" type="button"
data-bs-toggle="collapse" data-bs-target="#actionsCollapse"
aria-expanded="false" aria-controls="actionsCollapse">
<span class="title"><i class="bi-journal-text me-1"></i>Кадровые события</span>
<i class="bi-chevron-down ios-collapse-caret"></i>
</button>
<div id="actionsCollapse" class="collapse">
<div class="ios-acc-body">
<div class="ios-list">
{% for a in acts %}
<div class="ios-cell">
<i class="bi-clipboard-check" style="width:1.25rem;text-align:center;opacity:.9;"></i>
<div class="flex-grow-1">
<div class="fw-bold">{% firstof a.action_display a.action %}</div>
{% if a.comment %}<div class="text-secondary">{{ a.comment }}</div>{% endif %}
<div class="small text-secondary">
<time>{% firstof a.date_display a.date|date:"d.m.Y H:i" %}</time>
</div>
{% if a.extra %}
<div class="small text-secondary mt-1">
{% for k, v in a.extra.items %}
<div>{{ k }}: {{ v }}</div>
{% endfor %}
</div>
{% endif %}
{% if a.history and a.history|length %}
<details class="mt-2 small">
<summary class="text-secondary">Подробнее</summary>
<div class="mt-2">
{% for h in a.history %}
{% with new_action=h.changes.action.new|default:a.action %}
<div class="py-2 border-top">
<div class="d-flex align-items-baseline gap-2">
<span class="fw-semibold">
{% if new_action == 'hired' %}Принят
{% elif new_action == 'dismissed' %}Уволен
{% elif new_action == 'on_leave' %}В отпуске
{% elif new_action == 'returned_from_leave' %}Вернулся из отпуска
{% elif new_action == 'on_maternity' %}В декрете
{% elif new_action == 'returned_from_maternity' %}Вернулся из декрета
{% elif new_action == 'transferred' %}Переведен
{% elif new_action == 'rehired' %}Восстановлен
{% else %}{% firstof a.action_display a.action %}{% endif %}
</span>
<span class="text-secondary">
{% firstof h.history_date_display h.history_date %}{% if h.history_user %} • {{ h.history_user }}{% endif %}
</span>
</div>
{% if h.changes %}
<div class="ms-3 mt-1">
{% for field, diff in h.changes.items %}
{% if field != 'action' %}
<div>
<span class="text-secondary">
{% if field == 'date' %}Дата события
{% elif field == 'comment' %}Комментарий
{% elif field == 'extra' %}Дополнительно
{% elif field == 'employee_id' %}Сотрудник
{% else %}{{ field }}{% endif %}
:
</span>
{% if field == 'extra' and diff.new %}
{% if diff.new.items %}
{% for ek, ev in diff.new.items %}
{% if not forloop.first %}, {% endif %}{{ ek }}: {{ ev }}
{% endfor %}
{% else %}
{{ diff.new }}
{% endif %}
{% else %}
{{ diff.new }}
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
{% endwith %}
{% endfor %}
</div>
</details>
{% endif %}
</div>
</div>
{% empty %}
<div class="ios-cell"><span class="text-secondary">Событий нет.</span></div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% endwith %}
{% endwith %}
{% endif %}
{# === УПРАВЛЕНИЕ КАДРОВЫМИ СОБЫТИЯМИ === #}
{% if request.user.is_authenticated and not is_me %}
{% if perms.employees.add_employeeaction or request.user.is_staff or request.user.is_superuser %}
<div class="mt-3 text-start">
<div class="ios-acc">
<button class="ios-collapse-trigger" type="button"
data-bs-toggle="collapse" data-bs-target="#actionsManageCollapse"
aria-expanded="false" aria-controls="actionsManageCollapse">
<span class="title"><i class="bi-clipboard-plus me-1"></i>Управление событиями</span>
<i class="bi-chevron-down ios-collapse-caret"></i>
</button>
<div id="actionsManageCollapse" class="collapse">
<div class="ios-acc-body">
<form id="employeeActionForm" class="p-2">
{% csrf_token %}
<input type="hidden" name="employee" value="{{ emp.id }}">
<div class="mb-3">
<label for="actionType" class="form-label">Тип события</label>
<select class="form-select" id="actionType" name="action" required>
<option value="">Выберите событие</option>
<option value="hired">📋 Принят на работу</option>
<option value="dismissed">👋 Уволен</option>
<option value="transferred">🔄 Переведен</option>
<option value="on_leave">🏖️ В отпуске</option>
<option value="returned_from_leave">↩️ Вернулся из отпуска</option>
<option value="on_maternity">👶 В декрете</option>
<option value="returned_from_maternity">↩️ Вернулся из декрета</option>
<option value="rehired">🔄 Восстановлен</option>
</select>
</div>
<div class="mb-3">
<label for="actionDate" class="form-label">Дата события</label>
<input type="datetime-local" class="form-control" id="actionDate" name="date" required>
</div>
<div class="mb-3">
<label for="actionComment" class="form-label">Комментарий/причина</label>
<textarea class="form-control" id="actionComment" name="comment" rows="3" placeholder="Опционально"></textarea>
</div>
<div class="alert alert-warning d-none" id="dismissalWarning">
<i class="bi-exclamation-triangle-fill me-2"></i>
<strong>Внимание:</strong> Увольнение деактивирует сотрудника в системе и LDAP (если настроено).
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi-check-lg me-1"></i>Сохранить событие
</button>
<button type="button" class="btn btn-ghost" data-bs-toggle="collapse" data-bs-target="#actionsManageCollapse">
Отмена
</button>
</div>
<div class="mt-2">
<div class="alert alert-success d-none" id="actionSuccess"></div>
<div class="alert alert-danger d-none" id="actionError"></div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% endif %}
{# ====== НАВЫКИ ====== #}
<div class="mt-3 text-start" id="skillsBlock"
{% if perms.employees.manage_employee_skills or request.user.is_staff or request.user.is_superuser or is_me %}
data-editable="1"
{% endif %}>
<div class="ios-section-title mb-2"><i class="bi bi-stars me-1"></i>Навыки</div>
<div class="chips align-items-center">
{% with skills=emp.skills %}
{% if skills %}
{% for s in skills %}
{% firstof s.name s as sk_name %}
{% if sk_name %}
<span class="chip chip-skill"
data-skill-id="{{ s.id|default:'' }}"
data-skill-name="{{ sk_name }}">
<span class="lbl">{{ sk_name }}</span>
{% if perms.employees.manage_employee_skills or request.user.is_staff or request.user.is_superuser or is_me %}
<button type="button" class="chip-del-btn" title="Убрать навык" aria-label="Убрать навык">
<i class="bi bi-dash-lg"></i>
</button>
{% endif %}
</span>
{% endif %}
{% endfor %}
{% else %}
<span class="skills-empty"></span>
{% endif %}
{% endwith %}
{% if perms.employees.manage_employee_skills or request.user.is_staff or request.user.is_superuser or is_me %}
<button type="button" class="chip chip-add"
data-bs-toggle="collapse" data-bs-target="#skillsForm"
aria-expanded="false" aria-controls="skillsForm" title="Добавить навык">
<i class="bi bi-plus-lg"></i><span class="d-none d-sm-inline"></span>
</button>
{% endif %}
</div>
{% if perms.employees.manage_employee_skills or request.user.is_staff or request.user.is_superuser or is_me %}
<div id="skillsForm" class="collapse mt-2">
<form id="skillAddForm" class="skill-form"
action="{% url 'employees:skill_add' emp.id %}" method="post">
{% csrf_token %}
<div class="row g-2">
<div class="col-12">
<input class="form-control" name="skill_name" list="skillsAll"
placeholder="Выберите или введите новый навык" required>
<datalist id="skillsAll">
{% for sk in all_skills %}
{% if sk.name %}<option value="{{ sk.name }}"></option>{% endif %}
{% endfor %}
</datalist>
</div>
<div class="col-12 d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg me-1"></i>Добавить
</button>
<button type="button" class="btn btn-ghost btn-sm" data-bs-toggle="collapse" data-bs-target="#skillsForm">
Отмена
</button>
</div>
</div>
</form>
</div>
{% endif %}
</div>
</div>
</article>
</div>
<!-- RIGHT -->
<div class="col-md-8">
{% if can_edit %}
<div id="editFormWrap" class="collapse mb-3">
<article class="ios-card">
<div class="p-3">
<div class="d-flex align-items-center gap-2">
<i class="bi-pencil-square"></i>
<div class="ios-section-title mb-0">Редактирование профиля</div>
<div class="ms-auto">
<button type="button" class="btn btn-sm btn-ghost"
data-bs-toggle="collapse" data-bs-target="#editFormWrap">Скрыть</button>
</div>
</div>
<div class="mt-3">
{% if is_me %}
{% url 'employees:employee_edit_me' as edit_endpoint %}
{% else %}
{% url 'employees:employee_edit' emp.id as edit_endpoint %}
{% endif %}
{% include "employees/_employee_edit.html" with emp=emp endpoint=edit_endpoint %}
</div>
</div>
</article>
</div>
{% endif %}
<section class="ios-card mb-3">
<div class="p-3">
<div class="d-flex align-items-center gap-2 mb-3">
<i class="bi-activity"></i>
<div class="ios-section-title mb-0">Активность</div>
</div>
<div id="employeePosts">
{# Посты загружаются через JavaScript из employeeProfile.js #}
{% if posts %}
{% include "feed/_feed_cards.html" with items=posts %}
{% else %}
<div class="text-secondary small">Публикаций пока нет.</div>
{% endif %}
</div>
</div>
</section>
</div>
</div>
</div>
<!-- Модальное окно для просмотра публикации -->
{% include "feed/components/modal_post_detail.html" %}
<!-- Модальное окно для увеличенного просмотра аватара -->
<div class="modal fade" id="avatarModal" tabindex="-1" aria-labelledby="avatarModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content bg-transparent border-0">
<div class="modal-header border-0 pb-0">
<button type="button" class="btn-close btn-close-white ms-auto" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body text-center p-0">
<img id="avatarModalImage" src="" alt="Аватар" class="img-fluid rounded" style="max-height: 80vh; object-fit: contain;">
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{# === JavaScript логика === #}
{% include "employees/components/employee_detail_scripts.html" %}
<!-- Emoji Picker Library -->
<script type="module" src="{% static 'vendor/emoji-picker/index.js' %}"></script>
{% endblock %}