diff --git a/backend/api/v1/employees/serializers.py b/backend/api/v1/employees/serializers.py index bbabfbce..307358d7 100644 --- a/backend/api/v1/employees/serializers.py +++ b/backend/api/v1/employees/serializers.py @@ -6,7 +6,7 @@ from io import BytesIO from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, Permission from django.core.files.base import ContentFile -from employees.constants import ACTION_CHOICES, DeptPerm +from employees.constants import ACTION_CHOICES from employees.models import ( Department, DepartmentRole, @@ -375,6 +375,7 @@ class EmployeeSerializer(serializers.ModelSerializer): class EmployeeBriefSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField() + avatar = Base64ImageField(read_only=True) class Meta: model = Employee @@ -386,6 +387,7 @@ class EmployeeBriefSerializer(serializers.ModelSerializer): "email", "phone_number", "display_name", + "avatar", ) def get_display_name(self, obj) -> str: diff --git a/backend/api/v1/employees/views.py b/backend/api/v1/employees/views.py index 39bfbdb5..a68ea6b4 100644 --- a/backend/api/v1/employees/views.py +++ b/backend/api/v1/employees/views.py @@ -881,33 +881,49 @@ class DepartmentViewSet(viewsets.ModelViewSet): @transaction.atomic def remove_member(self, request, pk: int | None = None): """ - Деактивирует (удаляет) сотрудника из отдела. - Роль НЕ изменяется. + Удаляет связь сотрудника с отделом (строку EmployeeDepartment), а не просто + помечает её неактивной. - Тело: + Тело запроса: { "employee_id": } + + Ограничения: + - Нельзя удалить руководителя отдела (вернёт 400). + Права: ManagePerm (DeptPerm.MANAGE) + + Ответ: + 200 OK + { + "employee_id": , + "removed": true + } """ dept = self.get_object() + payload = RemoveMemberInput(data=request.data) payload.is_valid(raise_exception=True) + emp_id: int = payload.validated_data["employee_id"] - emp_id = payload.validated_data["employee_id"] + # Защита от удаления руководителя + if dept.head_id == emp_id: + return Response( + {"detail": "Нельзя удалить руководителя отдела."}, + status=status.HTTP_400_BAD_REQUEST, + ) link = get_object_or_404( - EmployeeDepartment, employee_id=emp_id, department_id=dept.id + EmployeeDepartment, + employee_id=emp_id, + department_id=dept.id, ) - if link.is_active: - link.is_active = False - link.save(update_fields=["is_active"]) + + # Жёсткое удаление связи + link.delete() return Response( - { - "employee_id": emp_id, - "is_active": False, - "role_id": (link.role_id if getattr(link, "role_id", None) else None), - }, + {"employee_id": emp_id, "removed": True}, status=status.HTTP_200_OK, ) diff --git a/backend/employees/api_client.py b/backend/employees/api_client.py deleted file mode 100644 index 8f7a478e..00000000 --- a/backend/employees/api_client.py +++ /dev/null @@ -1,82 +0,0 @@ -# employees/api_client.py -import requests -from django.conf import settings -from django.http import HttpRequest -from django.urls import reverse - - -class APIError(Exception): - pass - - -def _json_safe(x): - try: - from phonenumber_field.phonenumber import PhoneNumber - except Exception: - PhoneNumber = None - if isinstance(x, dict): - return {k: _json_safe(v) for k, v in x.items()} - if isinstance(x, (list, tuple)): - return [_json_safe(v) for v in x] - if PhoneNumber and isinstance(x, PhoneNumber): - return x.as_e164 or x.raw_input or str(x) - return x - - -def _abs_url( - request: HttpRequest, url_name: str, url_kwargs: dict | None = None -) -> str: - rel = reverse(url_name, kwargs=url_kwargs or None) - return request.build_absolute_uri(rel) - - -def api_post(request: HttpRequest, url_name: str, payload: dict, **kwargs): - url = _abs_url(request, url_name) - payload = _json_safe(payload or {}) - headers = {"Accept": "application/json"} - try: - resp = requests.post(url, json=payload, headers=headers, timeout=10, **kwargs) - except requests.RequestException as e: - raise APIError(f"api_network_error: {e}") - try: - data = resp.json() - except ValueError: - # ← Вот тут будем видеть «почему» - snippet = (resp.text or "")[:500] - raise APIError(f"api_bad_json status={resp.status_code} body={snippet!r}") - return resp.status_code, data - - -def _session_cookies(request: HttpRequest) -> dict: - sid_name = getattr(settings, "SESSION_COOKIE_NAME", "sessionid") - sid_value = request.COOKIES.get(sid_name) - return {sid_name: sid_value} if sid_value else {} - - -def api_get( - request: HttpRequest, - url_name: str, - params: dict | None = None, - url_kwargs: dict | None = None, - **req_kwargs, -): - url = _abs_url(request, url_name, url_kwargs) - headers = {"Accept": "application/json"} - cookies = _session_cookies(request) - try: - resp = requests.get( - url, - params=params or {}, - headers=headers, - cookies=cookies, - timeout=10, - **req_kwargs, - ) - except requests.RequestException as e: - raise APIError(f"api_network_error: {e}") - try: - data = resp.json() - except ValueError: - snippet = (resp.text or "")[:500] - raise APIError(f"api_bad_json status={resp.status_code} body={snippet!r}") - return resp.status_code, data diff --git a/backend/employees/models.py b/backend/employees/models.py index de340817..d1226127 100644 --- a/backend/employees/models.py +++ b/backend/employees/models.py @@ -346,7 +346,7 @@ class Department(models.Model): employee_id=old_head_id, department_id=self.pk, defaults={ - "is_active": False, + "is_active": True, "date_from": timezone.now().date(), "date_to": timezone.now().date(), }, @@ -354,7 +354,7 @@ class Department(models.Model): if not created: EmployeeDepartment.objects.filter( employee_id=old_head_id, department_id=self.pk - ).update(is_active=False, date_to=timezone.now().date()) + ).update(is_active=True, date_to=timezone.now().date()) else: # Смена A -> B: по текущей логике бывший остаётся сотрудником отдела (активным) # Если хотите наоборот — поменяйте is_active=True на False и добавьте date_to. diff --git a/backend/employees/views_front.py b/backend/employees/views_front.py index 9dc37e48..8a92e6aa 100644 --- a/backend/employees/views_front.py +++ b/backend/employees/views_front.py @@ -106,15 +106,6 @@ def _api_ok_or_error(resp) -> Tuple[bool, Dict[str, Any], int]: return resp.ok, data, getattr(resp, "status", 500) -def _has_role(link): - r = link.get("role") - if r is None: - return False - if isinstance(r, dict): - return bool(r.get("id") or r.get("name")) - s = str(r).strip() - return s not in {"", "None", "null", "0"} - # ========================= # Сотрудники @@ -627,7 +618,9 @@ def set_member_role(request, dept_id: int, emp_id: int): role_id = None # заодно не даём менять самому себе (UI тоже скрывает) - if str(emp_id) == str(getattr(request.user, "id", None)): + if (str(emp_id) == str(getattr(request.user, "id", None))) and not ( + request.user.is_staff or request.user.is_superuser + ): messages.error(request, "Нельзя изменять собственную роль в отделе.") return redirect( request.POST.get("next") or reverse("employees:profile", args=("me",)) diff --git a/backend/templates/base.html b/backend/templates/base.html index 4ef958d4..a7cfc7c0 100644 --- a/backend/templates/base.html +++ b/backend/templates/base.html @@ -15,7 +15,6 @@ integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> - + {% block extra_css %}{% endblock %} @@ -241,8 +476,6 @@ }); - - - {% block extra_js %}{% endblock %} diff --git a/backend/templates/employees/_department_controls.html b/backend/templates/employees/_department_controls.html index 69f04a10..4fc88ed7 100644 --- a/backend/templates/employees/_department_controls.html +++ b/backend/templates/employees/_department_controls.html @@ -1,78 +1,66 @@ {# backend/templates/employees/_department_controls.html #} -{# Контекст:#} - {# dept, head_choices, roles, dept_perm_choices,#} - {# can_manage, can_assign_roles, can_change_head#} +{# Контекст: dept, head_choices, roles, dept_perm_choices, can_manage, can_assign_roles, can_change_head #} +{# Модалки поверх любых локальных dropdown/оверлеев #} + {% if can_manage %} -
-
- -
-
Управление отделом
-
Название и описание
-
+
+
+ +
Управление отделом
-
+
{% csrf_token %} - - -
- -
+
+
- +
-
+
-
-
+
-
-
+ {% endif %} - {% if can_change_head %} -
-
+
+
-
-
Руководитель отдела
-
Назначение / снятие
-
+
Руководитель отдела
-
+
{% csrf_token %} - -
-
+
+
-
- - - - - - - - -
Выберите сотрудника из списка, введите точный email или очистите поле.
-
Очистите поле и сохраните, чтобы снять руководителя.
-
+
@@ -112,137 +87,121 @@
-
+ {% endif %} - {% if can_assign_roles %} -
-
+
+
-
-
Роли отдела
-
Редактирование и создание
-
+
Роли отдела
-
-
- +
+
-
- {% csrf_token %} - - - -
+
+ + {% csrf_token %} + + +
-
-
- +
+
+ +
+ + + +
Выберите роль из списка или введите новое название для создания.
+
+
-
+
+ +
+
- - - - - - - - - -
Выберите роль из списка или введите новое название для создания.
+
+ + +
+ +
-
- -
-
- -
- - - -
-
- - -
+ {# ——— УБРАНО
——— #} -
- {% csrf_token %} - - -
+
+ + {% csrf_token %} + +
-
-
- - +
+
+ + +
+ +
+ +
+
+ +
+ + +
- -
- -
-
- -
- - -
-
- - + +
-
+ {% endif %} - {% if not can_manage and not can_assign_roles and not can_change_head %}
У вас нет прав для управления этим отделом.
{% endif %} @@ -310,16 +269,16 @@ -{# ===== Форма удаления роли (постит на edit_department_role/) ===== #} +{# ===== Форма удаления роли ===== #}
{% csrf_token %}
+{# ===== JS: автогроу / пикеры / роли ===== #} + + + + \ No newline at end of file diff --git a/backend/templates/employees/department_detail.html b/backend/templates/employees/department_detail.html index 57d48047..8b04d2fb 100644 --- a/backend/templates/employees/department_detail.html +++ b/backend/templates/employees/department_detail.html @@ -1,4 +1,4 @@ -{# templates/employees/department_detail.html #} +{# backend/templates/employees/department_detail.html #} {% extends 'base.html' %} {% load static django_bootstrap5 %} @@ -7,201 +7,247 @@ {% block content %} {% with dept_id=dept.id %}
-
-
- {# ===== Заголовок ===== #} -
- -

{{ dept.name }}

+ {# ===== Заголовок ===== #} +
+ +

{{ dept.name }}

+ {% if user.is_staff or user.is_superuser or can_manage or dept.head.id == user.id %} +
-
- {% url 'feed:post_create' as post_create_url %} - {% if post_create_url %} - - Создать публикацию - - {% endif %} - {% url 'employees:invite_to_department' dept_id as invite_url %} - {% if user.is_staff or user.is_superuser or can_manage or dept.head and dept.head.id == user.id %} - - {% endif %} -
-
+ {# Добавить участника #} + - {# ===== Инфо-блок ===== #} - {% if dept.description or dept.head %} -
- {% if dept.description %} -
- {{ dept.description }} -
- {% endif %} - - {% if dept.head %} -
- Руководитель: - {% url 'employees:employee_detail' dept.head.id as head_url %} - {% if head_url %} - - {{ dept.head.last_name }}{% if dept.head.first_name %} {{ dept.head.first_name }}{% endif %} - - {% else %} - {{ dept.head.last_name }}{% if dept.head.first_name %} {{ dept.head.first_name }}{% endif %} - {% endif %} -
- {% endif %} - - {# «голая» шестерёнка, немного отступлена от краёв #} - -
+ {% endif %} {% endif %} - {# ===== Управление отделом (для staff/главы) ===== #} - {% if user.is_staff or dept.head and dept.head.id == user.id %} -
- {% include "employees/_department_controls.html" with dept=dept edit_form=edit_form head_choices=head_choices %} -
- {% endif %} + {# Удалить участника #} + - {# ===== Команда отдела — как в "Профиль" + две кнопки ===== #} -
-
- -
-
Команда отдела
-
Участники и их роли
+ {% endif %} -
- {% url 'employees:employees_list' as employees_list_url %} - {% if employees_list_url %} - - Все сотрудники → +
+ {% url 'feed:post_create' as post_create_url %} + {% if user.is_authenticated and user.id in members_exclude_ids %} + + Создать публикацию {% endif %} - {% if user.is_staff or user.is_superuser or can_manage or dept.head and dept.head.id == user.id %} - - - - {% endif %} +
-
-
- {% for link in links %} - {% if link.role %} - {% with emp=link.employee cur_role_id=link.role.id|default:link.role %} - {% with eid=emp.id|default:emp %} -
- + {# ===== Две колонки как в employee_detail: слева основное, справа сайдбар ===== #} +
-
- - {% if link.role.name %}{{ link.role.name }}{% else %}Роль #{{ cur_role_id }}{% endif %} - + {# ===== ЛЕВАЯ КОЛОНКА (основная, как в профиле): команда + инфо ===== #} +
+ {# --- Команда отдела (круг) --- #} +
+
+ + +
+
{% url 'employees:employees_list' as employees_list_url %} + {% if employees_list_url %} + + Команда отдела → + + {% endif %}
+
+
+ +
+
- {% if user.is_staff or user.is_superuser or dept.head and dept.head.id == user.id %} - {% if roles %} -
- {% csrf_token %} - - - -
- {% endif %} - {% endif %} + {# КРУГ #} +
+
+
+
+
+
+
- {% endwith %} - {% endwith %} - {% endif %} - {% empty %} -
-
—
-
Пока нет сотрудников с назначёнными ролями.
+ + {# Слой фокуса #} +
+
+ + +
+
+ + {# ПОЛУКРУЖНЫЙ ИНДИКАТОР СКРОЛЛА (справа по дуге) #} + +
+ + {# Источник данных для круга #} +
+ {% for li in links %} + {% with emp=li.employee %} +
+ {% endwith %} + {% empty %} + {% endfor %} +
+ + {# Текст под кругом #} +
+
+
+
+
+
+
+ +
- {% endfor %} +
+ + {# --- Инфо-блок отдела (под командой) --- #} + {% if dept.description or dept.head %} +
+
+ {% if dept.description %} +
+ {{ dept.description }} +
+ {% endif %} + + {% if dept.head %} +
+ Руководитель: + {% url 'employees:employee_detail' dept.head.id as head_url %} + {% if head_url %} + + {{ dept.head.last_name }}{% if dept.head.first_name %} {{ dept.head.first_name }}{% endif %} + + {% else %} + {{ dept.head.last_name }}{% if dept.head.first_name %} {{ dept.head.first_name }}{% endif %} + {% endif %} +
+ {% endif %} + + {% if user.is_staff or dept.head.id == user.id or can_manage or can_assign_roles or can_change_head %} + + {% endif %} +
+
+ {% endif %} +
-
-
+ {# ===== ПРАВАЯ КОЛОНКА (сайдбар): публикации/активность ===== #} +
+ {% if user.is_staff or user.is_superuser or can_manage or dept.head.id == user.id %} +
+
+
+
+ +
Настройки отдела
+
+ +
+
+
+ {% include "employees/_department_controls.html" with dept=dept edit_form=edit_form head_choices=head_choices %} +
+
+
+
+ {% endif %} +
+
+ +
+
Публикации отдела
+
+
+
+ {% if posts %} + {% include "feed/_feed_cards.html" with items=posts only %} + {% else %} +
Публикаций пока нет.
+ {% endif %} +
+
- - {# ===== Лента отдела ===== #} -
- {% if posts %} - {% include "feed/_feed_cards.html" with items=posts only %} - {% else %} -
Публикаций пока нет.
- {% endif %} -
- - {# Простейшая пагинация без Page-объекта #} + {# Простая пагинация (если нужно) #} {% if page|default:0 %} -
+
{% if page|add:"-1" %} ← Назад {% endif %}
{% endif %} -
+
+ {# ---------- МОДАЛ «Удалить участника» ---------- #} -{% if user.is_staff or user.is_superuser or can_manage or dept.head and dept.head.id == user.id %} +{% if user.is_staff or user.is_superuser or can_manage or dept.head.id == user.id %}