743 lines
25 KiB
Python
743 lines
25 KiB
Python
# backend/employees/views_front.py
|
||
|
||
from __future__ import annotations
|
||
|
||
import base64
|
||
import mimetypes
|
||
from typing import Any
|
||
|
||
from api.client import get_api_client
|
||
from api.decorators import require_api_auth
|
||
from django.contrib import messages
|
||
from django.contrib.auth.decorators import login_required
|
||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||
from django.shortcuts import get_object_or_404, redirect, render
|
||
from django.urls import reverse, reverse_lazy
|
||
from django.utils import timezone
|
||
from django.views.decorators.http import require_http_methods
|
||
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||
from django.views.decorators.http import require_POST
|
||
from django.http import JsonResponse
|
||
|
||
from .forms import (
|
||
DepartmentEditForm,
|
||
DepartmentForm,
|
||
DepartmentMemberRoleForm,
|
||
InviteToDepartmentForm,
|
||
SkillForm,
|
||
)
|
||
from .forms_front import DepartmentEditForm, SetHeadForm, SetMemberRoleForm
|
||
from .models import Department, EmployeeDepartment, Skill
|
||
|
||
|
||
# =========================
|
||
# Утилиты
|
||
# =========================
|
||
|
||
|
||
def _file_to_data_uri(uploaded) -> str:
|
||
"""
|
||
Превращаем загруженный файл (InMemoryUploadedFile/TemporaryUploadedFile)
|
||
в data URI (data:image/...;base64,xxxx).
|
||
"""
|
||
if not uploaded:
|
||
return ""
|
||
content = uploaded.read()
|
||
if hasattr(uploaded, "seek"):
|
||
uploaded.seek(0)
|
||
mime = (
|
||
getattr(uploaded, "content_type", None)
|
||
or mimetypes.guess_type(getattr(uploaded, "name", ""))[0]
|
||
or "application/octet-stream"
|
||
)
|
||
b64 = base64.b64encode(content).decode("ascii")
|
||
return f"data:{mime};base64,{b64}"
|
||
|
||
|
||
def _extract_items(payload: Any):
|
||
"""
|
||
Универсальный парсер: пагинированный ответ DRF или «сырой» список.
|
||
"""
|
||
if isinstance(payload, dict) and "results" in payload:
|
||
return (
|
||
payload["results"],
|
||
payload.get("count", 0),
|
||
payload.get("next"),
|
||
payload.get("previous"),
|
||
)
|
||
if isinstance(payload, list):
|
||
return payload, len(payload), None, None
|
||
return [], 0, None, None
|
||
|
||
|
||
def _cleanup_payload(data: dict, keep_empty: set[str] = frozenset()) -> dict:
|
||
"""
|
||
Убираем None и пустые строки, КРОМЕ ключей из keep_empty.
|
||
"""
|
||
cleaned = {}
|
||
for k, v in data.items():
|
||
if v is None:
|
||
continue
|
||
if v == "" and k not in keep_empty:
|
||
continue
|
||
cleaned[k] = v
|
||
return cleaned
|
||
|
||
|
||
def _extract_results(payload):
|
||
if isinstance(payload, dict):
|
||
return payload.get("results", payload.get("items", [])) or []
|
||
return payload or []
|
||
|
||
|
||
def _fetch_employee_posts(api, employee_id: int):
|
||
# сначала пробуем ?author=, если в API другое имя фильтра — fallback на ?employee=
|
||
for url in (
|
||
f"v1/posts/?author={employee_id}&ordering=-created_at",
|
||
f"v1/posts/?employee={employee_id}&ordering=-created_at",
|
||
):
|
||
r = api.get(url)
|
||
if r.ok:
|
||
data = r.json or []
|
||
return _extract_results(data)
|
||
|
||
|
||
# =========================
|
||
# Сотрудники
|
||
# =========================
|
||
|
||
|
||
# ---------- LIST ----------
|
||
@require_api_auth
|
||
def employee_list(request):
|
||
"""
|
||
Список сотрудников.
|
||
Ходит в /api/v1/employees/ c поддержкой поиска/сортировки/пагинации.
|
||
"""
|
||
api = get_api_client(request)
|
||
|
||
page = request.GET.get("page") or 1
|
||
ordering = request.GET.get("o") or "last_name"
|
||
q = (request.GET.get("q") or "").strip()
|
||
department = request.GET.get(
|
||
"department"
|
||
) # опциональный фильтр по отделу (из detail отдела)
|
||
|
||
params = {"page": page, "ordering": ordering}
|
||
if q:
|
||
params["search"] = q
|
||
if department:
|
||
params["department"] = department
|
||
|
||
resp = api.get("v1/employees/", params=params)
|
||
if not resp.ok:
|
||
messages.error(request, f"Ошибка API ({resp.status})")
|
||
return render(
|
||
request,
|
||
"employees/employees_list.html",
|
||
{"employees": [], "q": q, "o": ordering, "page": page},
|
||
)
|
||
|
||
payload = resp.json or {}
|
||
items, count, next_url, prev_url = _extract_items(payload)
|
||
context = {
|
||
"employees": items,
|
||
"count": count,
|
||
"q": q,
|
||
"o": ordering,
|
||
"page": page,
|
||
"next_url": next_url,
|
||
"prev_url": prev_url,
|
||
}
|
||
return render(request, "employees/employees_list.html", context)
|
||
|
||
|
||
# ---------- DETAIL ----------
|
||
@require_api_auth
|
||
def employee_detail(request, pk: int):
|
||
api = get_api_client(request)
|
||
resp = api.get(f"v1/employees/{pk}/")
|
||
if not resp.ok:
|
||
messages.error(request, f"Сотрудник не найден ({resp.status})")
|
||
return redirect("employees:employees_list")
|
||
|
||
emp = resp.json or {}
|
||
can_edit = bool(
|
||
request.user and (request.user.is_staff or request.user.id == emp.get("id"))
|
||
)
|
||
posts = _fetch_employee_posts(api, pk)
|
||
sresp = api.get("v1/skills/", params={"ordering": "name"})
|
||
all_skills = sresp.json if sresp.ok else []
|
||
return render(
|
||
request,
|
||
"employees/employee_detail.html",
|
||
{"emp": emp, "can_edit": can_edit, "all_skills": all_skills, "posts": posts},
|
||
)
|
||
|
||
|
||
# ---------- ME ----------
|
||
@require_api_auth
|
||
def employee_me(request):
|
||
api = get_api_client(request)
|
||
|
||
resp = api.get("v1/employees/me/")
|
||
if not resp.ok:
|
||
messages.error(request, f"Ошибка API ({resp.status})")
|
||
return redirect("employees:employees_list")
|
||
|
||
emp = resp.json or {}
|
||
|
||
# новости сотрудника
|
||
posts = _fetch_employee_posts(api, emp.get("id"))
|
||
|
||
# все навыки для даталиста (разворачиваем results)
|
||
sresp = api.get("v1/skills/", params={"ordering": "name"})
|
||
data = sresp.json if sresp.ok else []
|
||
all_skills = data.get("results", data) if isinstance(data, dict) else data
|
||
|
||
return render(
|
||
request,
|
||
"employees/employee_detail.html",
|
||
{
|
||
"emp": emp,
|
||
"can_edit": True,
|
||
"is_me": True,
|
||
"all_skills": all_skills,
|
||
"posts": posts,
|
||
},
|
||
)
|
||
|
||
|
||
# ---------- EDIT (self or staff) ----------
|
||
@require_api_auth
|
||
@require_http_methods(["GET", "POST"])
|
||
def employee_edit(request, pk: int):
|
||
"""
|
||
Редактирование пользователя по id.
|
||
Разрешено staff/superuser или самому себе (проверку делает API).
|
||
"""
|
||
api = get_api_client(request)
|
||
|
||
if request.method == "GET":
|
||
resp = api.get(f"v1/employees/{pk}/")
|
||
if not resp.ok:
|
||
messages.error(request, f"Сотрудник не найден ({resp.status})")
|
||
return redirect("employees:employees_list")
|
||
emp = resp.json or {}
|
||
return render(request, "employees/employee_edit.html", {"emp": emp})
|
||
|
||
# POST → PATCH
|
||
data = {
|
||
"last_name": request.POST.get("last_name", "").strip(),
|
||
"first_name": request.POST.get("first_name", "").strip(),
|
||
"patronymic": request.POST.get("patronymic", "").strip(),
|
||
"gender": request.POST.get("gender") or None,
|
||
"birth_date": request.POST.get("birth_date") or None,
|
||
"position": request.POST.get("position") or None,
|
||
"telegram": request.POST.get("telegram", "").strip(),
|
||
"whatsapp": request.POST.get("whatsapp", "").strip(),
|
||
"wechat": request.POST.get("wechat", "").strip(),
|
||
}
|
||
|
||
# skills (множественный select): skills=1&skills=2 …
|
||
skills = request.POST.getlist("skills")
|
||
if skills:
|
||
data["skills"] = skills
|
||
|
||
# avatar: либо новый файл, либо снять (clear_avatar=on)
|
||
if request.FILES.get("avatar"):
|
||
data["avatar"] = _file_to_data_uri(request.FILES["avatar"])
|
||
elif request.POST.get("clear_avatar") == "on":
|
||
data["avatar"] = "" # Base64ImageField очистит
|
||
|
||
# удаляем пустые строки, чтобы не перетирать nullables
|
||
data = _cleanup_payload(data, keep_empty={"avatar"})
|
||
|
||
resp = api.patch(f"v1/employees/{pk}/", json=data)
|
||
if not resp.ok:
|
||
messages.error(request, f"Не удалось сохранить: {resp.status} — {resp.text}")
|
||
# Подставим обратно форму с введёнными данными
|
||
current = api.get(f"v1/employees/{pk}/").json or {}
|
||
current.update(
|
||
{k: request.POST.get(k) for k in ["last_name", "first_name", "patronymic"]}
|
||
)
|
||
return render(request, "employees/employee_edit.html", {"emp": current})
|
||
|
||
messages.success(request, "Профиль обновлён.")
|
||
return redirect("employees:employee_detail", pk=pk)
|
||
|
||
|
||
# ---------- EDIT ME ----------
|
||
@require_api_auth
|
||
@require_http_methods(["GET", "POST"])
|
||
def employee_edit_me(request):
|
||
"""
|
||
Редактирование собственного профиля через /api/v1/employees/me/
|
||
"""
|
||
api = get_api_client(request)
|
||
|
||
if request.method == "GET":
|
||
resp = api.get("v1/employees/me/")
|
||
if not resp.ok:
|
||
messages.error(request, f"Ошибка API ({resp.status})")
|
||
return redirect("employees:employees_list")
|
||
emp = resp.json or {}
|
||
return render(
|
||
request, "employees/employee_edit.html", {"emp": emp, "is_me": True}
|
||
)
|
||
|
||
data = {
|
||
"last_name": request.POST.get("last_name", "").strip(),
|
||
"first_name": request.POST.get("first_name", "").strip(),
|
||
"patronymic": request.POST.get("patronymic", "").strip(),
|
||
"gender": request.POST.get("gender") or None,
|
||
"birth_date": request.POST.get("birth_date") or None,
|
||
"position": request.POST.get("position") or None,
|
||
"telegram": request.POST.get("telegram", "").strip(),
|
||
"whatsapp": request.POST.get("whatsapp", "").strip(),
|
||
"wechat": request.POST.get("wechat", "").strip(),
|
||
}
|
||
skills = request.POST.getlist("skills")
|
||
if skills:
|
||
data["skills"] = skills
|
||
|
||
if request.FILES.get("avatar"):
|
||
data["avatar"] = _file_to_data_uri(request.FILES["avatar"])
|
||
elif request.POST.get("clear_avatar") == "on":
|
||
data["avatar"] = ""
|
||
|
||
data = _cleanup_payload(data, keep_empty={"avatar"})
|
||
|
||
resp = api.patch("v1/employees/me/", json=data)
|
||
if not resp.ok:
|
||
messages.error(request, f"Не удалось сохранить: {resp.status} — {resp.text}")
|
||
current = api.get("v1/employees/me/").json or {}
|
||
current.update(
|
||
{k: request.POST.get(k) for k in ["last_name", "first_name", "patronymic"]}
|
||
)
|
||
return render(
|
||
request, "employees/employee_edit.html", {"emp": current, "is_me": True}
|
||
)
|
||
|
||
messages.success(request, "Профиль обновлён.")
|
||
return redirect("employees:employee_me")
|
||
|
||
|
||
# ---------- CREATE (staff) ----------
|
||
@require_api_auth
|
||
@require_http_methods(["GET", "POST"])
|
||
def employee_create(request):
|
||
api = get_api_client(request)
|
||
|
||
if request.method == "GET":
|
||
return render(request, "employees/employee_create.html")
|
||
|
||
data = {
|
||
"email": request.POST.get("email", "").strip(),
|
||
"phone_number": request.POST.get("phone_number", "").strip(),
|
||
"last_name": request.POST.get("last_name", "").strip(),
|
||
"first_name": request.POST.get("first_name", "").strip(),
|
||
"patronymic": request.POST.get("patronymic", "").strip(),
|
||
"gender": request.POST.get("gender") or None,
|
||
"birth_date": request.POST.get("birth_date") or None,
|
||
"position": request.POST.get("position") or None,
|
||
"telegram": request.POST.get("telegram", "").strip(),
|
||
"whatsapp": request.POST.get("whatsapp", "").strip(),
|
||
"wechat": request.POST.get("wechat", "").strip(),
|
||
}
|
||
if request.FILES.get("avatar"):
|
||
data["avatar"] = _file_to_data_uri(request.FILES["avatar"])
|
||
skills = request.POST.getlist("skills")
|
||
if skills:
|
||
data["skills"] = skills
|
||
password = request.POST.get("password", "").strip()
|
||
if password:
|
||
data["password"] = password # API сгенерирует сам, если не передали
|
||
|
||
resp = api.post("v1/employees/", json=data)
|
||
if not resp.ok:
|
||
messages.error(request, f"Не удалось создать: {resp.status} — {resp.text}")
|
||
return render(request, "employees/employee_create.html", {"form": request.POST})
|
||
|
||
new_emp = resp.json or {}
|
||
messages.success(request, "Сотрудник создан.")
|
||
return redirect("employees:employee_detail", pk=new_emp.get("id"))
|
||
|
||
|
||
# =========================
|
||
# Отделы
|
||
# =========================
|
||
|
||
|
||
# ---------- LIST ----------
|
||
@require_api_auth
|
||
def department_list(request):
|
||
"""
|
||
Список отделов.
|
||
Ходит в /api/v1/departments/ c поддержкой поиска/сортировки/пагинации DRF.
|
||
"""
|
||
api = get_api_client(request)
|
||
|
||
page = request.GET.get("page") or 1
|
||
ordering = request.GET.get("o") or "name"
|
||
q = (request.GET.get("q") or "").strip()
|
||
|
||
params = {"page": page, "ordering": ordering}
|
||
if q:
|
||
params["search"] = q # DRF SearchFilter
|
||
|
||
resp = api.get("v1/departments/", params=params)
|
||
if not resp.ok:
|
||
messages.error(request, f"Ошибка API ({resp.status})")
|
||
return render(
|
||
request,
|
||
"employees/department_list.html",
|
||
{
|
||
"departments": [],
|
||
"count": 0,
|
||
"q": q,
|
||
"o": ordering,
|
||
"page": page,
|
||
"next_url": None,
|
||
"prev_url": None,
|
||
},
|
||
)
|
||
|
||
payload = resp.json # важно: без "or {}" — иначе потеряем список
|
||
|
||
# ✅ поддерживаем оба формата DRF: dict с results или list без пагинации
|
||
if isinstance(payload, dict) and "results" in payload:
|
||
items = payload["results"]
|
||
count = payload.get("count", len(items))
|
||
next_url = payload.get("next")
|
||
prev_url = payload.get("previous")
|
||
elif isinstance(payload, list):
|
||
items = payload
|
||
count = len(items)
|
||
next_url = prev_url = None
|
||
else:
|
||
items = []
|
||
count = 0
|
||
next_url = prev_url = None
|
||
|
||
context = {
|
||
"departments": items,
|
||
"count": count,
|
||
"q": q,
|
||
"o": ordering,
|
||
"page": page,
|
||
"next_url": next_url,
|
||
"prev_url": prev_url,
|
||
}
|
||
return render(request, "employees/department_list.html", context)
|
||
|
||
|
||
# ---------- DETAIL ----------
|
||
@require_api_auth
|
||
def department_detail(request, pk: int):
|
||
"""
|
||
Детальная страница отдела.
|
||
Тянет:
|
||
- /api/v1/departments/{id}/
|
||
- /api/v1/department-roles/?department={id}
|
||
- /api/v1/employee-departments/?department={id}&is_active=true
|
||
"""
|
||
api = get_api_client(request)
|
||
|
||
dep = api.get(f"v1/departments/{pk}/")
|
||
if not dep.ok:
|
||
messages.error(
|
||
request,
|
||
"Отдел не найден" if dep.status == 404 else f"Ошибка API ({dep.status})",
|
||
)
|
||
return redirect("employees:dept_list")
|
||
dept = dep.json
|
||
|
||
roles_resp = api.get(
|
||
"v1/department-roles/", params={"department": pk, "page_size": 1000}
|
||
)
|
||
roles = (
|
||
roles_resp.json.get("results", [])
|
||
if roles_resp.ok and isinstance(roles_resp.json, dict)
|
||
else roles_resp.json or []
|
||
)
|
||
|
||
links_resp = api.get(
|
||
"v1/employee-departments/",
|
||
params={"department": pk, "is_active": True, "page_size": 1000},
|
||
)
|
||
links = (
|
||
links_resp.json.get("results", [])
|
||
if links_resp.ok and isinstance(links_resp.json, dict)
|
||
else links_resp.json or []
|
||
)
|
||
|
||
context = {
|
||
"dept": dept,
|
||
"roles": roles,
|
||
"links": links, # элементы вида {"employee": <id>, "role": <id>|null, ...}
|
||
"edit_form": DepartmentEditForm(
|
||
initial={
|
||
"name": dept.get("name", ""),
|
||
"description": dept.get("description", ""),
|
||
}
|
||
),
|
||
"set_head_form": SetHeadForm(),
|
||
"set_member_role_form": SetMemberRoleForm(),
|
||
}
|
||
page = request.GET.get("page") or 1
|
||
r = api.get(
|
||
"v1/feed/posts/",
|
||
params={"department": pk, "page": page, "ordering": "-created_at"},
|
||
)
|
||
posts = []
|
||
if r.ok:
|
||
data = r.json
|
||
posts = data.get("results", data) if isinstance(data, dict) else data
|
||
|
||
context.update(
|
||
{
|
||
"posts": posts,
|
||
"page": page, # если пагинация нужна в шаблоне
|
||
}
|
||
)
|
||
return render(request, "employees/department_detail.html", context)
|
||
|
||
|
||
# ---------- EDIT BASIC FIELDS ----------
|
||
@require_api_auth
|
||
@require_http_methods(["POST"])
|
||
def department_edit(request, pk: int):
|
||
"""
|
||
Редактирование name/description через PATCH /api/v1/departments/{id}/.
|
||
Права проверяет API (manage_department).
|
||
"""
|
||
api = get_api_client(request)
|
||
form = DepartmentEditForm(request.POST)
|
||
if not form.is_valid():
|
||
messages.error(request, "Проверьте форму")
|
||
return redirect("employees:dept_detail", pk=pk)
|
||
|
||
payload = {
|
||
"name": form.cleaned_data["name"],
|
||
"description": form.cleaned_data.get("description") or "",
|
||
}
|
||
resp = api.patch(f"v1/departments/{pk}/", json=payload)
|
||
if not resp.ok:
|
||
messages.error(
|
||
request,
|
||
f"Не удалось обновить отдел ({resp.status}) — {resp.json or resp.text}",
|
||
)
|
||
else:
|
||
messages.success(request, "Отдел обновлён")
|
||
return redirect("employees:dept_detail", pk=pk)
|
||
|
||
|
||
# ---------- SET HEAD ----------
|
||
@require_api_auth
|
||
@require_http_methods(["POST"])
|
||
def department_set_head(request, pk: int):
|
||
"""
|
||
Назначение/снятие руководителя через POST /api/v1/departments/{id}/set_head/.
|
||
Права проверяет API (change_department_head).
|
||
"""
|
||
api = get_api_client(request)
|
||
form = SetHeadForm(request.POST)
|
||
if not form.is_valid():
|
||
messages.error(request, "Проверьте форму")
|
||
return redirect("employees:dept_detail", pk=pk)
|
||
|
||
head_id = form.cleaned_data.get("head_id", None)
|
||
payload = {"head_id": head_id if head_id is not None else None} # None → снять
|
||
resp = api.post(f"v1/departments/{pk}/set_head/", json=payload)
|
||
if not resp.ok:
|
||
messages.error(
|
||
request,
|
||
f"Не удалось изменить руководителя ({resp.status}) — {resp.json or resp.text}",
|
||
)
|
||
else:
|
||
messages.success(
|
||
request, "Руководитель обновлён" if head_id else "Руководитель снят"
|
||
)
|
||
return redirect("employees:dept_detail", pk=pk)
|
||
|
||
|
||
# ---------- SET MEMBER ROLE ----------
|
||
@require_api_auth
|
||
@require_http_methods(["POST"])
|
||
def department_set_member_role(request, pk: int):
|
||
"""
|
||
Назначение/снятие роли участнику отдела через POST /api/v1/departments/{id}/set_member_role/.
|
||
Права проверяет API (assign_department_role).
|
||
"""
|
||
api = get_api_client(request)
|
||
form = SetMemberRoleForm(request.POST)
|
||
if not form.is_valid():
|
||
messages.error(request, "Проверьте форму")
|
||
return redirect("employees:dept_detail", pk=pk)
|
||
|
||
payload = {
|
||
"employee_id": form.cleaned_data["employee_id"],
|
||
"role_id": form.cleaned_data.get("role_id", None),
|
||
"is_active": form.cleaned_data.get("is_active", None),
|
||
}
|
||
resp = api.post(f"v1/departments/{pk}/set_member_role/", json=payload)
|
||
if not resp.ok:
|
||
messages.error(
|
||
request,
|
||
f"Не удалось обновить роль ({resp.status}) — {resp.json or resp.text}",
|
||
)
|
||
else:
|
||
messages.success(request, "Роль участника обновлена")
|
||
return redirect("employees:dept_detail", pk=pk)
|
||
|
||
|
||
class StaffOnlyMixin(UserPassesTestMixin):
|
||
def test_func(self):
|
||
return self.request.user.is_staff
|
||
|
||
def handle_no_permission(self):
|
||
messages.error(self.request, "Недостаточно прав.")
|
||
return redirect("employees:department_list")
|
||
|
||
|
||
class DepartmentCreateView(LoginRequiredMixin, StaffOnlyMixin, CreateView):
|
||
model = Department
|
||
form_class = DepartmentForm
|
||
template_name = "employees/department_form.html"
|
||
success_url = reverse_lazy("employees:department_list")
|
||
|
||
|
||
class DepartmentUpdateView(LoginRequiredMixin, StaffOnlyMixin, UpdateView):
|
||
model = Department
|
||
form_class = DepartmentForm
|
||
template_name = "employees/department_form.html"
|
||
success_url = reverse_lazy("employees:department_list")
|
||
|
||
|
||
class DepartmentDeleteView(LoginRequiredMixin, StaffOnlyMixin, DeleteView):
|
||
model = Department
|
||
template_name = "employees/department_confirm_delete.html"
|
||
success_url = reverse_lazy("employees:department_list")
|
||
|
||
|
||
@login_required
|
||
def edit_department_role(request, dept_id, employee_id):
|
||
department = get_object_or_404(Department, pk=dept_id)
|
||
|
||
# только staff или руководитель отдела
|
||
if not (request.user.is_staff or department.head_id == request.user.id):
|
||
messages.error(request, "Нет прав изменять роли в этом отделе.")
|
||
return redirect("employees:employees_list")
|
||
|
||
link = get_object_or_404(
|
||
EmployeeDepartment,
|
||
department=department,
|
||
employee_id=employee_id,
|
||
)
|
||
|
||
if request.method == "POST":
|
||
form = DepartmentMemberRoleForm(request.POST, instance=link)
|
||
if form.is_valid():
|
||
form.save()
|
||
messages.success(request, "Роль в отделе обновлена.")
|
||
next_url = request.POST.get("next") or (
|
||
reverse("employees:employees_list") + f"?department={dept_id}"
|
||
)
|
||
return redirect(next_url)
|
||
else:
|
||
form = DepartmentMemberRoleForm(instance=link)
|
||
|
||
employee = link.employee
|
||
next_url = request.GET.get("next") or (
|
||
reverse("employees:employees_list") + f"?department={dept_id}"
|
||
)
|
||
return render(
|
||
request,
|
||
"employees/department_role_form.html",
|
||
{
|
||
"department": department,
|
||
"employee": employee,
|
||
"form": form,
|
||
"next_url": next_url,
|
||
},
|
||
)
|
||
|
||
|
||
# ---------- Приглашение в отдел ----------
|
||
|
||
|
||
@login_required
|
||
def invite_to_department(request, pk):
|
||
department = get_object_or_404(Department, pk=pk)
|
||
|
||
if not (request.user.is_staff or request.user == department.head):
|
||
messages.error(request, "Нет прав приглашать сотрудников.")
|
||
return redirect("employees:department_detail", pk=pk)
|
||
|
||
if request.method == "POST":
|
||
form = InviteToDepartmentForm(department, request.POST)
|
||
if form.is_valid():
|
||
employee = form.cleaned_data["employee"]
|
||
|
||
emp_dep, created = EmployeeDepartment.objects.get_or_create(
|
||
employee=employee,
|
||
department=department,
|
||
defaults={"is_active": True, "date_from": timezone.now().date()},
|
||
)
|
||
if not created and not emp_dep.is_active:
|
||
emp_dep.is_active = True
|
||
emp_dep.date_from = timezone.now().date()
|
||
emp_dep.date_to = None
|
||
emp_dep.save()
|
||
|
||
messages.success(request, f"{employee.get_full_name()} приглашён в отдел!")
|
||
return redirect("employees:department_detail", pk=pk)
|
||
else:
|
||
form = InviteToDepartmentForm(department)
|
||
if not form.fields["employee"].queryset.exists():
|
||
messages.info(
|
||
request,
|
||
"Нет сотрудников, которых можно пригласить — похоже, все уже в отделе.",
|
||
)
|
||
|
||
return render(
|
||
request,
|
||
"employees/invite_to_department.html",
|
||
{"form": form, "department": department},
|
||
)
|
||
|
||
|
||
# =========================
|
||
# Навыки
|
||
# =========================
|
||
|
||
|
||
@login_required
|
||
@require_POST
|
||
@require_api_auth
|
||
def skill_add(request, pk: int):
|
||
api = get_api_client(request)
|
||
skill_id = request.POST.get("skill_id")
|
||
skill_name = (request.POST.get("skill_name") or "").strip()
|
||
payload = {
|
||
k: v for k, v in (("skill_id", skill_id), ("skill_name", skill_name)) if v
|
||
}
|
||
resp = api.post(f"v1/employees/{pk}/add_skill/", data=payload)
|
||
return JsonResponse(resp.json or {"detail": resp.text}, status=resp.status or 500)
|
||
|
||
|
||
@login_required
|
||
@require_POST
|
||
@require_api_auth
|
||
def skill_remove(request, pk: int):
|
||
api = get_api_client(request)
|
||
skill_id = request.POST.get("skill_id")
|
||
skill_name = (request.POST.get("skill_name") or "").strip()
|
||
payload = {
|
||
k: v for k, v in (("skill_id", skill_id), ("skill_name", skill_name)) if v
|
||
}
|
||
# NB: у DRF-экшена underscore: remove_skill
|
||
resp = api.post(f"v1/employees/{pk}/remove_skill/", data=payload)
|
||
return JsonResponse(resp.json or {"detail": resp.text}, status=resp.status or 500)
|